1. Introduction: What Are We Actually Signing?
Here is a sentence you have read a hundred times: "Alice signs the transaction with her private key, proving she authorised it."
Cool. What does that actually mean?
Not "what is a digital signature" in the abstract, you already know that a signature is some math that only the private-key holder could have produced and anyone with the public key can check. What I mean is: what bytes, exactly, does Alice's wallet hand to the signing algorithm, and who decided that those particular bytes mean "send 1 ETH to Bob"?
Because here is the thing nobody says out loud often enough: cryptographic signature algorithms do not know what a transaction is. They do not know what a smart contract is. They do not know what "1 ETH" or "Bob" or "authorise" mean. ECDSA and EdDSA are functions that take a private key and some bytes and spit out other bytes. That is it. Everything that makes those bytes mean something, everything that turns "some bytes got signed" into "Alice approved this specific transfer", is bolted on around the signature, not inside it.
This gap between cryptographic fact ("this signature is valid for this digest under this public key") and semantic meaning ("Alice authorised sending 1 ETH to Bob") is the entire subject of this article. Once you have a rock-solid mental model of that gap, Part 2, which is basically a catalogue of ways people fall into it, will feel obvious instead of scary.
So here is our roadmap, and it is the same roadmap every signature on every chain follows in one form or another:
human intent → structured data → encoded bytes → digest → signature → transmission → verification → authorisationWe are going to follow one thread through all eight of those stages: Alice wants to send 1 ETH to Bob. We will watch that plain-English sentence get progressively less human-readable and more machine-precise at every step, until it is a blob of bytes that a wallet signs, a node verifies, and a blockchain accepts as fact. Later, when we get to smart contracts, we will bring in a second thread, Alice authorises Bob to spend 100 USDC on her behalf, because that one takes a different path through the pipeline, and the contrast is where most of the interesting engineering lives.
One warning before we start: I am going to use real algorithms (ECDSA over secp256k1, EdDSA over Ed25519) and mostly-real encodings (RLP, Keccak-256, EIP-712), but private keys, specific hex values, and toy numbers throughout are illustrative, not live. Do not send funds to any address in this article. It will not go well for you.
Let us start disassembling "Alice sends 1 ETH to Bob."
2. From Human Intent to Bytes
"Alice sends 1 ETH to Bob" is a sentence. Sentences are a terrible input to a signature algorithm, for a very specific reason: they are ambiguous, and signatures despise ambiguity.
If Alice's wallet signed the literal UTF-8 string "Alice sends 1 ETH to Bob", a verifier would have no idea which Bob, how many wei that "1 ETH" resolves to, which nonce this is meant to consume, which chain it is valid on, or how much gas Alice is willing to pay. English is great for humans and useless for consensus. We need something with zero room for interpretation.
That is what a canonical encoding buys you: a deterministic function encode(data) -> bytes such that the same logical transaction always produces the exact same byte string, no matter who encodes it or when. If encoding were not deterministic, two honest parties could disagree about what bytes represent "the same" transaction, and a signature only proves something about specific bytes, so any wiggle room in encoding is wiggle room in what got authorised.
Turning intent into fields
First, Alice's wallet turns her intent into structured transaction fields. For a simple EVM legacy-style transaction, that is something like:
| Field | Value | Meaning |
|---|---|---|
nonce | 42 | Alice's account transaction counter, prevents replay of this specific tx |
gasPrice | 20000000000 | 20 gwei |
gasLimit | 21000 | Standard ETH transfer cost |
to | 0xB0B0... | Bob's address |
value | 1000000000000000000 | 1 ETH, in wei |
data | 0x | Empty, plain transfer, no contract call |
chainId | 1 | Ethereum mainnet |
Notice something: "1 ETH" became 1000000000000000000 wei, and "Bob" became a 20-byte address. The human intent has already lost some of its human-friendliness in exchange for precision. That trade only gets more aggressive from here.
Serialisation: RLP
Ethereum encodes these fields using RLP (Recursive Length Prefix), a deliberately minimal encoding whose entire job is "turn nested lists of bytes and integers into an unambiguous byte string, and vice versa." RLP does not care about types the way JSON or Protobuf do. Everything is either a byte string or a list of byte strings, with length prefixes telling you how much to read. The point is to make it aggressively boring, boring is easy to make deterministic.
Simplified (skipping the exact prefix-byte arithmetic, if you want the full rule set, ethereum.org's RLP reference is the cleanest write-up and worth a detour if you are implementing an encoder yourself), Alice's transaction RLP-encodes into something like:
0xec 2a 8504a817c800 825208 94 b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0
880de0b6b3a7640000 80 01 80 80Each chunk corresponds to one field in order: nonce, gasPrice, gasLimit, to, value, data, and (for a legacy tx pre-signing) placeholders related to chainId per EIP-155. You do not need RLP's prefix rules to get the point:
This is now a single, unambiguous byte string.
There is exactly one way to encode this transaction and exactly one way to decode these bytes back into the same fields. Nothing about "1 ETH to Bob" survives as text, it is all positional byte offsets now.
Hashing: from bytes to digest
We are not done. We do not sign the RLP bytes directly, we hash them first, using Keccak-256:
digest = keccak256(rlp_encoded_tx)
= 0x7a3f... (32 bytes)Why hash before signing rather than signing the raw encoded bytes? A few compounding reasons, all of which resurface constantly:
- Message hashing is fixed size. ECDSA and EdDSA operate on inputs (effectively, the "message" input to the signing math as a byte string). Transactions can be arbitrarily long (that
datafield for a contract call could be kilobytes). Hashing collapses any input length down to a constant 32 bytes, so the signing algorithm does not care whether you are signing a 100-byte transfer or a 50KB contract deployment. - Performance. Elliptic-curve operations are comparatively expensive. Signing a 32-byte digest is fast regardless of how large the original message was; signing large messages directly (in schemes that even allow it) would scale signing cost with message size for no cryptographic benefit.
- Collision resistance does the real work. For the digest to safely stand in for the original message, it has to be computationally infeasible to find two different transactions that hash to the same digest. If someone could find
tx_a ≠ tx_bwithkeccak256(tx_a) == keccak256(tx_b), they could get Alice's signature overtx_a's digest and then claim it is also a valid signature overtx_b, because as far as the math is concerned, a signature is over the digest, not over "the transaction" as a concept. Keccak-256's 256-bit output makes finding such a collision astronomically expensive with current techniques (~2^128 work by the birthday bound), comfortably beyond "not happening."
So by the end of this section, "Alice sends 1 ETH to Bob" has become:
0x7a3f8e21c9b4... (32 bytes, and this is the ONLY thing the signing
algorithm will ever see)Everything downstream, ECDSA, EdDSA, verification, ecrecover, operates on this. Not on "a transaction." Not on "Alice's intent." On 32 bytes that happen, if you trust the encoding pipeline that produced them, to uniquely represent Alice's intent. That "if you trust the encoding pipeline" clause does a lot of work in Part 2.
3. The Anatomy of a Digital Signature
Let us get precise about what a signature actually is, because "proves the key holder authorised it" is doing some hand-waving we now need to unpack.
A signature scheme gives you three functions:
KeyGen() -> (privkey, pubkey)
Sign(privkey, digest) -> signature
Verify(pubkey, digest, signature) -> true | falseStructurally, pubkey is derived from privkey via some one-way operation (in elliptic-curve schemes: scalar multiplication of a base point, pubkey = privkey · G, easy to compute forward, believed computationally infeasible to invert). Sign combines privkey with the digest (and, depending on the scheme, some nonce) to produce a signature. Verify uses only the public key, the digest, and the signature to return a boolean, no private key required, which is the point of asymmetric cryptography.
Here is the precise claim a valid signature supports:
Whoever produced this signature had access to the private key corresponding to this public key, at the time this specific digest was signed.
That is it, and it is narrower than it sounds. Now let us be very deliberate about what that claim does not include, because every item on this list is something people quietly assume a signature guarantees when it does not:
- It does not prove intent. A signature proves a key was used; it says nothing about why. A key can be used under duress, by malware, by a compromised signer, or by a human who did not read what they were signing (this last one is going to become extremely relevant when we get to wallet UX around raw message signing).
- It does not prove the digest represents what you think it represents. If your encoding pipeline is broken or ambiguous, the digest might not correspond to the transaction you believe it does. The signature is only ever a statement about the digest, not about the human-readable meaning someone has attached to it.
- It does not prove freshness or uniqueness. ECDSA/EdDSA verification does not know or care whether this exact signature has been seen before. (This is the premise of replay attacks, flagged here, fully explored in Part 2.)
- It does not prove the key has not been compromised. Cryptographic validity and "this key is still solely controlled by the person you think controls it" are unrelated questions.
So why do we treat "valid signature" as "authorised" in practice? Because everything around the signature, the encoding pipeline, the chain ID in the digest, the nonce, the verifier's business logic, is engineered specifically to narrow that gap until, for well-designed systems, the cryptographic claim and the semantic claim line up. That narrowing is the craft of signature integration, as opposed to signature algorithms. Sections 6 through 8 are largely about how that narrowing is done well (and Part 2 is about how it is done badly).
One more role: the verifier. A signature is not self-verifying in a vacuum, someone has to run Verify(pubkey, digest, signature) and decide which pubkey to check against and what counts as the correct digest. For a blockchain transaction, that verifier is any full node. Each node independently re-derives the digest and validates signatures using deterministic rules, never trusting peers' claims about signature validity. That is different from a majority vote, signature validity is not something 51% of the network decides, it is something each node can and does check unilaterally, and a node that finds a signature invalid will reject the block regardless of what anyone else thinks. In PoW/PoS, a single invalid signature makes the entire block invalid. Any invalid block will never be accepted by an honest node. For a signed message handed to a smart contract, on the other hand, the verifier is that contract's code, and as we will see in Section 7, the contract can get this wrong in ways the protocol-level rules generally cannot.
With that model locked in, signature as a narrow cryptographic claim about a digest, meaning assembled around it by everything else, let us look at the two algorithm families that do the signing.
4. ECDSA: Signing and Verification
ECDSA, Elliptic Curve Digital Signature Algorithm, is what actually gets called when Alice's wallet asks her private key to produce a signature over that digest we ended Section 2 with. It is the workhorse behind Bitcoin, Ethereum, and without being too dramatic, basically the entire internet. ECDSA has a habit of getting explained in a single dense paragraph of Greek letters that loses everyone halfway through, so we will build it up one piece at a time.
The curve, and getting the equation right
You will often see secp256k1 written as y² = x³ + 7. That is correct, but it is a simplification of a more general shape, and the "why" is instructive. The general family of curves ECDSA can run on (short-Weierstrass curves) looks like:
y² = x³ + a·x + b (mod p)secp256k1 is this general equation with two specific constants plugged in: a = 0 and b = 7. Setting a = 0 is not arbitrary laziness, it kills the a·x term entirely, which simplifies the point-doubling formula (the operation you do over and over when computing k · G) and shaves off real computation in every single signing and verification operation, at scale that adds up. b = 7 was chosen, alongside the prime p and the base point G, by the curve's original designers, though the specific rationale for these constants is not well documented. None of this changes the math we are about to walk through. y² = x³ + 7 is not a special ECDSA-only formula; it is the short-Weierstrass equation with Bitcoin and Ethereum's chosen constants baked in.
This equation is evaluated over a large prime field p (secp256k1's p is a specific 256-bit prime), and the set of (x, y) points satisfying it, plus one extra "point at infinity" that acts as a zero element, form a mathematical group under an addition operation defined geometrically: draw a line through two points on the curve, find where it hits the curve a third time, flip that point over the x-axis, and that is your sum. (Interactive visualisation of this geometric addition.) Do this enough times in a row and you get scalar multiplication, written k · G, meaning "add the point G to itself k times" (computed efficiently via a doubling trick, not by literally looping k times, which would be hopeless for a 256-bit k).
ECDSA notation throws a lot of single letters at you at once, and much of the confusion in learning it is not knowing which letter means what:
| Symbol | What it is |
|---|---|
G | A fixed, publicly-known point on the curve, the "generator." Everyone signing on this curve uses the same G. |
n | The order of the curve, a huge prime number that tells you how many distinct points you can reach by repeatedly adding G to itself before it wraps back to the start. All the arithmetic in the signing/verification equations below is done modulo n, not modulo p. |
privkey | Alice's private key, just a randomly chosen 256-bit integer, secret. |
pubkey | Alice's public key, a point on the curve, computed once as pubkey = privkey · G. Easy to compute forward; believed infeasible to reverse. |
k | A one-time, per-signature secret number Alice's wallet generates fresh for this specific signature, not to be confused with the long-term privkey. |
R | The curve point you get from k · G, an intermediate result used only during this one signature. |
r | Just the x-coordinate of R, reduced modulo n, one half of the final signature. |
s | The other half of the final signature, a number that ties together r, the digest, privkey, and k. |
z | The message digest (our keccak256(rlp_tx) from Section 2) reinterpreted as a plain integer, since the signing equations need a number, not a byte string. |
The security of the whole scheme rests on one hard problem: given the public point pubkey = privkey · G, finding privkey back out is believed to be computationally infeasible, with the best known attacks like Pollard's Rho algorithm still requiring roughly 2^128 operations for a 256-bit curve. That one-way trapdoor, trivial to go from privkey to pubkey, infeasible to go back, is the foundation everything below is built on.
Key generation
privkey = random integer in [1, n-1]
pubkey = privkey · GThat is it. privkey is a number; pubkey is a point (usually represented as 64 bytes, 32 for x, 32 for y, or compressed to 33). Alice generated this pair once, when she first created her wallet, long before she ever thought about sending anyone any ETH.
Signing: turning Alice's digest into a signature
Now Alice actually wants to sign something, the digest z we produced at the end of Section 2 from her "send 1 ETH to Bob" transaction. Here is what her wallet does:
1. Generate a nonce k, a fresh, secret, uniformly random number in [1, n-1]
2. Compute the point R = k · G
3. Take r = R's x-coordinate, reduced mod n
4. Compute s = k⁻¹ · (z + r · privkey) mod n
5. The signature is the pair (r, s)Walk through what is happening in plain terms: Alice's wallet picks a fresh, throwaway secret k, used for this one signature and never again, turns it into a curve point R, and then combines R's x-coordinate with the digest and her long-term private key into the second number, s. The final signature that gets attached to her transaction is just two numbers, (r, s), each roughly 32 bytes.
Step 1 carries the most weight here: k must be secret, uniformly random, and never, ever reused across two different signatures from the same key. What happens when that rule gets broken is a dedicated, gleefully detailed chapter in Part 2. For now: the requirement is load-bearing, and an astonishing number of real-world security incidents trace back to this line being violated.
Verification: how anyone can check it without the private key
Alice broadcasts (r, s) along with her transaction. Now a node needs to check: is this actually a valid signature over this digest, from the account that claims to have sent it? Here is the check, using only pubkey, no private key required:
1. Compute u1 = z · s⁻¹ mod n
2. Compute u2 = r · s⁻¹ mod n
3. Compute the point P = u1 · G + u2 · pubkey
4. The signature is valid if P's x-coordinate, reduced mod n, equals rThat probably looks like it came out of nowhere, so here is why it works. Start from the signing equation and rearrange it to isolate k:
s = k⁻¹(z + r·privkey)
⟹ s·k = k⁻¹·k·(z + r·privkey) (multiply both sides by k)
⟹ s·k = z + r·privkey (since k⁻¹·k = 1)
⟹ k = s⁻¹·(z + r·privkey) (multiply both sides by s⁻¹)
⟹ k = s⁻¹·z + s⁻¹·r·privkey (distribute s⁻¹)
⟹ k·G = (s⁻¹·z)·G + (s⁻¹·r)·privkey·G
⟹ R = u1·G + u2·pubkey (since privkey·G = pubkey, by definition)Look closely at that last line: the verifier's P = u1·G + u2·pubkey from step 3 is algebraically forced to equal exactly the same point R that Alice's wallet computed back in signing step 2, and it gets there using only public quantities: G, pubkey, and the signature itself. Nobody had to reveal k or privkey at any point in this whole exchange; the verifier is not "trusting" anything, it is reconstructing the one point in the universe that could only have come from someone who actually knew privkey when they picked that k. That is the entire magic trick of ECDSA, laid bare: the signing and verification equations are two different algebraic routes to the same destination, one that only someone holding the private key can walk forward, and one that anyone holding the public key can retrace to confirm they would end up in the same place.
Ethereum's v, and recovering Alice's address without her public key
Here is where Ethereum does something genuinely elegant with the raw algorithm above: it does not ask Alice to attach her public key to the transaction at all. It recovers it from the signature.
The trick hinges on a detail we glossed over: r is only the x-coordinate of R. For a given x-coordinate, there are, generically, two possible y-values on the curve (y and p − y), meaning two candidate points could have produced this same r. Ethereum's third signature value, v (historically the number 27 or 28, later folded into the chain-ID-aware scheme from EIP-155), simply tells a verifier which of those two candidates was the real R.
Once you know which candidate R is, recovering the public key is just another rearrangement of the same signing equation:
ecrecover(digest, v, r, s):
1. Use v to pick the correct point R from the two candidates matching r
2. pubkey = r⁻¹ · (s·R − z·G)(That formula falls out of the same algebra we just did above, solved for pubkey instead of for the verification check, everything here is still the same underlying identity, just aimed at a different unknown.)
And once a node has pubkey, an Ethereum address is nothing more than keccak256(pubkey)[-20:], the last 20 bytes of the hash of the uncompressed public key point. So for Alice's transaction, a node takes her broadcast (digest, r, s, v), runs ecrecover, gets a public key back, hashes it, and lands on 0xAlice..., all without Alice ever having shipped her public key or her address as a separate field. That is not a minor space-saving trick; it is why an Ethereum transaction has no from field at all. The sender is not derived not declared, and it is precisely because that derivation only produces the correct address when the signature was genuinely produced by the matching private key that a node can trust the result unconditionally, with zero need to "look up" who Alice claims to be.
So, tying it all the way back to where we started: Alice's wallet took the digest from Section 2 and 3, generated a fresh k, produced (r, s), worked out v, and appended all three to her transaction. Any node, anywhere, can now run ecrecover(digest, v, r, s), get Alice's address straight back out, and know with cryptographic certainty that whoever controls that account authorised this digest. We will watch this play out inside the full transaction lifecycle in Section 6.
5. EdDSA: A Different Approach
Let us imagine, for a moment, Alice has a second wallet on a different, EdDSA-based chain, and she wants to send the equivalent of that same 1-ETH transfer there. The intent-to-digest pipeline from Sections 2 and 3 looks basically the same shape, structured data, canonical encoding, a hash, but the moment that digest hits the signing algorithm, everything changes. This is where we look at EdDSA, and specifically its most common real-world flavour, Ed25519.
Before touching any equations, the core idea: EdDSA signing is deterministic. ECDSA, remember, needed Alice's wallet to generate a fresh, secret, random number k for every single signature (Section 4's bolded warning). EdDSA throws that requirement out entirely. Sign the exact same message with the exact same key twice, and you get the exact same signature, byte for byte, both times. No dice roll, no random-number generator involved at signing time at all. That is a deliberate design choice, and the way it is achieved is a clever bit of engineering.
The curve, briefly, and why its shape matters
Ed25519 runs on a curve called Edwards25519, which belongs to a different family than the short-Weierstrass curves (like secp256k1) we saw in Section 4. Its defining equation looks like this:
-x² + y² = 1 + d·x²·y² (mod p), p = 2²⁵⁵ - 19What matters for understanding why EdDSA works the way it does is one structural property this shape gives you: Edwards-form curves have "complete" addition formulas. In plain terms, that means there is a single formula for adding two points together that works correctly no matter what those two points are, including the awkward edge cases (adding a point to itself, adding a point to the zero element) that short-Weierstrass curves need separate special-case formulas for. The benefits are: fewer branches in the code, fewer edge cases to get wrong, and, fewer places for an implementation to accidentally leak information through timing differences and side channel attacks.
Key generation: one extra step that makes everything else possible
Here is where EdDSA quietly sets up the trick that makes determinism possible later. Generating an Ed25519 keypair takes one more step than ECDSA's "just pick a random number":
1. seed = 32 random bytes (this is what you
generate and store as
"the private key")
2. h = SHA-512(seed) (a 64-byte hash of that seed)
3. scalar = clamp(h[0:32]) (the first 32 bytes of h,
with a few specific bits
fixed, this plays the
role privkey played in
ECDSA)
4. prefix = h[32:64] (the second 32 bytes of h,
kept secret, used for
one job: generating
nonces later)
5. A = scalar · B (B is Ed25519's base point,
equivalent to G in Section
4; A is the public key)The important thing to notice: that random seed gets hashed once, into two separate 32-byte outputs. scalar does the job privkey did before. prefix exists so that, at signing time, Alice's wallet has a secret ingredient it can combine with the message to produce a nonce, without ever needing fresh randomness.
Signing: deriving the nonce instead of gambling on one
One more small but important difference before we get to the steps: EdDSA is designed to sign the message directly (call it M) rather than requiring you to hash it down to a fixed-size digest first the way ECDSA does, the hashing happens automatically, inside the scheme itself, as part of these steps.
1. r = SHA-512(prefix || M) mod L (L is Ed25519's curve order,
this r is EdDSA's version of
ECDSA's k, but look closely
at how it is produced)
2. R = r · B
3. k = SHA-512(R || A || M) mod L (a second hash, this k is
just a number used in the
next step, not to be confused
with ECDSA's random k)
4. S = (r + k · scalar) mod L
5. Signature = R || S (64 bytes total: 32 for R,
32 for S)Now compare step 1 here against ECDSA's signing step 1 back in Section 4. ECDSA needed Alice's wallet to reach out to some external source of randomness and pull a fresh secret number, every single time, and if that random-number source were ever weak or predictable, the whole signature falls apart (again, Part 2 has feelings about this). EdDSA's r is instead computed as a hash of two things Alice's wallet already has sitting right there: her secret prefix and the message she is about to sign. No external randomness required, no RNG call, nothing that can fail at signing time, which means signing is idempotent (sign the same thing twice, get the same signature both times), a property that turns out to be convenient for testing, for hardware wallets with limited entropy sources, and for any protocol that wants reproducible behaviour.
Verification: checking without ever seeing the private ingredients
Given: A (Alice's public key), M (the message), signature (R, S)
1. Recompute k = SHA-512(R || A || M) mod L
2. Check whether S · B equals R + k · AThis is the exact same algebraic-identity trick from Section 4, just a little cleaner here because Ed25519 checks a full point-for-point equality instead of reconstructing and comparing a single x-coordinate the way ECDSA's r = R.x mod n requires. Substitute the signing equation for S straight back in and watch it collapse the same way it did before:
S = r + k·scalar
⟹ S·B = r·B + k·scalar·B
⟹ S·B = R + k·A (since A = scalar·B, by definition) ✓Same underlying idea as ECDSA's verification derivation in Section 4: the verifier arrives at the identical point using only public information, and only someone who actually held scalar (equivalently, the original private seed) at signing time could have produced an S that makes this equation balance.
So how does this differ from what Alice did in Section 4?
A few concrete, practical differences, now that we've seen the mechanics of both:
- Signature shape. ECDSA hands you
(r, s), two scalars, and Ethereum bolts a thirdvbyte onto that for address recovery, 65 bytes total. EdDSA hands you(R, S), a full curve point plus a scalar, landing at 64 bytes, butRhere is a full point rather than a bare x-coordinate, so there is no equivalent lightweight "which candidate point was it" recovery trick available. - No public-key recovery. While the verification equation
S·B = R + k·Acan be rearranged toA = k⁻¹·(S·B − R), there is a circular dependency: the noncekis computed ask = SHA-512(R ‖ A ‖ M), which includes the public keyAyou are trying to recover. EdDSA-based systems therefore transmit the public key alongside the signature rather than recovering it likeecrecoverdoes. - Zero signing-time randomness required, ever. The headline difference: EdDSA needs no fresh entropy at signing time beyond what went into generating the key once, at the very start. ECDSA traditionally needs fresh, secret entropy for every single signature, though RFC 6979 enables deterministic ECDSA by deriving the nonce from the private key and message.
- Determinism is the point. If you are used to ECDSA, where every signature over the same message looks different because of the fresh
k, EdDSA's "same input always produces the same output" behaviour can feel unsettling at first glance, like something is wrong. Nothing's wrong; the security argument for EdDSA was never built on signature-to-signature variation the way some people instinctively assume it should be.
None of this makes one scheme strictly "better" than the other, it makes them different answers to the same underlying design question, shaped by different priorities. Bitcoin and Ethereum wanted cheap address recovery and were willing to accept per-signature randomness as the cost. Ecosystems that lean on Ed25519 wanted to eliminate the entire "what if the randomness is bad" failure surface and were willing to give up recovery to get it. What matters for you walking away from this section is that you now know why the two produce differently-shaped signatures and why they get integrated differently, which is exactly the context you will need when Part 2 starts talking about signature malleability in a way that, as you might now guess, does not apply to EdDSA in quite the same way it applies to ECDSA.
6. Following a Real Blockchain Transaction
We've built every piece separately, encoding, hashing, ECDSA. Now let us stop treating them as separate topics and just watch Alice's transfer walk through the whole pipeline, start to finish.
It starts, as it always does, with Alice's wallet turning her intent into fields: nonce = 42, gasPrice, gasLimit = 21000, to = Bob, value = 1 ETH in wei, data = 0x, chainId = 1, exactly as we laid out back in Section 2. The wallet RLP-encodes that field set into a single flat byte string, deterministically, so there is never any ambiguity about field order or representation, and then hashes those bytes with Keccak-256 to get a 32-byte digest. That digest is the only thing that is about to get signed; everything upstream of it was just careful bookkeeping to make sure the digest actually, unambiguously, represents "send 1 ETH to Bob."
Now Section 4 kicks in for real. Alice's wallet generates a fresh nonce k, computes R = k·G, and derives r and s from the digest and her private key, along with the recovery value v that tells anyone later which of the two candidate points R actually was. The final payload Alice broadcasts is not just the signature on its own, it is the original transaction fields with (r, s, v) appended, RLP-encoded one more time as the complete package that goes out over the wire. Her wallet hands this to whatever node it is connected to, and from there it propagates across the peer-to-peer network through the mempool, the waiting room where unconfirmed transactions sit until a block producer picks them up.
Picking up the verifier thread from Section 3: any full node that receives this transaction, and chooses to independently validate rather than just trust its peers, runs its own checks, using nothing but the rules baked into the client software everyone's running. It re-derives the digest itself by re-encoding the claimed fields and hashing them; if that does not match what the signature is actually over, something is malformed and the transaction gets rejected outright, full stop, no discussion needed with anyone else on the network. It runs ecrecover(digest, v, r, s) to pull Alice's address back out. It checks that address's onchain nonce matches the transaction's claimed 42, the first line of defence against replaying this exact signed transaction a second time, a topic Part 2 will dig into properly. It checks that Alice's account holds enough ETH to cover both the transfer and the gas. And it checks the chainId folded into the signing process (via EIP-155) matches the network this node is running, which is what stops a transaction signed for mainnet from also being valid on some other EVM-compatible chain. None of that is a vote. Every node re-derives the same deterministic answer for itself, which is why they all agree without having to coordinate.
From there, a block proposer picks the transaction up out of the mempool and includes it in a block; that block propagates across the network and, through whatever consensus process the chain uses to agree on the canonical ordering of blocks, eventually settles into the chain everyone treats as final. Once it is in, the EVM executes it: 1 ETH moves out of Alice's recovered address and into Bob's, Alice's balance drops further to cover the gas she paid, and, this part matters, her account nonce ticks up to 43. That last increment is precisely what makes replaying this exact same signed transaction fail the nonce check on any future attempt; the transaction has, in a very real sense, used itself up.
Trace that path one more time: at no point did any of it need to know "Alice sends 1 ETH to Bob" as an English sentence. Every single step operated on precisely-defined bytes, fields, then RLP bytes, then a digest, then a signature, then a recovered address, then a nonce and balance update. The sentence was purely for your benefit as a human reading this article; the chain itself only ever sees 0xec 2a 8504..., and everything about why that is trustworthy traces straight back to the guarantees we built up in Sections 3 and 4.
transaction fields → RLP encoding → keccak256 digest →
ECDSA signature (r,s,v) → ecrecover → matched address →
nonce/balance checks → block inclusion → state updateThat is the whole loop, gapless, start to finish.
7. When Signatures Meet Smart Contracts
Everything up to this point has been about transaction signatures, signatures whose meaning is nailed down by Ethereum's protocol-level rules. Every node agrees on exactly what a valid transaction signature proves, because that meaning is baked directly into the client software they are all running, as we just watched in Section 6.
What comes next is structurally different, and it is one of the two pivotal ideas in this article: message signatures, verified not by the protocol, but by smart contract code that some team of engineers wrote.
Why Alice would sign a message instead of a transaction
Let us bring in the second example we've been promising: Alice wants to authorise Bob to spend 100 USDC on her behalf, the classic ERC-20 approval pattern. The obvious way to do this is a direct onchain transaction calling approve(bob, 100e6), following the pipeline from Section 6. Simple, but it costs Alice gas, and increasingly, protocols would rather let Alice sign something offchain, for free, and let Bob, or some relayer acting on Bob's behalf, submit it onchain later and cover the gas themselves, using Alice's signature as proof she agreed to it.
That means Alice's wallet is now signing a message, not a transaction. That message does not get interpreted by Ethereum's protocol-level rules the way her ETH transfer did. It gets interpreted by whatever Solidity code the USDC contract's authors happened to write. Transaction-signature meaning is fixed, protocol-wide, for everyone. Message-signature meaning is defined per-application, by whoever built that particular contract, which means it can be defined carefully, or carelessly, and ECDSA itself has absolutely no opinion on which. The algorithm does not know or care what it is being asked to sign; it just does the math from Section 4, faithfully, regardless of what is riding on the outcome.
A first, naive attempt at verifying Alice's approval
Here is the crude first pass a contract author might reach for: hash Alice's approval data, say keccak256(abi.encode(alice, bob, 100e6)), have Alice sign that digest the way her wallet signed the ETH-transfer digest back in Section 4, and have the USDC contract verify it on receipt:
function verify(address alice, address bob, uint256 amount,
uint8 v, bytes32 r, bytes32 s) public pure returns (address) {
bytes32 digest = keccak256(abi.encode(alice, bob, amount));
address recovered = ecrecover(digest, v, r, s);
require(alice == recovered, "alice should be signer");
return recovered;
}Notice that ecrecover here is doing exactly what it did inside Section 6's transaction verification, same algorithm, same "reconstruct the point, derive the address" trick from Section 4, just called directly from Solidity instead of being invoked automatically by the protocol's own transaction-processing logic. This is the actual cryptographic core that makes the whole offchain-sign, onchain-verify pattern possible, and it is the same math we already understand.
But look at how much is now resting on the judgment of whoever wrote this particular contract, rather than on protocol-wide guarantees. What exactly got hashed, and is it guaranteed to be unique to this contract, this purpose, this occasion? Nothing about ECDSA prevents a signature Alice produced meaning "approve Bob for USDC" from also being, purely as a matter of arithmetic, a technically-valid input to some other contract's ecrecover call that happens to hash its own data down to that same digest. The algorithm has no built-in concept of "this signature was only ever supposed to mean one specific thing", that concept, if it exists at all, has to be engineered in by the people building on top of it. The exploit mechanics are squarely Part 2's job. What matters here is how thin that naive verify function is.
The earliest fix: eth_sign and its prefix
Ethereum wallets historically exposed a raw signing primitive called eth_sign, which would take arbitrary bytes and, before signing, prepend a fixed string: keccak256("\x19Ethereum Signed Message:\n" + len(message) + message). That prefix is not decorative, it exists to solve a nasty problem. Without it, a malicious dApp could ask Alice to "sign this message," where the bytes it hands her wallet are secretly a fully-formed, validly RLP-encoded Ethereum transaction, and the resulting signature would be completely indistinguishable from one Alice produced by genuinely authorising that transaction through the normal Section 6 flow. The prefix makes that impossible by construction: it guarantees the space of digests used for arbitrary message-signing can never overlap with the space of digests used for transaction-signing, because no validly-prefixed message can ever also be a validly-formed RLP transaction.
That is domain separation, in its earliest and crudest form, a mechanism specifically for making sure a signature produced for one purpose can never be mistaken for a valid signature for a different purpose. We will see a far more thorough version of this same idea in Section 8.
eth_sign had a real problem of its own, though: it let dApps ask users to sign completely opaque blobs of hex with zero human-readable context about what they were actually agreeing to. Alice's wallet would show her something like "Sign this message: 0x8f3a91b2..." with no way for her, or almost anyone, to tell whether that was an innocuous login challenge or a devastating authorisation handing away her funds. That is not a cryptographic weakness; ECDSA did its job perfectly. It is a legibility problem, humans signing things they cannot read, and it is precisely the gap EIP-712 was built to close, which is exactly where we are headed next.
8. EIP-712 and Structured Data
eth_sign's prefix from Section 7 solved one problem, making sure a signed message could never be mistaken for a signed transaction, but it left two others sitting right there unsolved. First, it did nothing for legibility: Alice's wallet still had no way to show her anything more meaningful than a wall of hex. Second, it did nothing to separate one application's messages from another's, nothing stopped a digest format one protocol used for "approve spender for amount" from accidentally colliding with a completely different protocol that happened to hash its own data the same way. EIP-712 is the standard that closes both gaps at once, by defining a structured, typed way to build the digest that gets signed.
The construction layers a few pieces of context directly into the final digest, each closing off a specific ambiguity:
- A domain separator, a hash of the signing contract's name, version, chain ID, and address, gets folded in first. This is what pins a signature to this specific contract, on this specific chain, at this specific version; reuse the same signature anywhere else and the domain separator comes out different, so the digest comes out different, so the signature simply does not verify there.
- A type hash, essentially
keccak256of the struct's field names and types, written out as a string, gets folded in next. This pins the shape of the data being signed, so a contract expecting anowner/spender/value/nonce/deadlinestruct cannot be tricked by a differently-shaped payload wearing the same digest. - The actual struct data, for our example, Alice's address, Bob's address,
100 USDC, her permit nonce, and a deadline, gets hashed together with that type hash. - All of this gets combined with a fixed prefix (Ethereum's own version of the
eth_signprefix trick from Section 7) into one final digest:keccak256("\x19\x01" || domainSeparator || hashStruct(message)).
That final digest is what Alice's wallet signs, using exactly the same ECDSA machinery from Section 4, nothing new there, and it is what the USDC contract reconstructs and checks via ecrecover, exactly as in Section 7's naive example, just built from unambiguous, contract-pinned, chain-pinned inputs instead of a loosely-hashed tuple.
The part that matters most in practice is not the cryptography, it is what this unlocks on the human side. Because the data being signed is now typed and structured rather than an opaque blob, a wallet can parse it and show Alice something like "Approve Bob to spend 100 USDC, expires August 31 2026" instead of a string of hex she has no real way to evaluate. A large share of real-world signing mistakes are human mistakes, not cryptographic failures, someone approving something they did not understand. EIP-712 does not fix that by itself, but it is the mechanism that makes it possible for a wallet to try.
9. From Signature to Authorisation
Let us put the two running examples side by side, because that contrast is where all of this lands.
Alice's ETH transfer: intent → transaction fields → RLP bytes → Keccak digest → ECDSA signature → broadcast → node-side ecrecover + nonce/balance checks defined by protocol consensus rules → state update.
Alice's USDC approval: intent → typed struct → domain separator + type-hash → hashStruct → EIP-712 digest → ECDSA signature → handed to Bob (or a relayer) → contract-side ecrecover + owner-match check defined entirely by whatever Solidity the USDC contract's authors wrote → state update.
Both threads end at the same cryptographic primitive, ECDSA signing and ecrecover-based verification, mechanically identical at that layer. But look at where the authorisation judgment gets made in each case. For the transaction, "this signature means this transfer is authorised" is a claim enforced by every single node running identical, protocol-mandated validation logic, there is no discretion, no room for one node to interpret the digest differently than another. For the USDC approval, "this signature means this approval is authorised" is a claim enforced by one specific contract's code, written by some specific engineering team, who had to correctly implement domain separation, correct type-hashing, correct field ordering, and correct nonce/deadline handling for that guarantee to hold.
That is the hinge this article has been building toward: cryptographic validity is a fact about bytes and keys. Authorisation is a semantic judgment layered on top, and the strength of that judgment is only ever as strong as the code and conventions surrounding the signature, not the signature algorithm itself. ECDSA and EdDSA are, in a real sense, the least interesting part of "is this authorised" once you zoom out far enough. They're rock-solid, mathematically. Everything interesting, and everything that goes wrong in practice, lives in the scaffolding we spent Sections 6 through 8 building around them.
10. What We've Learned, and What's Coming in Part 2
We followed one pipeline the whole way through:
human intent → structured data → encoded/serialised bytes → digest →
signing algorithm → signature → transmission → verification → authorisationAlice's "send 1 ETH to Bob" became RLP-encoded transaction fields, became a Keccak-256 digest, became an ECDSA signature (r, s, v), got broadcast, got ecrecover-ed back into an address independently by any full node that checked, and became state. Alice's "approve Bob for 100 USDC" took a structurally different path, typed data, a domain separator, a type-hash, an EIP-712 digest, through the exact same underlying ECDSA machinery, verified not by protocol consensus but by one contract's Solidity code. We looked at ECDSA's group-law mechanics and Ethereum's public-key-recovery trick in full, and we looked at Ed25519's deterministic-nonce design as a different architectural answer to the same underlying problem.
The throughline, if you take away exactly one thing: a signature is a narrow, precise cryptographic claim about specific bytes, nothing about intent, nothing about correctness, nothing about freshness, nothing about context, and everything that turns that narrow claim into "this action was authorised" is scaffolding built by engineers, not guaranteed by the algorithm.
That scaffolding is exactly what Part 2 is going to go after. Briefly, by name only, here is what is coming:
- Replay attacks, what happens when a validly-signed digest gets reused somewhere its signer never intended.
- Signature malleability, ECDSA's
(r, s)has a subtle property that lets a different validsexist for the same(r, digest, pubkey), and systems that assume signature uniqueness get bitten by it. - Nonce-related issues, remember that bolded warning in Section 4 about
k? That is a whole chapter. - Domain separation failures, what happens when the EIP-712 machinery from Section 8 is implemented incompletely or incorrectly.
- Message ambiguity, the naive
ecrecoverpattern from Section 7, and all the ways under-specified digests get exploited. - Implementation mistakes, the gap between "the algorithm is secure" and "this specific codebase implements it correctly," which turns out to be a very large gap in practice. Part 1 was how the machine works. Part 2 is how people take it apart.
