JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Digital signatures: signing without revealing your secret

A digital signature lets anyone verify you authorized a message, while no one can forge it and your secret key never leaves your wallet. We open the hood on ECDSA and trace a real transaction from sign to verify.

A seal no one can carve

For three thousand years, the way you proved a letter came from you was a wax seal pressed with a signet ring. It worked because the ring was hard to copy — until someone stole the ring, or carved a good fake. Handwritten signatures have the same flaw: a forger with a steady hand can trace yours, and a photocopy is as 'valid' as the original. Worse, your signature looks the same on every document, so it tells you nothing about which page you actually agreed to.

A digital signature fixes both flaws at once. Anyone can verify it, no one can forge it, and it is welded to the exact bytes you signed — flip a single bit of the message and the signature stops verifying. In the last guide you built a key pair: a private key, which is just a giant secret number d, and a public key Q derived from it. Signing uses the private number; verifying uses the public point. The secret itself never moves.

What a signature must guarantee

Before the math, pin down what we actually want. A good signature scheme delivers three guarantees together: authentication (it really was the holder of the private key), integrity (the message was not altered by even one byte after signing), and non-repudiation (the signer can't later deny it, because only their key could have produced that signature).

You might know a simpler tool: a MAC (message authentication code), where two parties share one secret and tag messages with it. But a MAC can't prove anything to a third party — anyone who can verify the tag could also have forged it, because they hold the same secret. Public-key signatures break that symmetry: verification is public, forging is not. That public verifiability is precisely what trustlessness is built on — thousands of nodes who never met you can each independently check your transaction.

  1. KeyGen → produces a key pair (d, Q): the private key d stays in your wallet, the public key Q can be shared with the world.
  2. Sign(d, m) → takes your private key and a message m, and outputs a signature σ. Only someone holding d can run this.
  3. Verify(Q, m, σ) → takes the public key, the message, and the signature, and returns true or false. Anyone can run this; it never touches d.

Inside ECDSA: the signing equation

Bitcoin and Ethereum both sign with ECDSA — the Elliptic Curve Digital Signature Algorithm — over the curve secp256k1 you met last guide. Recall the setup from elliptic-curve cryptography: there is a fixed base point G on the curve, and a huge prime n (about 1.158 × 10⁷⁷) called the order — do n hops of G and you loop back to the start. Your private key is a secret number d in [1, n−1]; your public key is the point Q = d·G. Recovering d from Q means undoing that scalar multiplication, which is the elliptic-curve discrete-log problem: believed astronomically hard.

Signing doesn't sign the message directly — it signs the message's hash (a fixed 32-byte digest), so the cost is the same whether you sign one byte or a whole contract. Here is the actual algorithm:

# secp256k1 parameters
G = base point;   n = order of G        # n is a 256-bit prime, ~1.158e77
d = private key                         # secret integer in [1, n-1]
Q = d * G                               # public key (a point on the curve)

function sign(message, d):
    z = keccak256(message)              # 32-byte hash, read as an integer
    do:
        k = secure_random(1, n-1)       # the nonce: fresh, secret, single-use!
        (x1, y1) = k * G                # one scalar multiplication
        r = x1 mod n
        s = inverse(k, n) * (z + r * d) mod n   # inverse() is modular inverse
    while r == 0 or s == 0              # vanishingly rare; just retry
    return (r, s)                       # the signature is the pair (r, s)
ECDSA signing. The private key d appears only inside the term r·d, scrambled by the secret nonce k and the modular inverse — the signature reveals neither.

The signature is just two numbers, *(r, s)*. The first, r, is the x-coordinate of the random point k·G; the second, s, mixes the hash z, that same r, and your private key d together, all stirred by the secret one-time value k. Because k is fresh each time, the same key signing the same message twice produces two different signatures — and that is by design.

Verification: the algebra that just works

Now the other side. A verifier has your public key Q, the message, and the signature *(r, s)* — but never d or k. They run this:

function verify(message, (r, s), Q):
    if not (1 <= r < n and 1 <= s < n): return false   # reject junk
    z  = keccak256(message)
    w  = inverse(s, n)                  # w = s^{-1} mod n
    u1 = (z * w) mod n
    u2 = (r * w) mod n
    (x1, y1) = u1 * G + u2 * Q          # two scalar mults and one point add
    return (x1 mod n) == r              # valid iff the x-coordinate equals r

# Why it works (the whole point of the design):
#   u1*G + u2*Q  =  u1*G + u2*(d*G)  =  (u1 + u2*d) * G
#   u1 + u2*d    =  w*(z + r*d)  =  s^{-1}*(z + r*d)
#   but s = k^{-1}*(z + r*d), so s^{-1}*(z + r*d) = k
#   therefore the point is exactly k*G, whose x-coordinate is r.  QED.
ECDSA verification. The verifier reconstructs the point k·G using only public values — without ever knowing k itself. The comment block shows the algebra collapsing to k·G.

Read the comment block slowly, because it is genuinely beautiful. The verifier combines public numbers and ends up holding the point k·G — the very point only the signer could have built, since only the signer chose the secret k and knows the d that made the s check out. The x-coordinate of that reconstructed point must equal r. A forger who doesn't know d cannot pick an *(r, s)* that lands on the right point except by guessing a 256-bit number: hopeless.

Signing a real transaction

Let's make it concrete with an Ethereum transaction. When you send 1 ETH to a friend, your wallet assembles the fields (recipient, value, nonce, gas, chain id, data), serializes them, and runs Keccak-256 over the bytes to get a 32-byte hash z. That hash — not the raw transaction — is what ECDSA signs. The output is bundled as *(r, s, v)*, where the extra byte v is a recovery id.

Why the extra byte? Given just *(r, s)* and the message, you can actually recover the public key that signed it — there are only two candidate points with x-coordinate r, and v says which one. This is a small Ethereum-specific trick (`ecrecover`) that means the transaction doesn't need to carry your public key at all: the network recovers it from the signature, then hashes it down to your address (the subject of the next guide). A contract can do the same check on-chain:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract VerifySig {
    // n/2 for secp256k1 -- the EIP-2 "low-s" boundary (see next section)
    uint256 constant HALF_N =
        0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0;

    /// Recover the address that signed `hash` from an (r, s, v) signature.
    function signerOf(bytes32 hash, bytes32 r, bytes32 s, uint8 v)
        external pure returns (address)
    {
        // Reject the malleable "high-s" twin signature (r, n - s)
        require(uint256(s) <= HALF_N, "high-s");
        require(v == 27 || v == 28, "bad v");

        address signer = ecrecover(hash, v, r, s);  // EVM precompile at 0x01
        require(signer != address(0), "invalid signature");
        return signer;   // compare this to your expected signer
    }
}
ecrecover is a built-in EVM precompile: feed it the hash and (r, s, v) and it hands back the signer's address — no stored public key required.

