Skip to content

Add key proof psbt to descriptor ack#276

Merged
kwsantiago merged 5 commits intomainfrom
Add-key-proof-PSBT-to-DescriptorAck
Feb 25, 2026
Merged

Add key proof psbt to descriptor ack#276
kwsantiago merged 5 commits intomainfrom
Add-key-proof-PSBT-to-DescriptorAck

Conversation

@kwsantiago
Copy link
Contributor

@kwsantiago kwsantiago commented Feb 25, 2026

Closes #270

Summary by CodeRabbit

  • New Features

    • Added a key-proof workflow (Schnorr/Taproot) for deterministic key proof creation, signing, and verification.
    • Descriptor acknowledgments now include and validate a key-proof PSBT and enforce size/format constraints.
  • APIs

    • New public APIs to build, sign, and verify key-proof PSBTs; ACK payloads carry the proof.
  • Tests

    • Expanded tests cover determinism, signing/verification roundtrips, tamper detection, and multi-node flows.

@kwsantiago kwsantiago self-assigned this Feb 25, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Walkthrough

Introduces a new key-proof PSBT workflow (build, sign, verify) in keep-bitcoin and integrates signed PSBTs into keep-frost-net descriptor ACKs so participants prove control of contributed keys during the ACK phase.

Changes

Cohort / File(s) Summary
Key Proof Module
keep-bitcoin/src/key_proof.rs, keep-bitcoin/src/lib.rs
Adds a new key_proof module and re-exports: build_key_proof_psbt, sign_key_proof, verify_key_proof. Implements deterministic PSBT construction, child-key derivation, Schnorr/Taproot signing, verification, helper functions, and unit tests.
Descriptor ACK & Protocol
keep-frost-net/src/protocol.rs, keep-frost-net/src/descriptor_session.rs
Adds key_proof_psbt: Vec<u8> to DescriptorAckPayload, MAX size constant, validation on ACK messages, and updates DescriptorSession::add_ack signature to accept and verify the provided PSBT (pub(crate) fn parse_network visibility change). Tests updated to build/verify real xpubs and proofs.
Node Descriptor Flow
keep-frost-net/src/node/descriptor.rs
Threads derived account xpub and signing-share through finalize/ACK flows, adds signing_share_bytes helper, constructs and attaches key_proof_psbt to DescriptorAckPayload, and calls session ACK with proof for verification.
Tests & Utilities
keep-frost-net/tests/multinode_test.rs
Replaces manual xpub/key derivation with derive_account_xpub helper; updates test calls to pass fingerprint references; uses real proof generation helpers in tests.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Node as KfpNode
participant KeepBitcoin as keep-bitcoin
participant Session as DescriptorSession
participant Peer as Contributor

Node->>KeepBitcoin: build_key_proof_psbt(session_id, share_index, account_xpub, network)
KeepBitcoin-->>Node: unsigned PSBT bytes
Node->>Peer: send DescriptorAck (descriptor_hash, key_proof_psbt)
Peer->>KeepBitcoin: sign_key_proof(psbt, signing_secret, network)
KeepBitcoin-->>Peer: signed PSBT bytes
Peer->>Session: add_ack(share_index, descriptor_hash, key_proof_psbt)
Session->>KeepBitcoin: verify_key_proof(session_id, share_index, account_xpub, signed_psbt, network)
KeepBitcoin-->>Session: verification result
Session-->>Node: ACK accepted / error

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hop with a PSBT bright,

Schnorr ticks softly in moonlight,
Keys proved true with Taproot cheer,
Each ACK whispers: "I hold it here." ✨🔑

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive While most changes align with issue #270, the PR does not include CLI wallet or keep-mobile binding updates mentioned as required scope, making it unclear if the feature is fully implemented end-to-end. Clarify whether CLI wallet propose flow and keep-mobile bindings updates are in separate PRs or if they should be included in this PR to complete the feature delivery.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add key proof psbt to descriptor ack' directly and clearly describes the primary change: adding a key_proof_psbt field to the DescriptorAck payload.
Linked Issues check ✅ Passed The PR fully implements the requirements from issue #270: adds key_proof_psbt to DescriptorAckPayload [#270], implements deterministic PSBT generation [#270], verifies proofs in handle_descriptor_ack [#270], and validates in descriptor_session [#270].

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch Add-key-proof-PSBT-to-DescriptorAck

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
keep-bitcoin/src/key_proof.rs (1)

116-123: Intermediate Xpriv values (master, child) are not zeroized on drop.

The child_bytes are properly wrapped in Zeroizing, but the Xpriv structs containing private key material are dropped without explicit zeroization since the bitcoin crate's Xpriv doesn't implement Zeroize. This is a known limitation and was likely considered during the "harden key proof zeroization" commit.

