Skip to content

Add PSBT creator, signer and finalizer roles for BIP-322 - #75

Open
aagbotemi wants to merge 6 commits into
rust-bitcoin:masterfrom
aagbotemi:feat/psbt-multiparty-signing
Open

Add PSBT creator, signer and finalizer roles for BIP-322#75
aagbotemi wants to merge 6 commits into
rust-bitcoin:masterfrom
aagbotemi:feat/psbt-multiparty-signing

Conversation

@aagbotemi

Copy link
Copy Markdown
Contributor

Summary

Implement BIP-322's multi-party PSBT workflow, a creator builds an unsigned signing request, independent signers add partial signatures after detecting the request, and a finalizer assembles and encodes the result.

Rebased on #73

Changes

  • Add detect_bip322_psbt to check the detection properties.
  • Add Bip322Psbt to carry the detected message and challenge for signers.
  • Add create_bip322_psbt to build an unsigned PSBT with the message, UTXO, and script fields set.
  • Add sign_bip322_psbt_input to detect the request, adds a partial signature (or tap_key_sig for taproot) to the first input.
  • Add finalize_bip322_psbt to assemble signatures per BIP174 and encodes as a ful signature.
  • Split create_to_spend into a script-based create_to_spend_from_script so detection can reconstruct to_spend from a scriptPubKey.

Closes #74

@aagbotemi
aagbotemi force-pushed the feat/psbt-multiparty-signing branch from 169b567 to 10744cf Compare July 24, 2026 12:56
@aagbotemi
aagbotemi force-pushed the feat/psbt-multiparty-signing branch 2 times, most recently from 4a82844 to 99e7843 Compare July 27, 2026 10:03
@aagbotemi
aagbotemi force-pushed the feat/psbt-multiparty-signing branch 4 times, most recently from e2123af to ed645d3 Compare August 16, 2026 15:43
@raphjaph

Copy link
Copy Markdown
Collaborator

gmgm @aagbotemi I just merged #73, could you fix the conflicts in this branch and then I'll have a look at this. Thanks!

@aagbotemi
aagbotemi force-pushed the feat/psbt-multiparty-signing branch from ed645d3 to 4d83393 Compare August 24, 2026 15:47
@aagbotemi
aagbotemi force-pushed the feat/psbt-multiparty-signing branch from 4d83393 to 4256d84 Compare August 24, 2026 16:13
@aagbotemi

Copy link
Copy Markdown
Contributor Author

@raphjaph I have fixed the conflict. Thank you.

@aagbotemi
aagbotemi force-pushed the feat/psbt-multiparty-signing branch from 4256d84 to 16cf758 Compare August 24, 2026 16:48

@raphjaph raphjaph left a comment

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.

There's a lot going on in this PR (PSBTs, different address types, multisigs), which is unavoidable but makes it quite complex to review all edge cases. So this is just my first pass review. We definitely need more tests for all this. Here a list of what I can think of

p2tr_psbt_roundtrip
p2wpkh_psbt_roundtrip
p2sh_p2wpkh_psbt_roundtrip
p2pkh_psbt_roundtrip
p2sh_p2wsh_2of2_psbt_roundtrip
p2sh_multisig_2of2_psbt_roundtrip

ordinary_psbt_rejected
missing_global_message_field_rejected
tampered_prevout_txid_rejected
wrong_prevout_vout_rejected
nonzero_output_value_rejected
non_op_return_output_rejected
extra_output_rejected
missing_utxo_fields_rejected

sign_rejects_wrong_key
sign_rejects_key_not_in_multisig_script
sign_rejects_tampered_witness_script
sign_rejects_multi_input_psbt

finalize_rejects_insufficient_multisig_sigs
finalize_rejects_unsigned_single_sig
finalize_rejects_mismatched_partial_sig_key
finalize_rejects_extra_partial_sigs
finalize_rejects_tap_key_sig_on_non_p2tr_challenge

create_rejects_witness_script_for_single_sig
create_rejects_unknown_p2sh_script
create_bare_p2sh_sets_correct_utxo_field

These are a lot of tests but this PR introduces PSBTs, which are passed around between different parties so we need to be very strict about the structure and the edge cases.

Comment thread src/psbt.rs Outdated
Comment on lines +301 to +309
let (pub_key, signature) = psbt.inputs[0]
.partial_sigs
.iter()
.next()
.map(|(pub_key, signature)| (*pub_key, *signature))
.ok_or(Error::SignatureCount {
required: 1,
provided: 0,
})?;

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.

We should bind the pubkey to the challenge here. Also you always just take the first entry and then hardcode the Error to provided = 0. This applies to all 3 single sig checks in this module. It should make sure there's only 1 signature. For the multisig path you're already doing the correct thing.

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.

These probably need a proper helper function or smth

Comment thread src/psbt.rs Outdated
Comment on lines +289 to +296
if let Some(signature) = psbt.inputs[0].tap_key_sig {
let mut witness = Witness::new();
witness.push(signature.to_vec());
psbt.inputs[0].final_script_witness = Some(witness);
psbt.inputs[0].tap_key_sig = None;
psbt.inputs[0].tap_internal_key = None;
return encode_finalized(psbt);
}

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.

We should check that challenge really is P2TR

Comment thread src/psbt.rs
let first = psbt.inputs.first()?;
let first_txin = psbt.unsigned_tx.input.first()?;

let message_challenge = if let Some(txout) = &first.witness_utxo {

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.

We should validate that witness_utxo.value is 0

Comment thread src/psbt.rs Outdated
psbt.inputs[0].tap_internal_key = Some(x_only_public_key);

let prevouts = [TxOut {
value: Amount::from_sat(0),

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.

Suggested change
value: Amount::from_sat(0),
value: Amount::ZERO,

Comment thread src/psbt.rs Outdated
}

let sighash = SighashCache::new(psbt.unsigned_tx.clone())
.p2wpkh_signature_hash(0, challenge, Amount::from_sat(0), sighash_type)

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.

Suggested change
.p2wpkh_signature_hash(0, challenge, Amount::from_sat(0), sighash_type)
.p2wpkh_signature_hash(0, challenge, Amount::ZERO, sighash_type)

You use Amount::from_sat(0) and also Amount::ZERO inconsistently, let's use Amount::ZERO everywhere

Comment thread src/psbt.rs Outdated
Comment on lines +79 to +96
if spk.is_p2wpkh() || spk.is_p2tr() {
return Ok(psbt);
}
if spk.is_p2pkh() {
psbt.inputs[0].witness_utxo = None;
psbt.inputs[0].non_witness_utxo = Some(to_spend);
return Ok(psbt);
}

let Some(witness_script) = witness_script else {
if spk.is_p2sh() {
return Ok(psbt);
}
return Err(Error::InvalidWitness);
};

let p2wsh_program = ScriptBuf::new_p2wsh(&witness_script.wscript_hash());

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.

create_bip322_psbt: witness_script silently ignored for P2WPKH/P2TR; unsignable PSBT returned for bare P2SH, keeping witness_utxo where BIP-174 requires non_witness_utxo

@aagbotemi

Copy link
Copy Markdown
Contributor Author

@raphjaph, all requested changes has been addressed for re-review. Thank you.

@aagbotemi
aagbotemi requested a review from raphjaph August 26, 2026 12:09
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.

Implement multi-party PSBT signing

2 participants