Ethereum Transaction Cryptography: From Clock Math to Wallet Addresses
Crypto wallets look simple from the outside.
Click button. Sign transaction. Send ETH.
Under the hood?
A tiny cryptographic machine wakes up, does math over a giant finite field, touches an elliptic curve, produces a digital signature, and somehow your wallet address pops out of the other side.
Very normal.
This article explains that machine.
We’ll go from:
Modular arithmetic
Finite fields
The discrete logarithm problem
Elliptic curves
secp256k1Public/private keypairs
Digital signatures
Ethereum address derivation
How all of this shows up when you sign a transaction
No PhD required.
Just math, vibes, and a healthy respect for numbers too large to brute force.
The Big Picture
Ethereum accounts are built from cryptographic keypairs.
A keypair has:
A private key
A public key
The private key is secret.
The public key is derived from the private key.
The wallet address is derived from the public key.
Roughly:
random number
-> private key
-> public key
-> hash(public key)
-> Ethereum address
More specifically:
Generate random 256-bit number
-> private key
Private key * elliptic curve generator point
-> public key
Keccak-256(public key)
-> take last 20 bytes
-> Ethereum address
That is the wallet creation pipeline.
But to understand why this works, we need to start with something deceptively simple.
Clock math.
Modular Arithmetic: Clock Math for Computers
Modular arithmetic is arithmetic where numbers “wrap around.”
The easiest example is a clock.
If it is 10 o’clock and you add 5 hours, you get:
10 + 5 = 15
But clocks do not show 15.
They wrap around after 12.
15 mod 12 = 3
So:
10 + 5 ≡ 3 mod 12
That symbol ≡ means “congruent to.”
Basically:
15 and 3 are the same if we only care about remainders after dividing by 12
More examples:
17 mod 5 = 2
23 mod 7 = 2
100 mod 9 = 1
Because:
17 = 5 * 3 + 2
23 = 7 * 3 + 2
100 = 9 * 11 + 1
The modulo result is the remainder.
Why Modular Arithmetic Matters
Cryptography loves modular arithmetic because it gives us a fixed playground.
Instead of numbers growing forever, we force them to stay inside a range.
For example, in arithmetic modulo 7, the only possible values are:
0, 1, 2, 3, 4, 5, 6
If we add:
6 + 4 = 10
Then:
10 mod 7 = 3
So in this system:
6 + 4 ≡ 3 mod 7
Everything wraps around.
This gives cryptographic systems a clean mathematical world where operations are predictable, bounded, and efficient.
Also deeply unforgiving.
Which is on-brand.
Finite Fields: A Tiny Universe of Numbers
A field is a set of numbers where you can do:
Addition
Subtraction
Multiplication
Division
And still stay inside the set.
A finite field is a field with a limited number of elements.
For example, modulo arithmetic with a prime number gives us a finite field.
Take:
mod 7
The field contains:
0, 1, 2, 3, 4, 5, 6
We can add:
5 + 6 = 11
11 mod 7 = 4
We can multiply:
3 * 4 = 12
12 mod 7 = 5
We can subtract:
2 - 5 = -3
-3 mod 7 = 4
And we can divide, but division is a little weird.
In modular arithmetic, division means multiplying by an inverse.
For example:
3 / 2 mod 7
Means:
3 * inverse(2) mod 7
The inverse of 2 mod 7 is 4, because:
2 * 4 = 8
8 mod 7 = 1
So:
3 / 2 mod 7 = 3 * 4 mod 7
= 12 mod 7
= 5
That is field arithmetic.
A little strange at first.
But extremely useful.
Prime Fields
Ethereum’s elliptic curve math happens over a huge prime field.
A prime field is usually written like:
F_p
Where p is a prime number.
That means all calculations happen modulo p.
The values in the field are:
0 through p - 1
For Ethereum’s curve, p is enormous:
p = 2^256 - 2^32 - 977
Written out:
115792089237316195423570985008687907853269984665640564039457584007908834671663
Tiny little number.
Very cozy.
Every coordinate on Ethereum’s elliptic curve lives inside this finite field.
So when we do elliptic curve math, we are not drawing smooth curves over normal real numbers like in school.
We are doing modular arithmetic over a giant prime field.
That changes the picture a lot.
The Discrete Logarithm Problem
Before elliptic curves, let’s talk about the problem that makes this stuff useful.
The discrete logarithm problem is a one-way-ish math problem.
Easy in one direction.
Hard in the other.
Imagine this:
g^x mod p = y
If you know:
gxp
It is easy to compute y.
But if you know:
gyp
It is very hard to recover x.
That hidden x is the discrete logarithm.
Example:
3^x mod 17 = 13
Finding x by hand might be possible for tiny numbers.
But when the numbers are hundreds of bits long?
Nope.
Not with current computers.
Not before lunch.
Not before the sun expands.
This “easy forward, hard backward” property is the backbone of a lot of public-key cryptography.
Ethereum does not use the basic discrete logarithm problem directly.
It uses an elliptic curve version of it.
Naturally, because regular math was apparently too readable.
Enter Elliptic Curves
Image courtesy of globalsign.com
An elliptic curve is a set of points that satisfy an equation.
A common form is:
y^2 = x^3 + ax + b
Ethereum uses a specific curve called secp256k1.
Its equation is beautifully simple:
y^2 = x^3 + 7
That means:
a = 0
b = 7
So the curve is:
y^2 = x^3 + 7
But remember:
Ethereum does not use this over normal decimal numbers.
It uses it over a finite field.
So the actual equation is:
y^2 ≡ x^3 + 7 mod p
Where:
p = 2^256 - 2^32 - 977
A point (x, y) is on the curve if it satisfies that equation.
Elliptic Curves Over Real Numbers
Over normal real numbers, an elliptic curve looks like a smooth curve.
The important operation is point addition.
Given two points on the curve:
P + Q = R
There is a geometric way to define this:
Draw a line through
PandQThe line intersects the curve at a third point
Reflect that point over the x-axis
The result is
R
This gives us a way to “add” points.
Weird?
Yes.
Useful?
Very.
There is also point doubling:
P + P = 2P
For point doubling:
Draw the tangent line at
PFind where it intersects the curve again
Reflect that point over the x-axis
That gives 2P.
So elliptic curve math gives us operations like:
P + Q
2P
3P
4P
...
And eventually:
kP
Where k is a number and P is a point.
This is called scalar multiplication.
Elliptic Curves Over Finite Fields
Now take that same idea and move it into modular arithmetic.
Instead of a smooth curve, we get a scattered set of points.
There is no pretty continuous curve anymore.
Just valid (x, y) points inside a finite field.
But the group operation still works.
We can still do:
P + Q
2P
kP
All the formulas are done modulo p.
This gives us a finite mathematical group.
That group has a very useful property:
Given k and P, computing Q = kP is easy.
Given P and Q, finding k is hard.
That is the elliptic curve discrete logarithm problem.
And that is the magic door.
The Elliptic Curve Discrete Logarithm Problem
Ethereum uses this hard problem:
Q = kG
Where:
Gis a known generator point on the curvekis the private keyQis the public key
Forward direction:
private key * generator point = public key
Easy.
Reverse direction:
public key / generator point = private key
Not realistically possible.
That is why you can share your public key.
But you must never share your private key.
Your private key is the secret scalar.
Your public key is the curve point produced by multiplying the generator by that scalar.
What Is secp256k1?
secp256k1 is the elliptic curve Ethereum uses for account keys and transaction signatures.
Bitcoin uses it too.
The name breaks down roughly like this:
sec -> Standards for Efficient Cryptography
p -> prime field
256 -> 256-bit field size
k -> Koblitz curve
1 -> first curve in that category
The curve equation is:
y^2 = x^3 + 7
Over the finite field:
F_p
Where:
p = 2^256 - 2^32 - 977
It also has a standard generator point G.
The generator point is a fixed point on the curve.
Everyone uses the same G.
The private key changes.
So:
publicKey = privateKey * G
This produces a unique public key for that private key.
Private Keys
An Ethereum private key is basically a very large random number.
More precisely, it is an integer between:
1 and n - 1
Where n is the order of the generator point G.
For secp256k1, n is also a huge number:
115792089237316195423570985008687907852837564279074904382605163141518161494337
A private key usually appears as 32 bytes.
Example:
0x4c0883a69102937d6231471b5dbb6204fe512961708279d74f7c51c6c1c9e0c0
Do not use that key.
Do not use random keys from blog posts.
Do not use keys generated by “vibes.”
A wallet should generate private keys using a cryptographically secure random number generator.
If the randomness is bad, the wallet is cooked.
Not “slightly less secure.”
Cooked.
Public Keys
The public key is derived from the private key using elliptic curve scalar multiplication.
publicKey = privateKey * G
Where:
privateKeyis a large integerGis the standard generator pointpublicKeyis a point on the curve
That public key has two coordinates:
(x, y)
Each coordinate is 32 bytes.
So an uncompressed public key is usually:
64 bytes
Or sometimes:
65 bytes
If it includes the 0x04 prefix that means “uncompressed public key.”
Ethereum address derivation uses the raw x and y bytes.
So conceptually:
publicKey = x || y
That means:
32 bytes x-coordinate
+
32 bytes y-coordinate
=
64 bytes
Deriving an Ethereum Address
Now we can finally derive the address.
The process is:
Generate a random private key
Derive the public key using scalar multiplication
Hash the public key with Keccak-256
Take the last 20 bytes of the hash
Prefix with
0x
In pseudocode:
privateKey = random integer between 1 and n - 1
publicKey = privateKey * G
hash = Keccak256(publicKey)
address = last 20 bytes of hash
That is it.
An Ethereum address is not the public key itself.
It is the last 20 bytes of the Keccak-256 hash of the public key.
Example shape:
private key:
0x4c0883...
public key:
0x506bc1dc099358e5137292f4efdd57e400f29ba5132aa5d12b18dac1c1f6aab...
32-byte x
32-byte y
keccak256(public key):
0x...
address:
0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1
Again: blog example keys are for learning, not for money.
Please do not become the cautionary tale.
Why the Last 20 Bytes?
Ethereum addresses are 20 bytes, or 160 bits.
That is shorter than the full 32-byte hash.
Why?
Practicality.
A 20-byte address is still huge enough to make random collisions absurdly unlikely, while being shorter to store and display.
So Ethereum compresses the public identity into a shorter account identifier:
Keccak256(publicKey) -> 32-byte hash
last 20 bytes -> Ethereum address
This address is what users pass around.
The public key itself is usually revealed when the account signs a transaction.
Keccak-256 vs SHA3-256
Ethereum uses Keccak-256.
You may hear people call it SHA3.
That is slightly messy.
Keccak was the algorithm selected for SHA-3, but Ethereum uses the earlier Keccak-256 variant, not the finalized NIST SHA3-256 padding.
So for Ethereum addresses, the hash is:
Keccak-256
Not standardized SHA3-256.
Tiny detail.
Big difference.
Classic crypto footgun.
What About Checksums?
Ethereum addresses are often shown with mixed-case letters.
Example:
0x52908400098527886E0F7030069857D2E4169EE7
That casing is not random.
It comes from EIP-55, which adds a checksum using capitalization.
The lowercase version is the raw address:
0x52908400098527886e0f7030069857d2e4169ee7
The checksummed version uses uppercase letters in specific places so wallets can catch some typos.
It is not a security shield.
But it helps detect mistakes.
Which is good, because manually checking addresses is the UX equivalent of defusing a bomb through a keyhole.
Digital Signatures
Now let’s talk about signatures.
A digital signature proves that someone with a private key approved a message.
In Ethereum, users sign things like:
Transactions
Typed data
Login messages
Contract approvals
Random scary wallet popups that say “Sign this message” and give you emotional damage
A signature should prove:
The signer had the private key
The signed message was not changed
Anyone can verify the signature using public information
And importantly:
- The private key is never revealed
That is the whole trick.
ECDSA
Ethereum uses ECDSA over secp256k1.
ECDSA means:
Elliptic Curve Digital Signature Algorithm
At a high level, signing works like this:
message + private key -> signature
Verification works like this:
message + signature + public key/address -> valid or invalid
The signature proves the private key holder signed the message.
But the verifier does not need the private key.
That is the point.
What Is Inside an Ethereum Signature?
Ethereum signatures are usually represented with:
r
s
v
Where:
ris part of the ECDSA signaturesis part of the ECDSA signaturevhelps recover the public key
The classic Ethereum signature is 65 bytes:
r = 32 bytes
s = 32 bytes
v = 1 byte
So:
signature = r || s || v
The v value is sometimes called the recovery ID.
It helps Ethereum recover the public key from the signature and the signed message.
That matters because Ethereum transactions do not need to include the full public key directly.
The network can recover it from the signature.
Very neat.
Also the source of many “why is v 27 or 28 or 0 or 1 or chain-id-adjusted” debugging sessions.
Enjoy.
The Signing Flow
For a transaction, the simplified signing flow is:
Build the transaction data
Serialize it
Hash it
Sign the hash with the private key
Attach the signature
Broadcast the signed transaction
Conceptually:
transaction data
-> transaction hash
-> ECDSA sign with private key
-> signature {r, s, v}
-> signed transaction
The signature commits to the transaction contents.
If someone changes the transaction after signing, the signature no longer verifies.
So an attacker cannot simply take your signed transaction and change:
Send 0.1 ETH to Alice
Into:
Send 100 ETH to Mallory
The signature would break.
Cryptography says no.
For once, the computer is on your side.
What Exactly Gets Signed?
Ethereum does not sign a vague human sentence like:
Chris wants to send ETH
It signs structured transaction data.
A transaction includes fields like:
noncetovaluegasLimitmaxFeePerGasmaxPriorityFeePerGasdatachainId
Depending on the transaction type, the exact fields differ.
For a modern EIP-1559 transaction, the signed payload includes things like:
chain_id
nonce
max_priority_fee_per_gas
max_fee_per_gas
gas_limit
destination
amount
data
access_list
Then the wallet signs the hash of that encoded transaction.
The important idea:
The signature is tied to the exact transaction contents.
Change the contents, and verification fails.
Why Chain ID Matters
Before chain IDs were included in transaction signing, a transaction signed on one Ethereum-like chain could potentially be replayed on another chain.
That is bad.
Imagine signing a transaction on Chain A and having someone replay it on Chain B.
EIP-155 added chain ID into the signing process.
So the signature is bound to a specific chain.
Example:
Ethereum mainnet chainId = 1
Sepolia chainId = 11155111
If the chain ID is part of the signed data, then the transaction is valid only for that chain.
This is replay protection.
Boring name.
Very important feature.
Public Key Recovery
Ethereum has a fun trick.
Given:
The transaction hash
The signature
Ethereum can recover the signer’s public key.
Then it can derive the address from that public key.
That means the network can determine:
Who signed this transaction?
Without the transaction explicitly saying:
Here is my full public key.
The flow is:
transaction hash + signature
-> recover public key
-> Keccak-256(public key)
-> last 20 bytes
-> signer address
Then Ethereum checks whether that account is allowed to send the transaction.
For a normal externally owned account, the signature is the authorization.
Externally Owned Accounts vs Smart Contract Accounts
Ethereum has two main account types:
Externally Owned Accounts
Contract Accounts
Externally Owned Accounts, or EOAs, are controlled by private keys.
These are normal wallet accounts.
private key -> public key -> address
Contract accounts are controlled by code.
They do not have private keys in the same way.
Their addresses are derived differently, usually from:
The creator address
The creator nonce
Or for CREATE2:
Deployer address
Salt
Init code hash
This article is mostly about EOAs, because that is where secp256k1 keypairs and ECDSA signatures live.
Smart contract wallets can use different signature schemes internally.
Account abstraction makes this even more flexible.
Which is cool.
And also makes wallet architecture conversations approximately 4x longer.
Address Derivation in JavaScript
Here is a simple example using ethers.
import { Wallet } from "ethers";
const wallet = Wallet.createRandom();
console.log("Private key:", wallet.privateKey);
console.log("Address:", wallet.address);
That creates a random wallet.
Under the hood, the wallet has:
private key
public key
address
If you already have a private key:
import { Wallet } from "ethers";
const privateKey = "0x...";
const wallet = new Wallet(privateKey);
console.log(wallet.address);
Again:
private key -> public key -> address
The library handles the curve math.
Which is good.
You do not want to hand-roll this in production.
You want boring, audited cryptographic libraries.
Boring is good here.
Boring keeps funds where they belong.
Address Derivation Pseudocode
Here is the low-level shape:
privateKey = secureRandom(32 bytes)
if privateKey == 0 or privateKey >= secp256k1n:
reject and generate again
publicKeyPoint = privateKey * G
publicKeyBytes = encode(publicKeyPoint.x) || encode(publicKeyPoint.y)
hash = keccak256(publicKeyBytes)
address = hash[12:32]
Why hash[12:32]?
Because Keccak-256 returns 32 bytes.
The last 20 bytes start at byte index 12.
32 bytes - 20 bytes = 12
So:
address = last 20 bytes
Solidity: Recovering a Signer
In Solidity, you often verify signatures by recovering the signer address.
A simplified example:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SignatureVerifier {
function recoverSigner(
bytes32 messageHash,
uint8 v,
bytes32 r,
bytes32 s
) external pure returns (address) {
return ecrecover(messageHash, v, r, s);
}
}
Ethereum has a built-in precompile exposed in Solidity as:
ecrecover
It returns the address that produced the signature.
But production code should be more careful.
You usually want:
Message prefixing
Domain separation
Replay protection
Low-
ssignature checksNonce handling
Expiration timestamps
For production, use something like OpenZeppelin’s ECDSA utilities.
Because signature bugs are not “oops, layout broke.”
They are “oops, someone drained the protocol.”
Different energy.
Message Signing vs Transaction Signing
There are different things users can sign.
Transaction signing
This authorizes an onchain action.
Examples:
Send ETH
Call a contract
Deploy a contract
Approve token spending
This costs gas when broadcast and executed.
Message signing
This signs arbitrary data.
Examples:
Login with Ethereum
Prove wallet ownership
Accept offchain terms
Sign an order for a marketplace
This does not automatically submit a transaction.
But it can still be dangerous.
A signed message can authorize something in another system.
So wallet UX matters.
If a wallet popup shows unreadable hex and asks the user to sign, that is not informed consent.
That is a jump scare with a confirm button.
Why Domain Separation Matters
Signatures should be tied to a specific context.
Otherwise, the same signature might be reused somewhere else.
Bad.
Domain separation means the signed data includes context like:
App name
Version
Chain ID
Contract address
User nonce
Deadline
This helps ensure a signature for one purpose cannot be replayed for another.
EIP-712 typed data is popular for this.
It lets wallets show users structured signing data instead of raw bytes.
Example shape:
App: MyExchange
Action: Sell
Token: 1 ETH
Price: 3000 USDC
Deadline: June 7, 2026
Much better than:
0xa3f9c2b4000000000000000000000000...
One of these looks like a transaction.
The other looks like a cursed barcode.
The Security Model
Ethereum account security depends heavily on private keys staying private.
If someone gets your private key, they control the account.
They can:
Send ETH
Transfer tokens
Approve spenders
Move NFTs
Sign messages
Generally ruin your week
There is no password reset.
No support ticket.
No “forgot private key?” flow.
Just cryptographic ownership.
This is powerful.
It is also brutal.
That is why wallets usually protect private keys with:
Seed phrases
Hardware wallets
Secure enclaves
Password encryption
Multisig
Smart contract account policies
The core math is strong.
The weak point is usually everything around it.
Randomness.
Storage.
Phishing.
Bad signing UX.
Clipboard malware.
Humans being tired.
The usual production environment.
Why Randomness Is Everything
A private key must be unpredictable.
If the private key is generated with weak randomness, attackers may guess it.
This has happened in real systems.
Bad randomness can collapse the whole security model.
The private key is just a number.
If someone can predict the number, the game is over.
Good wallets use cryptographically secure randomness.
Bad wallets use things like:
Math.random()
current timestamp
user's birthday
"correct horse battery staple"
Please do not.
A 256-bit random private key has an unimaginably large search space.
But only if it is actually random.
Why You Should Not Implement Your Own Crypto
This is the part where every cryptography article says:
Do not roll your own crypto.
And yes.
That.
But let’s make it practical.
Do not implement your own:
Elliptic curve arithmetic
ECDSA signing
Random number generation
Key derivation
Signature verification rules
Unless you really know what you are doing and have audits lined up.
Use established libraries.
For Ethereum development, that usually means tools like:
ethersviemOpenZeppelin contracts
audited wallet libraries
hardware wallet SDKs
Your goal is not to be clever.
Your goal is to not lose money.
Different KPI.
Quick Recap
Here is the whole flow again.
1. Generate a private key
privateKey = random number between 1 and n - 1
2. Derive the public key
publicKey = privateKey * G
This uses elliptic curve scalar multiplication on secp256k1.
3. Derive the address
address = last20Bytes(Keccak256(publicKey))
4. Sign transactions
signature = ECDSA_sign(transactionHash, privateKey)
5. Verify signatures
signer = recoverAddress(transactionHash, signature)
If the recovered address matches the account, the transaction is valid.
The Mental Model
If you remember nothing else, remember this:
Private key = secret number
Public key = curve point derived from secret number
Address = shortened hash of public key
Signature = proof that the private key approved something
And the security comes from this:
private key -> public key is easy
public key -> private key is practically impossible
That “practically impossible” part is the elliptic curve discrete logarithm problem.
The curve is secp256k1.
The math happens in a finite field.
The address is the last 20 bytes of a Keccak hash.
The wallet hides most of this because, mercifully, nobody wants to think about finite fields before sending $12.
Final Thoughts
Ethereum transaction cryptography is elegant once you break it down.
Not simple.
But elegant.
A wallet starts with a random number.
That number becomes a private key.
The private key creates a public key through elliptic curve multiplication.
The public key becomes an address through hashing.
And the private key signs transactions without ever being revealed.
That is the whole dance.
The user sees:
Send
Confirm
Done
The protocol sees:
secp256k1
ECDSA
Keccak-256
public key recovery
nonce checks
signature validation
state transition
Both views are real.
One is for humans.
One is for the machine.
Good crypto products need to respect both.
Because yes, the math is beautiful.
But if the wallet popup still looks like a ransom note written in hexadecimal, we have work to do.