If the bitcoin crate adds Zeroize support for Xpriv in the future, it would be worth revisiting. For now, the sensitive window is minimal (scoped block).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@keep-bitcoin/src/key_proof.rs` around lines 116 - 123, The intermediate Xpriv
values (master, child) hold private material but aren't Zeroize-able; to
minimize the sensitive window, create master and derive child inside a
tightly-scoped inner block and immediately extract
child.private_key.secret_bytes() into the Zeroizing-wrapped child_bytes, then
let the inner block end so master and child are dropped right away (avoid
binding master/child outside the block and avoid holding them beyond
extraction). Reference the Xpriv construction/derive_priv calls and the
child_bytes Zeroizing wrapper when implementing this scope reduction so the
Xpriv instances don't outlive the extraction.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@keep-frost-net/src/node/descriptor.rs`:
- Around line 562-576: The initiator is waiting for ACKs from all online peers
(expected_acks) but non-contributor peers lack entries in contributions causing
our_xpub to be None and key proof generation to fail; fix this by restricting
expected_acks to only contributors (i.e., intersect the set used to build
expected_acks with the keys of contributions/expected_contributors) so peers
that are not in contributions aren’t expected to return DescriptorFinalize ACKs,
and/or make key proof generation in the DescriptorFinalize handler tolerate
missing our_xpub by treating non-contributors as optional (skip building
key_proof_psbt_bytes when our_xpub is None and don’t require their ACK). Ensure
you update places that reference expected_acks and the DescriptorFinalize flow
(symbols: expected_acks, contributions/expected_contributors, DescriptorFinalize
handler, our_xpub, key_proof_psbt_bytes, signing_share_bytes) so the initiator
only awaits ACKs from actual contributors or non-contributors are handled as
optional.

In `@keep-frost-net/src/protocol.rs`:
- Line 33: Add an assertion in the test_sign_and_verify_roundtrip test to ensure
the serialized key_proof PSBT length does not exceed MAX_KEY_PROOF_PSBT_SIZE:
after producing the signed PSBT (the variable holding the serialized key_proof
PSBT in keep-bitcoin/src/key_proof.rs), call assert!(psbt_bytes.len() <=
MAX_KEY_PROOF_PSBT_SIZE) (or an equivalent check) so the test fails if the
produced PSBT grows past the protocol constant MAX_KEY_PROOF_PSBT_SIZE;
reference the constant MAX_KEY_PROOF_PSBT_SIZE and the test function name
test_sign_and_verify_roundtrip when locating where to add this assertion.

---

Nitpick comments:
In `@keep-bitcoin/src/key_proof.rs`:
- Around line 116-123: The intermediate Xpriv values (master, child) hold
private material but aren't Zeroize-able; to minimize the sensitive window,
create master and derive child inside a tightly-scoped inner block and
immediately extract child.private_key.secret_bytes() into the Zeroizing-wrapped
child_bytes, then let the inner block end so master and child are dropped right
away (avoid binding master/child outside the block and avoid holding them beyond
extraction). Reference the Xpriv construction/derive_priv calls and the
child_bytes Zeroizing wrapper when implementing this scope reduction so the
Xpriv instances don't outlive the extraction.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65cb635 and cda6993.

📒 Files selected for processing (6)
  • keep-bitcoin/src/key_proof.rs
  • keep-bitcoin/src/lib.rs
  • keep-frost-net/src/descriptor_session.rs
  • keep-frost-net/src/node/descriptor.rs
  • keep-frost-net/src/protocol.rs
  • keep-frost-net/tests/multinode_test.rs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
keep-frost-net/src/node/descriptor.rs (1)

562-576: ⚠️ Potential issue | 🟠 Major

Non-contributor receivers of DescriptorFinalize will return a spurious error.

finalize_descriptor broadcasts to all online peers (lines 381–388), not just contributors. A non-contributor has no entry in session.contributions(), so our_xpub is None, and line 563 turns that into an Err. The session cleanup and error propagation that follows is incorrect — this is an expected, benign situation for a non-contributor.

Add an early exit for non-contributors before attempting key proof construction:

