Nonces everywhere#
Nonce means “number used once”, and blockchainkit has at least four of them. They share a name and almost nothing else: one must be secret, one must be public and counted, one is a lottery ticket, and one is a salt. Mixing them up is one of the most common confusions about blockchains, and one of the most expensive mistakes in their software.
Nonce |
Secret? |
Chosen how? |
What goes wrong if it is reused |
|---|---|---|---|
Signing nonce \(k\) |
yes |
random, or derived from key and message |
Two signatures reveal the private key. |
Account nonce |
no |
counts up by one |
A copied transaction is spent again (a replay). |
Proof-of-work nonce |
no |
searched |
Nothing: it only has to make the hash small. |
Salt |
until opened |
random |
Equal messages get equal commitments, which can be guessed. |
All snippets use import blockchainkit as bk.
The signing nonce: secret and fresh#
A Schnorr signature is \((R, s)\) with \(R = kG\) and \(s = k + c x\), where \(x\) is the private key and \(c\) a hash of the message. The nonce \(k\) hides \(x\) in \(s\), as a one-time pad would. Use it twice and the pad cancels out:
>>> curve = bk.crypto.TOY_CURVE
>>> public = bk.crypto.public_key(7, curve)
>>> first = bk.crypto.sign(b"pay Bob 5", 7, nonce=3, curve=curve)
>>> second = bk.crypto.sign(b"pay Eve 9", 7, nonce=3, curve=curve)
>>> c1 = bk.crypto.challenge(b"pay Bob 5", first.commitment, public, curve)
>>> c2 = bk.crypto.challenge(b"pay Eve 9", second.commitment, public, curve)
>>> c1 != c2 and bk.crypto.recover_reused_nonce_key(first, second, c1, c2, curve.order)
7
This is how Sony’s PlayStation 3 signing key leaked in 2010. RFC 6979 removes the need for a random number generator by deriving \(k\) from the key and the message, so the same message gets the same nonce (harmless: the same signature) and different messages get different ones:
>>> k1 = bk.crypto.deterministic_nonce(7, b"pay Bob 5")
>>> k1 == bk.crypto.deterministic_nonce(7, b"pay Bob 5")
True
>>> k1 != bk.crypto.deterministic_nonce(7, b"pay Eve 9")
True
See Nonce reuse in practice, and deterministic nonces (2010-2013).
The account nonce: public and sequential#
In an account-based ledger, a signed transfer is just bytes. Anyone who sees it could submit it again. The account nonce is a public counter inside the signed message: the ledger accepts a transaction only if its nonce equals the sender’s current count, then increments the count.
>>> alice_public = bk.crypto.public_key(7)
>>> alice = bk.structures.address(alice_public)
>>> bob = bk.structures.address(bk.crypto.public_key(11))
>>> ledger = bk.structures.Ledger({alice: 100})
>>> payment = bk.structures.Transaction(alice_public, bob, 25, 0).signed(7, signing_nonce=17)
>>> after = ledger.apply([payment])
>>> after.nonces[alice], after.balances[bob]
(1, 25)
>>> try:
... after.apply([payment])
... except ValueError as error:
... print("replay rejected")
replay rejected
Note the two nonces in that one line: 0 is the account nonce, public and
predictable; signing_nonce=17 is the signing nonce, which in real use must
be secret and never repeated. Bitcoin needs no account nonce, because each coin
can be spent only once: see Coins, not accounts: the unspent-output model (Nakamoto 2008).
The proof-of-work nonce: a lottery ticket#
A block’s header has a free field that miners change until the header’s hash falls below a target. The nonce carries no meaning; it only makes each attempt different.
>>> result = bk.consensus.mine(bk.structures.Block(difficulty=8))
>>> result.block.nonce == result.attempts - 1 # Tried 0, 1, 2, ... in turn.
True
>>> bk.consensus.valid_pow(result.block)
True
>>> bk.consensus.expected_trials(8)
256
Reusing a proof-of-work nonce is harmless: another block with other contents has a different hash. See Hashcash: proof of work you can verify in one hash (Back 1997).
The salt: randomness that hides#
A commitment \(H(\text{salt} \| m)\) hides \(m\) only if the salt is unpredictable. Without one, anyone can try the likely messages:
>>> import secrets
>>> salt = secrets.token_bytes(16)
>>> sealed = bk.crypto.commit(b"heads", salt)
>>> bk.crypto.verify_commitment(sealed, b"heads", salt)
True
>>> unsalted = bk.crypto.sha256(b"heads")
>>> [guess for guess in (b"heads", b"tails") if bk.crypto.sha256(guess) == unsalted]
[b'heads']
Compact blocks use a per-block salt for the same reason: it stops an attacker from precomputing colliding short IDs (see Compact blocks: send what the peer lacks (Corallo, BIP 152, 2016)).
Check yourself#
Which nonce appears in the signed bytes of a transaction, and which one never leaves the signer’s machine?
Why does a hardware wallet need a good random number generator, or RFC 6979, but a miner does not?
A transaction is broadcast, dropped, and broadcast again. Which nonce stops it being applied twice?