Summary
SetMuSig2SecNonce() in src/script/signingprovider.cpp calls try_emplace on the persistent per-wallet m_musig2_secnonces map and then Assert(inserted). When walletprocesspsbt is called twice on the same nonce-less MuSig2 PSBT (either a natural retry, or the case where a counterparty strips the victim's pubnonce and returns the PSBT) the deterministic session_id = SHA256(script_pubkey || part_pubkey || sighash) collides, try_emplace returns inserted=false, and the Assert aborts bitcoind.
Impact
- Attack surface: authenticated RPC plus a wallet containing a MuSig2 participant private key.
- Trigger: either (a) user or tooling retries
walletprocesspsbtassuming idempotency, or (b) a malicious MuSig2 cosigner returns the PSBT with the victim's pubnonce stripped, prompting the victim's client to re-process. - No nonce reuse occurs. The
Assertis intended to prevent nonce reuse, but regenerating a fresh secnonce (overwriting an unused old one) is cryptographically safe; it invalidates the previously-shared pubnonce, requiring a signing round restart, which is exactly the retry scenario. - Design conflict: the guidance at
src/util/check.h:125-126recommendsCHECK_NONFATALfor conditions reachable from RPC.Assert()usesstd::abort()in all builds (NDEBUGis#error'd incheck.h). - Affected versions:
v31.0rc1,v31.0rc2, andmaster. Not present in any GA release. Introduced in commitc9519c260b(PR #33636). Release-blocking forv31.0GA. - Severity: DoS — node abort.
Reproduction
On master (2d5ab09f0d at the time of testing):
# In regtest with wallet support:
# 1. create two descriptor wallets (musig_p0, musig_p1)
# 2. import tr(musig(priv0/<0;1>/*,pub1/<2;3>/*)) into wallet 0
# 3. fund the musig address and confirm
# 4. walletcreatefundedpsbt -> nonce-less MuSig2 PSBT
# 5. walletprocesspsbt $PSBT # first call: complete=false, stores secnonce
# 6. walletprocesspsbt $PSBT # second call on original PSBT: Assert(inserted), process aborts
Fix
Replace the try_emplace + Assert(inserted) in FlatSigningProvider::SetMuSig2SecNonce with insert_or_assign. Overwriting an unused secnonce is cryptographically safe: the previously returned pubnonce is invalidated, and the session must restart from the new nonce, which is the intent of the retry. Nothing else reads the old secnonce between the two calls.
Credit
Found by Claude during an automated review conducted by Anthropic; manually validated and patched by Trail of Bits. Reference: ANT-2026-05771.