🐛 Proposed fix
+        // Non-contributors have no key to prove; they have already acknowledged
+        // the descriptor by reaching this point without a NACK.
+        let Some(our_xpub) = our_xpub else {
+            // Emit completion event if the descriptor verified cleanly.
+            let _ = self.event_tx.send(KfpNodeEvent::DescriptorComplete {
+                session_id: payload.session_id,
+                external_descriptor: payload.external_descriptor,
+                internal_descriptor: payload.internal_descriptor,
+                network: session_network,
+            });
+            return Ok(());
+        };
+
         let key_proof_psbt_bytes = {
-            let our_xpub = our_xpub.ok_or_else(|| {
-                FrostNetError::Session("Missing own xpub contribution for key proof".into())
-            })?;
-
             let net = crate::descriptor_session::parse_network(&session_network)?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@keep-frost-net/src/node/descriptor.rs` around lines 562 - 576, The code in
finalize_descriptor attempts to build/sign a key proof for every receiver but
does not handle non-contributors: if our_xpub is None (no entry in
session.contributions()) the function currently returns an error when it should
treat this as a benign case. Add an early exit right before constructing
key_proof_psbt_bytes: check session.contributions() / our_xpub and if our_xpub
is None (i.e., we're not a contributor) skip key proof construction and
return/continue successfully (or otherwise avoid error propagation for
non-contributors) so that the block using our_xpub, our_index,
keep_bitcoin::build_key_proof_psbt and keep_bitcoin::sign_key_proof only runs
for actual contributors.
🧹 Nitpick comments (2)
keep-bitcoin/src/key_proof.rs (2)

178-315: Good test coverage overall; consider adding a cross-network mismatch case.

The suite covers determinism, session/share variations, roundtrip, tampered signature, and mainnet. One gap: no test for a PSBT signed on Network::Testnet submitted for verification on Network::Bitcoin (different coin_type → different child key → should fail). This is a cheap case to add given the other tamper tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@keep-bitcoin/src/key_proof.rs` around lines 178 - 315, Add a test that
ensures cross-network verification fails: use test_xpub to create an xpub for
Network::Testnet, build a PSBT with build_key_proof_psbt(..., Network::Testnet),
sign it with sign_key_proof(..., Network::Testnet), then call
verify_key_proof(..., Network::Bitcoin) and assert it returns an Err; reference
test helper test_xpub and the functions build_key_proof_psbt, sign_key_proof,
and verify_key_proof to locate where to wire the new test.

244-249: Extract the PSBT size cap as a named constant.

The test hard-codes 512 which must stay in sync with MAX_KEY_PROOF_PSBT_SIZE in keep-frost-net/src/protocol.rs. A silent drift would invalidate the guard. Define a public constant in keep-bitcoin (e.g. pub const MAX_KEY_PROOF_PSBT_SIZE: usize = 512;) and import it in both places.

♻️ Proposed change
+/// Maximum byte length of a serialised key-proof PSBT.
+/// Must match `MAX_KEY_PROOF_PSBT_SIZE` in keep-frost-net.
+pub const MAX_KEY_PROOF_PSBT_SIZE: usize = 512;

Then in the test:

-        assert!(
-            signed_bytes.len() <= 512,
-            "key proof PSBT {} bytes exceeds protocol max 512",
-            signed_bytes.len(),
-        );
+        assert!(
+            signed_bytes.len() <= MAX_KEY_PROOF_PSBT_SIZE,
+            "key proof PSBT {} bytes exceeds protocol max {MAX_KEY_PROOF_PSBT_SIZE}",
+            signed_bytes.len(),
+        );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@keep-bitcoin/src/key_proof.rs` around lines 244 - 249, Extract the hard-coded
512 into a shared named constant by adding a public constant pub const
MAX_KEY_PROOF_PSBT_SIZE: usize = 512 in the keep-bitcoin crate (e.g. near
key_proof.rs module), then replace the literal 512 in the assert in
key_proof::tests (the assert! checking signed_bytes.len() <= 512) with that
constant; update keep-frost-net/src/protocol.rs to import/use the same exported
MAX_KEY_PROOF_PSBT_SIZE from keep-bitcoin so both crates reference the single
source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@keep-frost-net/src/node/descriptor.rs`:
- Around line 562-576: The code in finalize_descriptor attempts to build/sign a
key proof for every receiver but does not handle non-contributors: if our_xpub
is None (no entry in session.contributions()) the function currently returns an
error when it should treat this as a benign case. Add an early exit right before
constructing key_proof_psbt_bytes: check session.contributions() / our_xpub and
if our_xpub is None (i.e., we're not a contributor) skip key proof construction
and return/continue successfully (or otherwise avoid error propagation for
non-contributors) so that the block using our_xpub, our_index,
keep_bitcoin::build_key_proof_psbt and keep_bitcoin::sign_key_proof only runs
for actual contributors.

---

Nitpick comments:
In `@keep-bitcoin/src/key_proof.rs`:
- Around line 178-315: Add a test that ensures cross-network verification fails:
use test_xpub to create an xpub for Network::Testnet, build a PSBT with
build_key_proof_psbt(..., Network::Testnet), sign it with sign_key_proof(...,
Network::Testnet), then call verify_key_proof(..., Network::Bitcoin) and assert
it returns an Err; reference test helper test_xpub and the functions
build_key_proof_psbt, sign_key_proof, and verify_key_proof to locate where to
wire the new test.
- Around line 244-249: Extract the hard-coded 512 into a shared named constant
by adding a public constant pub const MAX_KEY_PROOF_PSBT_SIZE: usize = 512 in
the keep-bitcoin crate (e.g. near key_proof.rs module), then replace the literal
512 in the assert in key_proof::tests (the assert! checking signed_bytes.len()
<= 512) with that constant; update keep-frost-net/src/protocol.rs to import/use
the same exported MAX_KEY_PROOF_PSBT_SIZE from keep-bitcoin so both crates
reference the single source of truth.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cda6993 and 0087249.

📒 Files selected for processing (2)
  • keep-bitcoin/src/key_proof.rs
  • keep-frost-net/src/node/descriptor.rs

@kwsantiago kwsantiago merged commit ea12fca into main Feb 25, 2026
9 checks passed
@kwsantiago kwsantiago deleted the Add-key-proof-PSBT-to-DescriptorAck branch February 25, 2026 12:51
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.

Add key proof PSBT to DescriptorAck

1 participant