On the network side, a node validating your transaction runs the same recovery, checks that the recovered address matches the from the transaction claims, and then checks the nonce and balance. No valid signature, no spend — full stop. This is the precise mechanism by which a blockchain knows you are you with no bank, no password file, and no central authority anywhere in the loop.

The nonce that destroyed a console — and stole Bitcoin

Look back at the signing code and one rule towers above the rest: the nonce k must be random, secret, and never reused. Break it and you don't just leak one signature — you leak the private key itself. In 2010, the group fail0verflow showed that Sony had signed every PlayStation 3 firmware with a constant k. Two signatures, one fixed k, and the console's master signing key fell out — Sony could never revoke it. In August 2013, a flaw in Android's `SecureRandom` made several Bitcoin wallets generate colliding k values; thieves scanned the chain for two transactions sharing an r, recovered the keys, and drained the coins.

# Two signatures by the SAME key that reused the SAME nonce k
# necessarily share the same r (because r = (k*G).x).  Then:
#
#   s1 = k^{-1} * (z1 + r*d)
#   s2 = k^{-1} * (z2 + r*d)
# Subtract:
#   s1 - s2 = k^{-1} * (z1 - z2)
#
# Solve for the secret nonce, then for the PRIVATE KEY:

k = (z1 - z2) * inverse(s1 - s2, n) mod n        # recover k
d = (s1 * k - z1) * inverse(r, n)   mod n        # recover d  <-- game over

# Anyone who watched the public chain can now sign as you, forever.
Nonce reuse is fatal: two signatures sharing k leak the private key with grade-school algebra. This is not theoretical — it has emptied real wallets.

Malleability, and life beyond ECDSA

ECDSA has one more quirk worth knowing honestly. For any valid signature *(r, s)*, the pair *(r, n − s)* is also a valid signature of the same message — you can flip s without the private key. This is signature malleability: a third party can alter the signature bytes, which changes the transaction's hash even though the transaction itself is unchanged. Code that keyed off the raw transaction hash got confused by this; it muddied the forensics during the 2014 Mt. Gox collapse. The fix is the low-s rule (Bitcoin's BIP-62, Ethereum's EIP-2): only accept the half with *s ≤ n/2* — exactly the `HALF_N` check in the contract above. Note this never let anyone spend your coins; it only let them rewrite an id, which is also why naive replay protection that trusts a tx hash can be a signature-replay hazard.

ECDSA was chosen for early Bitcoin partly because the cleaner, older Schnorr signature was under patent until 2008. Schnorr's signing equation is simply s = k + e·d (with e a hash of the public point and message) — and because it is linear in the key and nonce, several signers' keys and signatures can be added together into a single aggregate. That powers Bitcoin's Taproot (BIP-340, live since 2021): a multisig spend can look on-chain like one ordinary signature, saving space and improving privacy. Schnorr also has a cleaner security proof and no malleability twin.

At the far end of aggregation sits the BLS signature, built on elliptic-curve pairings. BLS lets you compress thousands of independent signatures into one short signature that verifies in a single check — which is exactly why Ethereum's proof-of-stake consensus uses it to fold tens of thousands of validator attestations into each slot. The trade-off is honest: BLS verification is heavier per signature and relies on pairing-friendly curves, so each scheme — ECDSA, Schnorr, BLS — earns its place by what it optimizes.