nonce-hash-sufficient-statistic
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:
- the plain tweaked key (
PlainPk, 33 bytes), - the full tweak list: every tweak value plus its mode, plain or x-only,
- the x-only tweaked key (
XonlyPk, 32 bytes, what the spec had).
Notation, from the BIP's Tweak Context. After tweaks the context holds the tweaked point , the accumulated negation factor , and the accumulated tweak scalar . They satisfy the invariant
where is the base threshold key with secret . One more sign matters: the factor , which is when has even y and 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 and the single tweak land on the same , so hashing only 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: and hash , Sign multiplies the share by , and PartialSigAgg adds . 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 (the defense-in-depth failure case). Since is , the sequences either share it or have opposite signs.
With the plain key, a collision means the same , and the invariant settles both cases. Same forces the same : the equations are identical, harmless. Opposite forces : building that pair requires knowing , and honest BIP32/BIP341 hash-derived tweaks hit it with probability about .
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 . No extra defense.
With the x-only key, a collision means , four cases:
| Case | Invariant forces | Verdict | ||
|---|---|---|---|---|
| 1 | same | same | identical equation, harmless | |
| 2 | same | opposite | needs | |
| 3 | opposite | same | needs | |
| 4 | opposite | opposite | harmless, 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 failed. It's also the one case that is cheap to construct, since a plain tweak and an x-only tweak on an odd-y key give and . The real verdict is that it can't hurt at all. Negating flips its parity, so flips too, and everything signing consumes is a product of two flipped values:
while . 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 or flipped the products would change, but the case definition forces both flips at once: comes from the flipped parity, from the invariant.
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.