nonce-hash-sufficient-statistic

Tue Jul 28 2026

Why this note exists Triggered by a review thread on bitcoin/bips#2070. NonceGen takes an x-only threshold pubkey while the Signers Context holds a plain one, and Tim suggested either clearer wording or hashing all tweaks and modes instead. The case analysis below is what I posted as the reply. The reusable part is the method: how to decide how much a defense-in-depth hash input needs to bind.

The setup

NonceGen hashes optional session data into the nonce so a broken RNG doesn't repeat a nonce across two different signing contexts (nonce-fn-defense-in-depth-inputs covers the general mechanism). One of those inputs is the threshold pubkey, and tweaking makes it unclear which form to pass. Three candidates:

  1. the plain tweaked key (PlainPk, 33 bytes),
  2. the full tweak list: every tweak value plus its mode, plain or x-only,
  3. the x-only tweaked key (XonlyPk, 32 bytes, what the spec had).

Notation, from the BIP's Tweak Context. After vv tweaks the context holds the tweaked point QvQ_v, the accumulated negation factor gaccv{1,1}gacc_v \in \{1, -1\}, and the accumulated tweak scalar taccvtacc_v. They satisfy the invariant

Qv=gaccvQ0+taccvG,Q_v = gacc_v \cdot Q_0 + tacc_v \cdot G,

where Q0Q_0 is the base threshold key with secret x0x_0. One more sign matters: the factor gg, which is 11 when QvQ_v has even y and 1-1 otherwise, since BIP340 verifies against the x-only key.

Where I got stuck

Hashing the full tweak list felt strictly strongest. More parameters in the hash means more separation between contexts, so more defense. The plain key looked lossy in comparison: two plain tweaks t1,t2t_1, t_2 and the single tweak t1+t2t_1 + t_2 land on the same QvQ_v, so hashing only QvQ_v can't tell those sequences apart. My earlier rule in nonce-fn-defense-in-depth-inputs pushed the same way: the hash input should be an injective encoding of the session. By that rule the tweak list wins and the single key is a compromise.

The key move: separate only what signing distinguishes

Ask what the signing computation actually reads from the tweak data, and demand separation only where those reads differ. The algorithms touch the tweak list in exactly three places: bb and ee hash xbytes(Qv)xbytes(Q_v), Sign multiplies the share by ggaccg \cdot gacc, and PartialSigAgg adds egtacce \cdot g \cdot tacc. Two contexts that agree on these values run byte-identical computations. If a broken RNG hands them the same nonce, they output the same signature twice, and a repeated signature reveals nothing. So a hash collision is only dangerous when the consumed values differ.

That flips the requirement. The input doesn't need to injectively encode the session. It needs to separate contexts whose signing equations differ, a sufficient statistic for the signing equation, in stats terms. Collisions inside an equivalence class of identical equations are free.

Checking the three candidates

Take two different tweak sequences applied to the same threshold key, and suppose they collide on the NonceGen hash with identical randrand' (the defense-in-depth failure case). Since gaccgacc is ±1\pm 1, the sequences either share it or have opposite signs.

With the plain key, a collision means the same QvQ_v, and the invariant settles both cases. Same gaccgacc forces the same tacctacc: the equations are identical, harmless. Opposite gaccgacc forces tacc=tacc+2gaccx0tacc' = tacc + 2 \cdot gacc \cdot x_0: building that pair requires knowing x0x_0, and honest BIP32/BIP341 hash-derived tweaks hit it with probability about 22562^{-256}.

With the full tweak list (count prefixed, so the encoding is unambiguous), distinct sequences always separate. But the separation gained over the plain key is exactly the harmless class, same (Qv,gacc,tacc)(Q_v, gacc, tacc). No extra defense.

With the x-only key, a collision means Qv=±QvQ_v' = \pm Q_v, four cases:

CasegaccgaccQvQ_vInvariant forcesVerdict
1samesametacc=tacctacc' = taccidentical equation, harmless
2sameoppositetacc=tacc2gaccx0tacc' = -tacc - 2 \cdot gacc \cdot x_0needs x0x_0
3oppositesametacc=tacc+2gaccx0tacc' = tacc + 2 \cdot gacc \cdot x_0needs x0x_0
4oppositeoppositetacc=tacctacc' = -taccharmless, the flips cancel

Case 4 is the surprise. My first pass called it a break, saved only by fresh randomness in practice. That defense contradicts the exercise's own premise: we already assumed randrand' failed. It's also the one case that is cheap to construct, since a plain tweak tt and an x-only tweak t-t on an odd-y key give QvQ_v and Qv-Q_v. The real verdict is that it can't hurt at all. Negating QvQ_v flips its parity, so gg flips too, and everything signing consumes is a product of two flipped values:

ggacc=(g)(gacc)=ggacc,gtacc=(g)(tacc)=gtacc,g' \cdot gacc' = (-g)(-gacc) = g \cdot gacc, \qquad g' \cdot tacc' = (-g)(-tacc) = g \cdot tacc,

while xbytes(Qv)=xbytes(Qv)xbytes(Q_v') = xbytes(Q_v). Both contexts produce byte-identical signatures for the same x-only key. Case 4 sits in the harmless bucket with Case 1. If only one of gg or tacctacc flipped the products would change, but the case definition forces both flips at once: g=gg' = -g comes from the flipped parity, tacc=tacctacc' = -tacc from the invariant.

case4-mirror-cancellation.excalidraw

So all three candidates are equally strong. Every possible collision either leaves the signing equation untouched or requires the threshold secret.

The takeaway

The injectivity rule was stronger than needed. Injective up to harmless collisions is enough, where harmless means the colliding contexts run the same signing equation anyway. And more hash inputs don't mean more defense: extra parameters only split contexts that were already identical where it matters.

Since the three candidates tie on security, the pick is an API call. I proposed the plain tweaked key, the output of GetPlainPubkey(tweak_ctx), on the PR: it matches the thresh_pk field in the Signers Context, matches what key generation outputs (ChillDKG returns a plain key), and the untweaked case passes thresh_pk through unchanged.

The method is portable. Before adding a defense-in-depth input to a nonce hash, write down what the signature computation consumes, then hash a value that pins those down. Anything beyond that is redundant, and anything less leaves a pair of distinguishable contexts sharing a nonce.