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?