Skip to main content

Command Palette

Search for a command to run...

Ethereum Transaction Cryptography: From Clock Math to Wallet Addresses

Updated
23 min read

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

  • secp256k1

  • Public/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:

  • g

  • x

  • p

It is easy to compute y.

But if you know:

  • g

  • y

  • p

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

elliptic curve image 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 P and Q

  • The 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 P

  • Find 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:

  • G is a known generator point on the curve

  • k is the private key

  • Q is 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:

  • privateKey is a large integer

  • G is the standard generator point

  • publicKey is 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:

  1. Generate a random private key

  2. Derive the public key using scalar multiplication

  3. Hash the public key with Keccak-256

  4. Take the last 20 bytes of the hash

  5. 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:

  • r is part of the ECDSA signature

  • s is part of the ECDSA signature

  • v helps 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:

  1. Build the transaction data

  2. Serialize it

  3. Hash it

  4. Sign the hash with the private key

  5. Attach the signature

  6. 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:

  • nonce

  • to

  • value

  • gasLimit

  • maxFeePerGas

  • maxPriorityFeePerGas

  • data

  • chainId

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-s signature checks

  • Nonce 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:

  • ethers

  • viem

  • OpenZeppelin 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.