Skip to content

feat: add p2pk_signing_keys to SendOptions#1750

Closed
vnprc wants to merge 2 commits into
cashubtc:mainfrom
vnprc:send-p2pk-signing-keys-plan
Closed

feat: add p2pk_signing_keys to SendOptions#1750
vnprc wants to merge 2 commits into
cashubtc:mainfrom
vnprc:send-p2pk-signing-keys-plan

Conversation

@vnprc
Copy link
Copy Markdown
Contributor

@vnprc vnprc commented Mar 20, 2026

Summary

Wallets that hold P2PK-locked proofs (received from another party, or minted with spending conditions) currently have no way to spend them through the standard prepare_send / confirm flow — they must manually call swap() and handle signing themselves. This PR closes that gap.

Changes:

  • Add p2pk_signing_keys: Vec<SecretKey> to SendOptions (defaults to empty, fully backward-compatible)
  • Extract P2PK/HTLC signing logic from receive/saga/mod.rs into a new sign_proofs() utility in wallet/util.rs, and call it from both the receive and send sagas
  • Call sign_proofs(&mut proofs_to_swap, &opts.p2pk_signing_keys) in PreparedSend::confirm() before the swap, so the mint accepts the spend
  • Mirror the field in cdk-ffi's SendOptions struct and both From conversions (matching the existing ReceiveOptions pattern)

Usage

// Wallet holds P2PK-locked proofs locked to `secret`
let prepared = wallet
    .prepare_send(
        Amount::from(10),
        SendOptions {
            p2pk_signing_keys: vec![secret],
            ..Default::default()
        },
    )
    .await?;

let token = prepared.confirm(None).await?;

Test plan

  • 4 unit tests in wallet/util.rs: correct key signs, wrong key is a no-op, plain proof is a no-op, empty key vec is a no-op
  • Integration test test_p2pk_send_options_signing_keys: funds a wallet, swaps clean proofs for P2PK-locked proofs via the raw mint API, then exercises the full prepare_send / confirm flow with signing keys — verifies the recipient receives the correct amount

Add `p2pk_signing_keys: Vec<SecretKey>` to `SendOptions` so wallets
holding P2PK-locked proofs can spend them through the standard
`prepare_send` / `confirm` flow without manually calling `swap()`.

- Extract `sign_proofs()` into `wallet/util.rs` with four unit tests
- Refactor `receive/saga/mod.rs` to call `sign_proofs()` (no behaviour change)
- Sign `proofs_to_swap` in `send/saga/mod.rs::confirm()` before swap
- Add `p2pk_signing_keys` to cdk-ffi `SendOptions` struct and both `From`
  conversions (was previously hardcoded to `Vec::new()`)
- Fix CHANGELOG `## Added` heading level to `### Added`
- Add integration test `test_p2pk_send_options_signing_keys` covering the
  full prepare_send / confirm flow with P2PK-locked input proofs
@github-project-automation github-project-automation Bot moved this to Backlog in CDK Mar 20, 2026
… conditions

When a wallet holds P2PK-locked proofs whose total exactly equals the
requested send amount, prepare_send short-circuits: all proofs go to
proofs_to_send and proofs_to_swap is empty. Since confirm only signs
proofs_to_swap, signing is skipped and the locked proofs flow out in
the token unchanged, causing the recipient to get NUT11 errors.

Fix by shadowing force_swap to true in internal_prepare when
p2pk_signing_keys is non-empty, ensuring all proofs go through a real
swap that signs and unlocks them before building the token.

Add a regression test covering the exact-denomination case.
@lescuer97
Copy link
Copy Markdown
Contributor

lescuer97 commented Mar 20, 2026

I think this is partly getting fixed here: #1466

This would not be a complete fix but I think it's more or less where you want to take it.

@vnprc
Copy link
Copy Markdown
Contributor Author

vnprc commented Mar 20, 2026

oh cool!

@thesimplekid thesimplekid self-requested a review March 25, 2026 00:31
@thesimplekid
Copy link
Copy Markdown
Collaborator

Sorry this dropped off my radar.

I think this is partly getting fixed here: #1466

This would not be a complete fix but I think it's more or less where you want to take it.

This would make it so most of the time you don't need to provide the keys manually. But we should probably still allow it as suggested in this pr.

Comment on lines +2232 to +2241
/// Regression test for the exact-denomination short-circuit bug in `p2pk_signing_keys`.
///
/// When a wallet holds P2PK-locked proofs whose total exactly equals the requested
/// send amount, `prepare_send` takes a short-circuit path: all proofs go directly
/// to `proofs_to_send` (no swap needed), so `proofs_to_swap` is empty.
/// `confirm` only signs `proofs_to_swap`, so signing is skipped entirely and the
/// P2PK-locked proofs flow out in the token unchanged.
///
/// Fix: when `p2pk_signing_keys` is non-empty, force any proofs in `proofs_to_send`
/// into `proofs_to_swap` so they are always signed and unlocked via a real swap.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we should address this. Sending proofs with a pubkey lock as well as their signature can have a negative privacy implication. So we should not just fall back to including it. This should be an explicit option to include tokens that have locks.

@vnprc
Copy link
Copy Markdown
Contributor Author

vnprc commented Apr 3, 2026

closed in favor of #1835 on latest main

@vnprc vnprc closed this Apr 3, 2026
@github-project-automation github-project-automation Bot moved this from Backlog to Done in CDK Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants