Skip to content

fix(client): grind ECDSA descriptor BIP-322 proofs to 71/72 bytes#9

Merged
johnzilla merged 1 commit into
mainfrom
fix/bip322-ecdsa-grind-descriptor-proofs
Jun 11, 2026
Merged

fix(client): grind ECDSA descriptor BIP-322 proofs to 71/72 bytes#9
johnzilla merged 1 commit into
mainfrom
fix/bip322-ecdsa-grind-descriptor-proofs

Conversation

@johnzilla

Copy link
Copy Markdown
Owner

Problem

mixed_script_e2e intermittently failed at input registration (~1-in-3 CI runs), and it turned out to be a real product bug, not a test artifact: any P2SH-P2WPKH or P2WPKH descriptor-wallet client had a ~5%+ chance per round of having its BIP-322 ownership proof silently rejected (register_input → 400 INVALID_PROOF / "BIP-322 crate verification failed").

Root cause

The pinned bip322 = "=0.0.10" verifier hardcodes a match signature_length { 71 | 72 } check and rejects valid 70/73-byte ECDSA DER signatures. The WIF client path already grinds signatures to 71/72 via shared::bip322::sign_simple (sign_ecdsa_compat_bip322_length). The descriptor path (generate / from_descriptor) signed proofs via bdk, which does no grinding — and bdk's RFC-6979-deterministic signer produces a 70/73-byte signature for ~5%+ of (key, message) combinations. Because the test generates a fresh key + random round_id each run, ~5%+ of runs hit a bad combination. P2TR is immune (fixed-length Schnorr).

This is fundamentally an upstream bug in the bip322 crate's verifier; this PR is the client-side workaround.

Fix

Route ECDSA descriptor proofs through the grinding-aware sign_simple:

  • Derive the External index-0 leaf secret key from the descriptor xprv at construction (derive_external_leaf_sk), stored as external_leaf_sk only when it controls the registered UTXO (ecdsa_leaf_controls_script).
  • sign_bip322 uses it for P2WPKH / P2SH-P2WPKH; P2TR and non-index-0 from_descriptor UTXOs fall back to bdk.
  • For in-range signatures sign_simple is byte-identical to bdk (guarded by the existing *_matches_bdk_sign_byte_for_byte tests), so only the previously-rejected ~5% changes.

Verification

  • New descriptor_ecdsa_proofs_grind_to_verifiable_length (64 fixed seeds; asserts 71/72 bytes + crate-verifies). Confirmed to fail when the fix is disabled.
  • mixed_script_e2e reproduced locally, then run 12× green with the fix (former rate ~1/3).
  • Client lib suite 39 passing; workspace clippy -D warnings clean.

Docs

Root cause + detection notes: docs/solutions/bip322-ecdsa-signature-length-flake.md.

🤖 Generated with Claude Code

Root-causes the intermittent mixed_script_e2e flake (also a real product bug).

The pinned bip322 = "=0.0.10" verifier hardcodes a 71/72-byte ECDSA witness
signature length check and rejects valid 70/73-byte signatures. The WIF client
path already grinds signatures to 71/72 via shared::bip322::sign_simple, but the
descriptor path (generate / from_descriptor) signed BIP-322 proofs via bdk,
which does no grinding. bdk's ECDSA is RFC-6979 deterministic, so ~5% of
(descriptor key, round_id) combinations produced a 70/73-byte signature the
verifier rejects — surfacing as register_input → 400 INVALID_PROOF for any
P2SH-P2WPKH or P2WPKH descriptor-wallet client ~5%+ of the time. P2TR is immune
(fixed-length Schnorr).

Fix: derive the External index-0 leaf secret key from the descriptor xprv at
construction (derive_external_leaf_sk) and store it (external_leaf_sk) only when
it controls the registered UTXO (ecdsa_leaf_controls_script); sign_bip322 then
routes P2WPKH/P2SH-P2WPKH proofs through sign_simple (grinded). Non-index-0
from_descriptor UTXOs and P2TR fall back to the bdk path. For in-range signatures
sign_simple is byte-identical to bdk (existing *_matches_bdk_sign tests), so only
the previously-rejected ~5% changes.

Verification: new descriptor_ecdsa_proofs_grind_to_verifiable_length test
(64 seeds, asserts 71/72 + crate-verifies; confirmed to fail when the fix is
disabled); mixed_script_e2e reproduced locally then run 12x green with the fix
(former rate ~1/3). Client lib suite 39 passing, workspace clippy -D warnings
clean. Learning written to docs/solutions/.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
@johnzilla johnzilla merged commit 982fc28 into main Jun 11, 2026
6 checks passed
@johnzilla johnzilla deleted the fix/bip322-ecdsa-grind-descriptor-proofs branch June 11, 2026 03:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant