Skip to content

Feat/silentpayments multi signer ecdh#135

Draft
Sosthene00 wants to merge 22 commits into
masterfrom
feat/silentpayments-multi-signer-ecdh
Draft

Feat/silentpayments multi signer ecdh#135
Sosthene00 wants to merge 22 commits into
masterfrom
feat/silentpayments-multi-signer-ecdh

Conversation

@Sosthene00

Copy link
Copy Markdown
Collaborator

Alternative to #125. Big change is to restructure the sending flow around 2 new types, GlobalSenderEcdhShare and PartialSenderEcdhShare. Dleq proof logic is still missing to be implemented if the general idea is validated

@Sosthene00 Sosthene00 force-pushed the feat/silentpayments-multi-signer-ecdh branch from a494bf4 to 63802df Compare June 8, 2026 20:05
Sosthene00 and others added 13 commits June 10, 2026 14:06
…nputsHash from public API

Replace the parallel `outpoints: NonEmptyArray<OutPoint>` +
`script_pubkeys: NonEmptyArray<(Vec<u8>, Option<PublicKey>)>` pattern
with a single `TransactionInputs` struct that owns all three fields (outpoints,
script pubkeys, extracted input pubkeys) and exposes `push` for incremental
construction.

Remove the now-redundant `eligible_input_pubkey_refs` free function and the
`InputsHash` public struct (input hash computation moves into
`TransactionSharedSecret` constructors in the next commit).

Make `LabelTag` and `LabelHash` module-private in `utils/hash.rs`; they have
no legitimate callers outside the hash module.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ansactionSharedSecret

Add `rust-dleq` as a workspace dependency behind two mutually exclusive
feature flags: `dleq-standalone` (pure Rust) and `dleq-native`
(libsecp256k1 C backend). A `compile_error!` guard enforces that exactly
one backend is selected whenever `sending` is enabled.

API changes
-----------
* `PartialSenderEcdhShare::new` now requires `secp` and `aux_rand`; it
  generates a BIP374 DLEQ proof at construction time. The old two-step
  `apply_input_hash` method is removed.
* `GlobalSenderEcdhShare::new_from_summed_keys` likewise requires `secp`
  and `aux_rand` and generates a proof; `apply_input_hash` and
  `from_partial_shares` are removed.
* Both share types expose `verify_dleq_proof` for the receiving side to
  verify correctness before trusting the share.
* `TransactionSharedSecret` now carries `recipient_scan_key` alongside the
  ECDH point; input-hash computation moves inside
  `new_from_global_share(secp, share, inputs)` and
  `new_from_public_tweak_data(secp, tweak, scan_key)`.
* `DleqProof` is re-exported from the crate root.
* Unit tests for DLEQ round-trips added to `utils/sending.rs`.

Co-authored-by: Cursor <cursoragent@cursor.com>
P2SH-P2WPKH: validate the redeem-script hash160 against the output script
pubkey before trusting it. Previously any push in script_sig was accepted
without checking the commitment, which would silently allow malformed
inputs to pass.

P2TR: the old check for a script-path spend indexed the last witness item
as `[1..33]` without verifying length, which panics on items shorter than
33 bytes. Tighten the guard to `control_block.len() >= 33` and only check
`effective_stack >= 2` (key-path spends have a single witness item).

Extract `last_push` (returns the last data push in a push-only script) and
`witness_compressed_pubkey` (verifies compressed pubkey + hash160 match)
as helpers, removing three near-identical match blocks.

Also includes `PublicTweakData::new` signature change from the two-`NonEmptyArray`
pattern to `&TransactionInputs` (companion to the TransactionInputs commit).

Co-authored-by: Cursor <cursoragent@cursor.com>
Mechanical update of the vector test suite, find_output example, and
spdk-wallet spend/client code:

* Replace the parallel `outpoints` + `script_pubkeys` vectors with
  `TransactionInputs::push` at each call site.
