T2 · Cryptography Fundamentals
Why can a signature prove "it was me"?
- Capabilities practiced
- BuilderProtocol Literacy
- Hands on
- Generate a key pair locally, sign a message, verify it with the public key, then change one byte and see verification fail.
- AI Lab
- Have AI write a Merkle proof verification function. Construct a forged proof yourself and confirm it is rejected.
A Real-World Problem
Logging in to your DApp needs no password.
A user clicks "Connect Wallet," sees a dialog, clicks "Sign," and is logged in. Your backend receives two things: an address and a 132-character hexadecimal string. From those alone, it accepts that the request came from the owner of that address.
A traditional backend would need registration, a stored password hash, a comparison, and a session. None appears here. The server has never met this user and stores no secret about them.
More unsettling, that hexadecimal string is public. It travels in an HTTP request, might land in access logs, could be seen by an intermediary, and might be printed to your console by accident.
Once someone sees it, why can they not impersonate you?
How can characters anyone may copy prove "it was me"?
Thought Experiment
Reduce the problem: in a strange city, you must prove to someone you have never met that "I wrote this note."
Three constraints: you share no secret; you cannot reveal your secret; anyone may see and copy the note.
Version one: agree on a secret code and write it at the end of the note.
The problem is immediate. Once the reader sees the code, they can write an identical note.**The verifier becomes a forger.**This is a structural dead end, not an implementation detail.
Version two: write a one-way scrambled value calculated from the code instead of the code itself.
The reader cannot reverse the value to recover the code. But they do not need to: they can copy the value onto another note. The proof is not bound to the content, so one copy works forever.
Version three: combine the code with the note's contents, then scramble them together.
Now the proof is bound to the content. A different note will not match. But to recalculate it, the verifier must know the code—and we are back to version one.
Either the reader can verify it or they cannot forge it. With this design, you cannot have both.
An ordinary one-way function has reached its limit. To escape, you need something stranger:
A pair of numbers: one generates the proof, the other checks it, and the checking number cannot reveal the generating number.
Make the checking number public, so anyone can verify. Never give anyone the generating number, so nobody else can forge. The dead end is resolved.
Now return to the opening question: the 132-character string is not your secret. It is a one-time result calculated using your secret. Seeing it is useless for other messages, because it is valid only for the message you signed.
Your Decision
The backend must confirm that "this request came from the owner of address A." Which approach do you take?
What Happens
The four approaches expose the same dimensions:
| Approach | Must the verifier know a secret? | Bound to a specific message? | Replayable? | Consequence of leakage |
|---|---|---|---|---|
| Send the secret | Yes | No | Freely | All assets lost |
| Send a digest of the secret | No | No | Freely | Identity can be impersonated |
| Send a digest of secret plus message | Yes | Yes | No | Either party can forge |
| Send a publicly verifiable proof | No | Yes | Depends on message contents | Affects only that message |
The last row has an ambiguity worth examining: replay protection depends on what you put in the message, not on the cryptography.
A signature remains valid forever. If you sign only the word "login," it may be reused at any time, on any site, and on any chain. To make it single-use and context-specific, put a nonce, domain, chain ID, and expiration time inside the signed content.
Cryptography will not design that content for you. You must.
Build a Model
Three primitives and one composite structure.
**Primitive one: a one-way hash.**It maps input of any length to fixed-length output. Identical input always yields identical output; one changed byte radically changes the result; reversing the result is computationally infeasible.
It serves three purposes: give arbitrary data a stable short name; detect changes; and compress "a lot of data" into "one value."
**Primitive two: a key pair.**A private key is a random number; a public key is derived from it in one direction. Forward calculation is fast, reversal infeasible. An address is in turn derived from a hash of the public key, forming a one-way chain:
- Random number = private key
- Elliptic-curve operation → public key
- Hash and truncate → address
**Primitive three: signing and verification.**Signing takes a private key and a message digest and returns a value—on Ethereum, the r, s, and v parts total 65 bytes. Verification takes the message and signature, recovers the signer's public key, and derives the address.
Note the direction: verification is not "decrypting with a public key." It is recovering the public key from the signature. You do not need to know in advance who signed; the verification process tells you. This is why an Ethereum transaction does not need to carry the sender address—it can be calculated.
A complete identity proof looks like this:
- Server provides a message with a nonce and domain
- Wallet signs its digest
- Client returns the message and signature
- Server recovers the address
- Server compares the address and invalidates the nonce
Composite structure: compress many items into one value while retaining proof that any individual item belongs.
Suppose a list contains 300,000 addresses, and an on-chain contract must decide whether one address is on it. Writing the full list into the contract would cost far too much.
Hash each of the 300,000 entries to get leaves. Combine and hash neighboring leaves to get 150,000 values. Repeat layer by layer until only one value remains. Store that one value on-chain.
To claim, a user submits their entry and the sibling value at each level on the path from their leaf to the top—only about 19 values for 300,000 entries. The contract recomputes the path. If its top value equals the stored value, it accepts.
In one sentence, the structure proves that one item is in a collection without revealing the entire collection.
It appears everywhere on-chain: block headers commit to transaction lists, airdrops commit to recipient lists, and bridges commit to batches of messages. The shape is the same.
What It Is Called
A one-way function mapping input of arbitrary length to fixed-length output. Identical inputs produce identical outputs; changing one bit produces a very different output; reversal is infeasible.
Ethereum uses Keccak-256; Bitcoin uses SHA-256. They are not the same function. Mixing them up is a common beginner error and produces completely different addresses.
A random number. It is not "generated" so much as "drawn"—the quality of randomness determines security.
It controls both identity and assets. Traditional systems have no exact counterpart: a reset password may recover an account there; losing this key means permanent loss here.
A value derived one-way from the private key that can be shared openly. An address is a further hash and truncation of that public key.
Exposing a public key is safe, and addresses are meant to be public. The danger is exposing the private key or anything that can reveal it—including a nonce used during signing.
The signature algorithm used by Ethereum and Bitcoin, on the secp256k1 curve. Solana uses a different scheme, Ed25519, with different key lengths and signature formats.
ECDSA needs a one-time random nonce for each signature. Reuse that nonce even once, and the private key can be derived directly. The Real-World Case section includes actual failures.
A proof tied to a particular message. On Ethereum, it is 65 bytes: r and s are 32 bytes each, and v is a one-byte recovery identifier.
A signature proves only two things: the message has not changed, and the signer held the private key when signing. It cannot prove the signer understood the message.
A structure that repeatedly hashes pairs of items into one root value. Given one item and the path from its leaf to the root, anyone can independently verify its membership without holding the entire collection.
Path length grows logarithmically with the number of items: about twenty hashes suffice for a million entries. That logarithmic scale is its key advantage.
Hands-on
Everything happens on your computer: no chain, testnet, or real asset is involved. You will send no transaction. Use the keys only for this experiment, then delete them.
Generate a secp256k1 private key and derive its public key.
openssl ecparam -name secp256k1 -genkey -noout -out priv.pem
openssl ec -in priv.pem -pubout -out pub.pem
cat priv.pem pub.pemThe relationship is one-way: priv.pem can regenerate pub.pem at any time, but not the other way around. Delete pub.pem, regenerate it, and confirm it is identical; there is no extra information in the public key.
Prepare and sign a message.
printf 'transfer 100 to alice' > msg.txt
openssl dgst -sha256 -sign priv.pem -out sig.bin msg.txt
xxd sig.bin | headLook at the length of sig.bin. It does not depend on message length: the signature operates on a digest of the message, not its full bytes directly.
Verify with the public key.
openssl dgst -sha256 -verify pub.pem -signature sig.bin msg.txtYou should see Verified OK. Notice that verification uses only pub.pem; the private key is absent. That resolves the thought experiment's dead end.
Change one byte and verify again.
printf 'transfer 100 to blice' > tampered.txt
openssl dgst -sha256 -verify pub.pem -signature sig.bin tampered.txtVerification fails. Only one letter in the name changed, but the digest is entirely different, so the signature no longer matches.
Try leaving the message alone and changing one byte in the signature file: that fails too. Signature verification has no "close enough" result—only pass or fail.
Inspect a real on-chain signature.
Open any wallet, find "Sign Message," sign a short message of your own, and copy the result.
Count its characters: 0x plus 130 hexadecimal characters is 65 bytes. The first 64 characters are r, the next 64 are s, and the final two are v.
Compare it with the OpenSSL output. The curve is the same, but Ethereum uses Keccak-256 rather than SHA-256, a fixed 65-byte format rather than DER encoding, and a fixed prefix before hashing a personal message.**The mathematics is the same; encoding and preprocessing differ.**This distinction often causes cross-language verification bugs.
Read the text you are signing.
Return to the wallet and find a real DApp login signature. Read the whole dialog. Does it contain a domain, nonce, expiration time, and chain ID?
Each missing field shows a dimension along which the signature can be replayed.
AI Lab
Work in two steps. The second is the important one.
Step one:
In my chosen language, write a function to verify a Merkle proof.
Inputs: leaf, proof array, root. Output: true or false.
State your assumptions about the hash function, child order, and distinction between leaves and internal nodes.
Step two:
Now act as an attacker. Given the implementation you just wrote,
construct inputs that make it return true for an item that is not actually in the tree.
If you think this is impossible, identify the exact line that prevents it.The second step matters because Merkle verification has a classic flaw: a second-preimage attack. If leaves and internal nodes are hashed the same way, an internal node can be submitted as a leaf with a shorter proof and still reach the correct root. Prevent it by adding different prefixes to leaves and internal nodes or enforcing different lengths.
The model may write a vulnerable first version and then claim the attack is impossible. Paste the flaw described above and ask it to rewrite the function.
Your exercise is not merely writing this function; it is learning what to ask.
If it supplies a runnable example and invents an airdrop contract address or Merkle root, check it on-chain. It may not exist.
Verify these points yourself after using AI
- It uses the hash function of the target chain and does not mix up SHA-256 and Keccak-256
- It specifies the order for combining child nodes instead of leaving it implicit
- It distinguishes leaves from internal nodes so an internal node cannot be passed off as a leaf
- The library and function signature it cites exist in official documentation
- It converts hexadecimal strings and byte arrays correctly, without counting the two 0x characters as bytes
- It does not confuse a valid proof with valid data; whether the root itself is trustworthy is a separate question
Real-World Case
ECDSA requires a one-time nonce for every signature. If the same private key signs two different messages with the same nonce, anyone holding both signatures can derive the private key with basic algebra.
In 2010, a game console's code-signing system used a constant nonce. Its private key was publicly calculated, invalidating the signing system. In 2013, a mobile system's random-number generator had a flaw that caused some wallets to reuse nonces, and the coins at those addresses were gradually drained.
The lesson: **the cryptographic algorithm was not broken; its implementation was.**The nonce generation in signature logic is one of the last things you should implement yourself.
A user clicks "Sign" on a fake site. The dialog shows incomprehensible hexadecimal text. The user signs.
The message is actually an order authorization to sell an NFT from the wallet to the attacker for zero. The signature is valid, the chain shows nothing abnormal, and the wallet has no reason to block it: the user agreed, without understanding what they agreed to.
Such attacks caused extensive losses in 2022–2023. Structured signing helps: expose the signed fields so the wallet can render them in plain language, and bind the domain and chain ID. T13 covers this layer in detail.
Early transaction formats did not include a chain ID. A transaction broadcast on a testnet could therefore also be valid when broadcast unchanged on mainnet.
Later standards put the chain ID into signed transaction data to prevent that replay. But applications must still handle off-chain signed messages themselves. Many DApp login signatures still lack a chain ID or expiration, so a signature captured a year ago may still log in today.
Writing the full recipient list into a contract costs too much. Requiring a project signature for each claim would force its server to stay online.
With a Merkle root, the chain stores one 32-byte value, while the list can live anywhere public. Users bring their own proof path. Claims still work if the project server goes down.
This is one of the most common application-level uses of Merkle trees, and one you may implement yourself.
Change One Variable
Two different inputs yield the same digest. A Merkle proof could then point to two different items, or a contract bytecode check could accept replacement code.
The entire approach of "a digest stands for data" would fail across applications, not just in one product. This is why chains are extremely conservative when choosing hash functions and may favor a slower, established one.
That is a brain wallet. An attacker need not break the algorithm; they can try common poems, lyrics, and password dictionaries one by one.
Such addresses can be drained within seconds. Security lies in the random number's entropy, not just the algorithm.
The signature becomes a permanent pass. Server logs, error-reporting systems, and captured network traffic can all leak the credential.
More subtly, even if the user later revokes all approvals for the address, the login signature still works. It never went on-chain, so nothing on-chain can revoke it.
The curve and algorithm differ. An address is the public key itself rather than a digest of it, so the public key can be read directly from the address.
One consequence: a Solana transaction must explicitly list all signers, whereas Ethereum recovers the sender from a signature. T6 explains the account-model difference behind this.
Takeaway Questions
Signature verification itself does not require a blockchain; SSH and HTTPS have used it for decades. What requires a chain here is that "the verification rules are the same for everyone, and nobody can alter them." Public-key recovery belongs to the protocol, not one company's server.
If a private key is lost, its holder bears all the risk with no recourse. Traditional finance allocates responsibility very differently. That is why every key-related design later in this course needs special care: multisignature, social recovery, and layered permissions all redistribute that risk.
If an AI Agent receives a private key, its permission is "everything at this address," with no intermediate level. Giving it narrower authority requires a signed authorization limited by amount, recipient, and time. Decide what authority to grant before deciding how to sign. Reverse that order and a prompt may be enough to drain the Agent's funds.
Chapter Check
A signature is a one-time result calculated from a private key for one specific message. Deriving the private key from it is computationally infeasible, and it is valid only for that message.
The private key is the ability to sign any message. It is not one credential; it is a credential factory.
No. On Ethereum, verification recovers the public key from the signature and message, derives an address, and compares it with the claimed address.
That is why transactions need not carry a sender address—it is calculated. It also explains how an address that has never interacted with the server can be verified on its first visit.
Cryptographically, a signature stays valid forever and can be submitted again. Replay resistance depends entirely on what the signed message contains.
Put the constraints in the message: a single-use nonce that the server invalidates after verification, domain, chain ID, and expiration. Omit one, and replay remains possible along that dimension.
It proves one item belongs to a collection without revealing the entire collection.
Cost falls from linear to logarithmic: a million items require only about twenty hashes in a proof. Designs that "store one commitment on-chain and the full dataset off-chain" often rest on this structure.
It does not establish that the root is trustworthy. If the root is wrong, proofs will correctly point to the wrong list.
There is no single standard answer. Check the following:
- Does the message include a server-issued single-use nonce, invalidated immediately after verification?
- Does it include the domain, preventing reuse on another site?
- Does it include the chain ID and expiration time?
- Is it structured so the wallet can display it in plain language, rather than bare hexadecimal text?
- Is the recovered address compared without case sensitivity, accounting for checksum formatting?
- Are "signing in" and "authorizing asset movement" clearly separated? Login should not look like approving a transfer, and asset authorization must never be hidden inside login.
If you included points 1 and 6, you understood the core lesson.
Key idea
You never send your private key; you only send a proof that anyone can verify.