* Pass `secp` to `TransactionSharedSecret::new_from_public_tweak_data`
  and `new_from_global_share`.
* Thread an `aux_rand: &[u8; 32]` parameter through
  `SpClient::finalize_transaction` and the private
  `shared_secrets_by_scan_key` helper.
* Remove now-deleted imports (`InputsHash`, `NonEmptyArray` where no
  longer needed).

Co-authored-by: Cursor <cursoragent@cursor.com>
…ial_shares`

Requirements that all shares should rely to the same scan key used to be implicit, we're making it explicit.
…ubkeys

- `generate_recipient_pubkeys` now takes `&[[u8; SILENT_PAYMENT_ADDRESS_BYTE_LEN]]`
  instead of `&[SilentPaymentAddress]`, and accepts an injected `&Secp256k1` rather
  than constructing one internally.  This makes the function usable from contexts
  (e.g. PSBT signing) where the caller already has a secp context and addresses stored
  as raw bytes rather than the typed wrapper.
- Export `SILENT_PAYMENT_ADDRESS_BYTE_LEN` from the crate root.
- Add `TransactionInputs::with_capacity` constructor.
- Widen `eligible_vins` visibility to `pub` for use in the new psbt crate.
- Add `PartialSenderEcdhShare::new_unchecked` and `GlobalSenderEcdhShare::new_unchecked`
  for reconstructing shares from serialized PSBT fields without re-running DLEQ
  generation (verification still happens in `new_from_partial_shares` /
  `new_from_global_share`).
- Update vector tests to the new signature.

Co-authored-by: Cursor <cursoragent@cursor.com>
`generate_recipient_pubkeys` no longer accepts `&[SilentPaymentAddress]` directly.
Convert the SP address vec to `Vec<[u8; SILENT_PAYMENT_ADDRESS_BYTE_LEN]>` before the
call, and look up results with `s.to_byte_array()` instead of the typed address key.

Co-authored-by: Cursor <cursoragent@cursor.com>
Implements the BIP-375 signer roles as a new workspace crate built on top of
rust-psbt (psbt-v2, from a fork at branch taproot_sign) and the silentpayments crate.

Core roles implemented:
- Constructor: create PSBT, add inputs/outputs (with output-order randomisation).
- Updater: set BIP32 derivation paths and SP tweaks on inputs.
- Signer (single-signer path): compute one global ECDH share + DLEQ proof per scan
  key, then derive output scriptPubKeys via generate_recipient_pubkeys.
- Signer (multi-signer path): each co-signer contributes one per-eligible-input ECDH
  share + DLEQ proof; compute_sp_outputs aggregates and verifies them.
- Input witness finalizer: moves tap_key_sig into final_script_witness.
- Extractor: extract the final bitcoin::Transaction.

Key design points:
- `compute_sp_outputs` uses `eligible_vins` to skip non-eligible inputs in the
  multi-signer aggregation loop, fixing the case where a PSBT mixes eligible and
  non-eligible inputs.
- `collect_scan_keys` correctly slices bytes [1..34] from the 67-byte SP address
  array to obtain the 33-byte scan key.
- `extract_eligible_input_pubkey` is public, covering P2WPKH, P2PKH, P2SH-P2WPKH,
  plain P2TR, and SP-tweaked P2TR; returns None for NUMS_H key-path-less outputs.

Integration tests (psbt/tests/signer.rs):
- Full single-signer end-to-end round-trip.
- Two outputs to the same scan key produce distinct n=0/n=1 keys.
- Two outputs to different scan keys each get their own share.
- Multi-signer share generation skips non-eligible inputs.
- Single-signer ignores non-eligible inputs in compute_sp_outputs.
- Unit tests for all extract_eligible_input_pubkey branches.
- One #[ignore] test documenting the outstanding is_partial detection bug for
  multi-signer PSBTs with non-eligible inputs.

Co-authored-by: Cursor <cursoragent@cursor.com>
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