diff --git a/README.md b/README.md index a2782f2..d9bc569 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ security is welcome and encouraged. - [x] simple - [x] full -- [ ] full (proof-of-funds) +- [x] full (proof-of-funds) - [x] legacy (BIP-137) The goal is to provide a full signing and verifying library similar to diff --git a/src/error.rs b/src/error.rs index d975c23..2006366 100644 --- a/src/error.rs +++ b/src/error.rs @@ -72,6 +72,8 @@ pub enum Error { PublicKeyMismatch, #[snafu(display("At least one private key is required"))] NoPrivateKeys, + #[snafu(display("At least one proof input is required"))] + NoProofInputs, #[snafu(display("Signer's public key not present in multisig script"))] UnknownSigner, #[snafu(display("Duplicate private key provided"))] @@ -92,4 +94,6 @@ pub enum Error { LegacyRecover { source: bitcoin::sign_message::MessageSignatureError, }, + #[snafu(display("Invalid proof input at index {index}: {reason}"))] + InvalidProofInput { index: usize, reason: String }, } diff --git a/src/lib.rs b/src/lib.rs index c0f6357..b304652 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,6 +77,14 @@ mod tests { const P2SH_MULTISIG_2OF2_PRIVATE_KEY_2: &str = "L4HsBh1Rb5DWP5Hf82tPw3whgwFyt8hdRTChxZQE4HzWfdbVgiWT"; + // PoF constants + const POF_P2TR_ADDRESS: &str = "bc1pk3vq3wpn4txexwq4dj0k2dugzp6kfwllvs89w49cvtk3j2cndcds3l9kw9"; + const POF_P2TR_CHALLENGE_KEY: &str = "L1p7QRghEregYbBvSCp1eW4YJg2RwMYwX2uhR1eAnkVoPJBaJ7Dy"; + const POF_P2TR_PROVEN_KEY_1: &str = "Kz5jBiqQKoppYvaxtWZJicxGZ3G3iJ4rLqNnv7MaQBusyoE731EJ"; + const POF_P2TR_PROVEN_KEY_2: &str = "L2fNJduiUkSytUDbxa58ivWoHevB3svcWUJMxMFebdugYP5jgJr1"; + const POF_P2TR_PROVEN_KEY_3: &str = "KxqVMn81AEYSwYuzBxe6xC4JDAgA2eU2qiNvBAgVZZwRFv1BqN3y"; + const POF_P2TR_MESSAGE: &str = "FUYMQWKYGS7HJEN7YFEZU5SNR5"; + #[test] fn message_hashes_are_correct() { assert_eq!( @@ -149,7 +157,7 @@ mod tests { assert!( verify::verify_simple_encoded( TAPROOT_ADDRESS, - "Hello World", + "Hello World", "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" ).is_ok() ); @@ -208,7 +216,7 @@ mod tests { assert_eq!( verify::verify_simple_encoded( TAPROOT_ADDRESS, - "Hello World", + "Hello World", "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViH" ).unwrap_err().to_string(), "Decode error for signature `AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViH`" @@ -383,8 +391,14 @@ mod tests { let mut aux_rand = [0u8; 32]; rand::rng().fill_bytes(&mut aux_rand); + let prevouts = [TxOut { + value: Amount::from_sat(0), + script_pubkey: to_spend.output[0].script_pubkey.clone(), + }]; + let witness = - create_message_signature_taproot(&to_spend, &to_sign, &private_key, Some(aux_rand)); + create_message_signature_taproot(&to_sign, &private_key, &prevouts, 0, Some(aux_rand)) + .unwrap(); assert!(verify_simple(&address, message, witness).is_ok()); } @@ -776,11 +790,13 @@ mod tests { let to_sign = create_to_sign(&to_spend, None).unwrap(); let witness = create_message_signature_p2wpkh( - &to_spend, &to_sign, &PrivateKey::from_wif(WIF_PRIVATE_KEY).unwrap(), + &to_spend.output[0], + 0, true, - ); + ) + .unwrap(); assert!(matches!( verify::verify_simple(&victim, "foo", witness), @@ -1019,4 +1035,250 @@ mod tests { Error::InvalidWitness )); } + + #[test] + fn roundtrip_pof_p2tr_with_inputs() { + let proof_inputs = vec![ + ProofInput { + outpoint: OutPoint { + txid: "1111111111111111111111111111111111111111111111111111111111111111" + .parse() + .unwrap(), + vout: 0, + }, + prevout: TxOut { + value: Amount::from_sat(345678), + script_pubkey: ScriptBuf::from_hex( + "5120788b90c2b523c73a4237d04df46b232858be3bbc0e65d8d049a7fa59d5719db8", + ) + .unwrap(), + }, + prev_tx: None, + private_keys: vec![PrivateKey::from_wif(POF_P2TR_PROVEN_KEY_1).unwrap()], + witness_script: None, + }, + ProofInput { + outpoint: OutPoint { + txid: "2222222222222222222222222222222222222222222222222222222222222222" + .parse() + .unwrap(), + vout: 1, + }, + prevout: TxOut { + value: Amount::from_sat(345678), + script_pubkey: ScriptBuf::from_hex( + "5120ca0dca0f4f6a2fe99f83c74ce304b031e4b94007b79ae2a94f35355563f9f5ca", + ) + .unwrap(), + }, + prev_tx: None, + private_keys: vec![PrivateKey::from_wif(POF_P2TR_PROVEN_KEY_2).unwrap()], + witness_script: None, + }, + ProofInput { + outpoint: OutPoint { + txid: "3333333333333333333333333333333333333333333333333333333333333333" + .parse() + .unwrap(), + vout: 1, + }, + prevout: TxOut { + value: Amount::from_sat(345678), + script_pubkey: ScriptBuf::from_hex( + "51205c2badbb20cebdce218800dda2fed598e51fab8c30e87112ec967a340b9c3099", + ) + .unwrap(), + }, + prev_tx: None, + private_keys: vec![PrivateKey::from_wif(POF_P2TR_PROVEN_KEY_3).unwrap()], + witness_script: None, + }, + ]; + + let prevouts: Vec = proof_inputs + .iter() + .map(|proof_input| proof_input.prevout.clone()) + .collect(); + + let signature = sign_pof_encoded( + POF_P2TR_ADDRESS, + POF_P2TR_MESSAGE, + &[POF_P2TR_CHALLENGE_KEY], + None, + &proof_inputs, + ) + .unwrap(); + + assert!(verify_pof_encoded(POF_P2TR_ADDRESS, POF_P2TR_MESSAGE, &signature, &prevouts).is_ok()); + } + + #[test] + fn pof_wrong_prevout_is_rejected() { + let proof_inputs = vec![ProofInput { + outpoint: OutPoint { + txid: "1111111111111111111111111111111111111111111111111111111111111111" + .parse() + .unwrap(), + vout: 0, + }, + prevout: TxOut { + value: Amount::from_sat(345678), + script_pubkey: ScriptBuf::from_hex( + "5120788b90c2b523c73a4237d04df46b232858be3bbc0e65d8d049a7fa59d5719db8", + ) + .unwrap(), + }, + prev_tx: None, + private_keys: vec![PrivateKey::from_wif(POF_P2TR_PROVEN_KEY_1).unwrap()], + witness_script: None, + }]; + + // Swap in a different scriptPubKey for verification + let wrong_prevouts = vec![TxOut { + value: Amount::from_sat(345678), + script_pubkey: ScriptBuf::from_hex( + "5120ca0dca0f4f6a2fe99f83c74ce304b031e4b94007b79ae2a94f35355563f9f5ca", + ) + .unwrap(), + }]; + + assert!(matches!( + verify_pof_encoded( + POF_P2TR_ADDRESS, + POF_P2TR_MESSAGE, + &sign_pof_encoded( + POF_P2TR_ADDRESS, + POF_P2TR_MESSAGE, + &[POF_P2TR_CHALLENGE_KEY], + None, + &proof_inputs, + ) + .unwrap(), + &wrong_prevouts + ), + Err(Error::ToSignInvalid) + )); + } + + #[test] + fn roundtrip_pof_with_legacy_input() { + let secp = Secp256k1::new(); + let legacy_key = PrivateKey::from_wif(WIF_PRIVATE_KEY).unwrap(); + let legacy_spk = ScriptBuf::new_p2pkh(&legacy_key.public_key(&secp).pubkey_hash()); + + // The real previous transaction whose output the proof claims + let prev_tx = Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn::default()], + output: vec![TxOut { + value: Amount::from_sat(345678), + script_pubkey: legacy_spk.clone(), + }], + }; + + let proof_inputs = vec![ProofInput { + outpoint: OutPoint { + txid: prev_tx.compute_txid(), + vout: 0, + }, + prevout: prev_tx.output[0].clone(), + prev_tx: Some(prev_tx), + private_keys: vec![legacy_key], + witness_script: None, + }]; + + let prevouts: Vec = proof_inputs + .iter() + .map(|proof_input| proof_input.prevout.clone()) + .collect(); + + assert!(verify_pof_encoded( + POF_P2TR_ADDRESS, + POF_P2TR_MESSAGE, + &sign_pof_encoded( + POF_P2TR_ADDRESS, + POF_P2TR_MESSAGE, + &[POF_P2TR_CHALLENGE_KEY], + None, + &proof_inputs, + ) + .unwrap(), + &prevouts + ) + .is_ok()); + } + + #[test] + fn sign_pof_rejects_no_proof_inputs() { + assert!(matches!( + sign_pof( + &Address::from_str(POF_P2TR_ADDRESS) + .unwrap() + .assume_checked(), + POF_P2TR_MESSAGE, + &[PrivateKey::from_wif(POF_P2TR_CHALLENGE_KEY).unwrap()], + None, + &[], + ), + Err(Error::NoProofInputs) + )); + } + + #[test] + fn roundtrip_pof_with_p2sh_multisig_input() { + let spk = Address::from_str(P2SH_MULTISIG_2OF2_ADDRESS) + .unwrap() + .assume_checked() + .script_pubkey(); + + let prev_tx = Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn::default()], + output: vec![TxOut { + value: Amount::from_sat(345678), + script_pubkey: spk.clone(), + }], + }; + + let proof_inputs = vec![ProofInput { + outpoint: OutPoint { + txid: prev_tx.compute_txid(), + vout: 0, + }, + prevout: prev_tx.output[0].clone(), + prev_tx: Some(prev_tx), + private_keys: vec![ + PrivateKey::from_wif(P2SH_MULTISIG_2OF2_PRIVATE_KEY_1).unwrap(), + PrivateKey::from_wif(P2SH_MULTISIG_2OF2_PRIVATE_KEY_2).unwrap(), + ], + witness_script: Some(ScriptBuf::from_hex(P2SH_MULTISIG_2OF2_REDEEM_SCRIPT).unwrap()), + }]; + + let to_sign = sign_pof( + &Address::from_str(POF_P2TR_ADDRESS) + .unwrap() + .assume_checked(), + POF_P2TR_MESSAGE, + &[PrivateKey::from_wif(POF_P2TR_CHALLENGE_KEY).unwrap()], + None, + &proof_inputs, + ) + .unwrap(); + + // a non-segwit input must carry the full previous transaction (BIP-174) + assert!(to_sign.inputs[1].witness_utxo.is_none()); + assert!(to_sign.inputs[1].non_witness_utxo.is_some()); + + assert!(verify_pof( + &Address::from_str(POF_P2TR_ADDRESS) + .unwrap() + .assume_checked(), + POF_P2TR_MESSAGE, + to_sign, + &[proof_inputs[0].prevout.clone()], + ) + .is_ok()); + } } diff --git a/src/sign.rs b/src/sign.rs index ce8ce70..e41a107 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -1,5 +1,20 @@ use super::*; +/// Extra UTXO included in a proof of funds. +#[derive(Clone, Debug)] +pub struct ProofInput { + /// The outpoint of the UTXO being proven + pub outpoint: OutPoint, + /// The previous output being spent + pub prevout: TxOut, + /// Full previous transaction for this input's outpoint. + pub prev_tx: Option, + /// Key(s) that satisfy the input: one for single-sig, `m` for an `m`-of-`n` multisig + pub private_keys: Vec, + /// Witness/redeem script. + pub witness_script: Option, +} + /// Signs a message in the BIP-137 legacy format from string inputs. #[allow(clippy::result_large_err)] pub fn sign_legacy_encoded(address: &str, message: &str, wif_private_key: &str) -> Result { @@ -97,7 +112,7 @@ pub fn sign_full_encoded( Ok(general_purpose::STANDARD.encode(buffer)) } -/// Signs in the BIP-322 simple format from proper Rust types and returns the witness. +/// Signs in the BIP-322 simple format and returns the witness. #[allow(clippy::result_large_err)] pub fn sign_simple( address: &Address, @@ -122,7 +137,7 @@ pub fn sign_simple( Ok(tx.input[0].witness.clone()) } -/// Signs in the BIP-322 full format from proper Rust types and returns the full transaction. +/// Signs in the BIP-322 full format and returns the full transaction. #[allow(clippy::result_large_err)] pub fn sign_full( address: &Address, @@ -137,57 +152,241 @@ pub fn sign_full( return Err(Error::NoPrivateKeys); } - let witness = match address.to_address_data() { - AddressData::Segwit { witness_program } => { - let version = witness_program.version().to_num(); - let program_len = witness_program.program().len(); - - match version { - 0 => match program_len { - 20 => { - create_message_signature_p2wpkh(&to_spend, &to_sign, single_key(private_keys)?, false) - } - 32 => { - let witness_script = witness_script.ok_or(Error::InvalidWitness)?; - - if address.script_pubkey() != ScriptBuf::new_p2wsh(&witness_script.wscript_hash()) { - return Err(Error::UnsupportedAddress { - address: address.to_string(), - }); - } - - create_message_signature_p2wsh(&to_spend, &to_sign, private_keys, witness_script)? - } - _ => return Err(Error::NotKeyPathSpend), - }, - 1 => { - if program_len != 32 { - return Err(Error::NotKeyPathSpend); - } - create_message_signature_taproot(&to_spend, &to_sign, single_key(private_keys)?, None) - } - _ => { - return Err(Error::UnsupportedAddress { - address: address.to_string(), - }) - } + let prevout = to_spend.output[0].clone(); + + sign_input(&mut to_sign, &[prevout], private_keys, witness_script, 0)?; + + to_sign.extract_tx().context(error::TransactionExtract) +} + +/// Signs the BIP-322 full proof of funds from string inputs. +#[allow(clippy::result_large_err)] +pub fn sign_pof_encoded( + address: &str, + message: &str, + wif_private_keys: &[impl AsRef], + witness_script_hex: Option<&str>, + inputs: &[ProofInput], +) -> Result { + let address = Address::from_str(address) + .context(error::AddressParse { address })? + .assume_checked(); + + let private_keys: Vec = wif_private_keys + .iter() + .map(|private_key| PrivateKey::from_wif(private_key.as_ref()).context(error::PrivateKeyParse)) + .collect::>>()?; + + let witness_script = witness_script_hex + .map(|hex| ScriptBuf::from_hex(hex).context(error::WitnessScriptParse)) + .transpose()?; + + let to_sign = sign_pof( + &address, + message, + &private_keys, + witness_script.as_ref(), + inputs, + )?; + + let mut buffer = Vec::new(); + to_sign + .serialize_to_writer(&mut buffer) + .context(error::TransactionEncode)?; + + Ok(general_purpose::STANDARD.encode(buffer)) +} + +/// Signs a BIP-322 full proof +#[allow(clippy::result_large_err)] +pub fn sign_pof( + address: &Address, + message: impl AsRef<[u8]>, + private_keys: &[PrivateKey], + witness_script: Option<&ScriptBuf>, + inputs: &[ProofInput], +) -> Result { + if private_keys.is_empty() { + return Err(Error::NoPrivateKeys); + } + + if inputs.is_empty() { + return Err(Error::NoProofInputs); + } + + let to_spend = create_to_spend(address, &message)?; + + let mut to_sign = create_to_sign(&to_spend, None)?; + + for input in inputs { + to_sign.unsigned_tx.input.push(TxIn { + previous_output: input.outpoint, + script_sig: ScriptBuf::new(), + sequence: Sequence::ZERO, + witness: Witness::new(), + }); + to_sign.inputs.push(Default::default()); + } + + to_sign.unknown.insert( + bitcoin::psbt::raw::Key { + type_value: PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE, + key: vec![], + }, + message.as_ref().to_vec(), + ); + + // create_to_sign sets a witness_utxo, but a non-segwit challenge (P2PKH or + // bare P2SH) requires the full to_spend transaction instead. + if !is_segwit_input(&to_spend.output[0].script_pubkey, witness_script) { + to_sign.inputs[0].witness_utxo = None; + to_sign.inputs[0].non_witness_utxo = Some(to_spend.clone()); + } + + let mut prevouts = Vec::with_capacity(inputs.len() + 1); + prevouts.push(to_spend.output[0].clone()); + for input in inputs { + prevouts.push(input.prevout.clone()); + } + + sign_input(&mut to_sign, &prevouts, private_keys, witness_script, 0)?; + + for (proof_index, input) in inputs.iter().enumerate() { + let input_index = proof_index + 1; + let spk = &input.prevout.script_pubkey; + + if is_segwit_input(spk, input.witness_script.as_ref()) { + to_sign.inputs[input_index].witness_utxo = Some(input.prevout.clone()); + } else { + let prev_tx = input + .prev_tx + .as_ref() + .ok_or_else(|| Error::InvalidProofInput { + index: proof_index, + reason: "legacy input requires prev_tx".into(), + })?; + + if prev_tx.compute_txid() != input.outpoint.txid { + return Err(Error::InvalidProofInput { + index: proof_index, + reason: "prev_tx txid does not match outpoint".into(), + }); } + + let claimed = prev_tx + .output + .get(input.outpoint.vout as usize) + .ok_or_else(|| Error::InvalidProofInput { + index: proof_index, + reason: "outpoint vout exceeds prev_tx outputs".into(), + })?; + + if *claimed != input.prevout { + return Err(Error::InvalidProofInput { + index: proof_index, + reason: "prevout does not match prev_tx output".into(), + }); + } + + to_sign.inputs[input_index].non_witness_utxo = Some(prev_tx.clone()); } - AddressData::P2sh { script_hash: _ } => match witness_script { + sign_input( + &mut to_sign, + &prevouts, + &input.private_keys, + input.witness_script.as_ref(), + input_index, + )?; + } + + Ok(to_sign) +} + +/// Whether the input is spent via segwit, which decides if BIP-174 requires a +/// `witness_utxo` or a `non_witness_utxo` for it. A P2SH input is only segwit +/// if it wraps a witness program. +fn is_segwit_input(spk: &ScriptBuf, witness_script: Option<&ScriptBuf>) -> bool { + if spk.is_p2wpkh() || spk.is_p2wsh() || spk.is_p2tr() { + true + } else if spk.is_p2sh() { + match witness_script { + Some(ws) => *spk != ScriptBuf::new_p2sh(&ws.script_hash()), + None => true, + } + } else { + false + } +} + +/// Signs input +#[allow(clippy::result_large_err)] +fn sign_input( + to_sign: &mut Psbt, + prevouts: &[TxOut], + private_keys: &[PrivateKey], + witness_script: Option<&ScriptBuf>, + input_index: usize, +) -> Result<()> { + if private_keys.is_empty() { + return Err(Error::NoPrivateKeys); + } + + let spk = &prevouts[input_index].script_pubkey; + + let witness = if spk.is_p2tr() { + create_message_signature_taproot( + to_sign, + single_key(private_keys)?, + prevouts, + input_index, + None, + )? + } else if spk.is_p2wsh() { + let ws = witness_script.ok_or(Error::InvalidWitness)?; + + if *spk != ScriptBuf::new_p2wsh(&ws.wscript_hash()) { + return Err(Error::UnsupportedAddress { + address: spk.to_string(), + }); + } + + create_message_signature_p2wsh( + to_sign, + private_keys, + ws, + &prevouts[input_index], + input_index, + )? + } else if spk.is_p2wpkh() { + create_message_signature_p2wpkh( + to_sign, + single_key(private_keys)?, + &prevouts[input_index], + input_index, + false, + )? + } else if spk.is_p2sh() { + match witness_script { Some(ws) => { let p2wsh_redeem = ScriptBuf::new_p2wsh(&ws.wscript_hash()); - if address.script_pubkey() == ScriptBuf::new_p2sh(&ws.script_hash()) { - create_message_signature_p2sh_multisig(&mut to_sign, private_keys, ws)? - } else if address.script_pubkey() == ScriptBuf::new_p2sh(&p2wsh_redeem.script_hash()) { - let witness = create_message_signature_p2wsh(&to_spend, &to_sign, private_keys, ws)?; + if *spk == ScriptBuf::new_p2sh(&ws.script_hash()) { + create_message_signature_p2sh_multisig(to_sign, private_keys, ws, input_index)? + } else if *spk == ScriptBuf::new_p2sh(&p2wsh_redeem.script_hash()) { + let witness = create_message_signature_p2wsh( + to_sign, + private_keys, + ws, + &prevouts[input_index], + input_index, + )?; - to_sign.inputs[0].final_script_sig = Some(push_only_script(&p2wsh_redeem)); + to_sign.inputs[input_index].final_script_sig = Some(push_only_script(&p2wsh_redeem)); witness } else { return Err(Error::UnsupportedAddress { - address: address.to_string(), + address: spk.to_string(), }); } } @@ -202,42 +401,54 @@ pub fn sign_full( .context(error::UncompressedPublicKey)?; let redeem = ScriptBuf::new_p2wpkh(&wpkh); - if address.script_pubkey() != ScriptBuf::new_p2sh(&redeem.script_hash()) { + if *spk != ScriptBuf::new_p2sh(&redeem.script_hash()) { return Err(Error::UnsupportedAddress { - address: address.to_string(), + address: spk.to_string(), }); } - let witness = create_message_signature_p2wpkh(&to_spend, &to_sign, private_key, true); + let witness = create_message_signature_p2wpkh( + to_sign, + private_key, + &prevouts[input_index], + input_index, + true, + )?; - to_sign.inputs[0].final_script_sig = Some(push_only_script(&redeem)); + to_sign.inputs[input_index].final_script_sig = Some(push_only_script(&redeem)); witness } - }, - AddressData::P2pkh { pubkey_hash: _ } => { - create_message_signature_p2pkh(&to_spend, &mut to_sign, single_key(private_keys)?)? - } - _ => { - return Err(Error::UnsupportedAddress { - address: address.to_string(), - }); } + } else if spk.is_p2pkh() { + create_message_signature_p2pkh( + to_sign, + single_key(private_keys)?, + &prevouts[input_index], + input_index, + )? + } else { + return Err(Error::UnsupportedAddress { + address: spk.to_string(), + }); }; if !witness.is_empty() { - to_sign.inputs[0].final_script_witness = Some(witness); + to_sign.inputs[input_index].final_script_witness = Some(witness); } - to_sign.extract_tx().context(error::TransactionExtract) + + Ok(()) } /// Sign for segwit inputs +#[allow(clippy::result_large_err)] pub fn create_message_signature_p2wpkh( - to_spend_tx: &Transaction, to_sign: &Psbt, private_key: &PrivateKey, + prevout: &TxOut, + input_index: usize, is_p2sh: bool, -) -> Witness { +) -> Result { let secp = Secp256k1::new(); let sighash_type = EcdsaSighashType::All; let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx.clone()); @@ -246,13 +457,17 @@ pub fn create_message_signature_p2wpkh( let sighash = sighash_cache .p2wpkh_signature_hash( - 0, + input_index, &if is_p2sh { - ScriptBuf::new_p2wpkh(&pub_key.wpubkey_hash().unwrap()) + ScriptBuf::new_p2wpkh( + &pub_key + .wpubkey_hash() + .context(error::UncompressedPublicKey)?, + ) } else { - to_spend_tx.output[0].script_pubkey.clone() + prevout.script_pubkey.clone() }, - to_spend_tx.output[0].value, + prevout.value, sighash_type, ) .expect("signature hash should compute"); @@ -264,7 +479,7 @@ pub fn create_message_signature_p2wpkh( ); let witness = sighash_cache - .witness_mut(0) + .witness_mut(input_index) .expect("getting mutable witness reference should work"); witness.push( @@ -277,41 +492,36 @@ pub fn create_message_signature_p2wpkh( witness.push(pub_key.to_bytes()); - witness.to_owned() + Ok(witness.to_owned()) } /// Sign for taproot inputs +#[allow(clippy::result_large_err)] pub fn create_message_signature_taproot( - to_spend_tx: &Transaction, to_sign: &Psbt, private_key: &PrivateKey, + prevouts: &[TxOut], + input_index: usize, aux_rand: Option<[u8; 32]>, -) -> Witness { +) -> Result { let mut to_sign = to_sign.clone(); let secp = Secp256k1::new(); let key_pair = Keypair::from_secret_key(&secp, &private_key.inner); let (x_only_public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair); - to_sign.inputs[0].tap_internal_key = Some(x_only_public_key); + to_sign.inputs[input_index].tap_internal_key = Some(x_only_public_key); let sighash_type = TapSighashType::All; let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx.clone()); let sighash = sighash_cache - .taproot_key_spend_signature_hash( - 0, - &sighash::Prevouts::All(&[TxOut { - value: Amount::from_sat(0), - script_pubkey: to_spend_tx.output[0].clone().script_pubkey, - }]), - sighash_type, - ) + .taproot_key_spend_signature_hash(input_index, &sighash::Prevouts::All(prevouts), sighash_type) .expect("signature hash should compute"); let key_pair = key_pair - .tap_tweak(&secp, to_sign.inputs[0].tap_merkle_root) + .tap_tweak(&secp, to_sign.inputs[input_index].tap_merkle_root) .to_keypair(); let signature = if let Some(aux_rand) = aux_rand { @@ -330,7 +540,7 @@ pub fn create_message_signature_taproot( }; let witness = sighash_cache - .witness_mut(0) + .witness_mut(input_index) .expect("getting mutable witness reference should work"); witness.push( @@ -341,23 +551,24 @@ pub fn create_message_signature_taproot( .to_vec(), ); - witness.to_owned() + Ok(witness.to_owned()) } /// Sign for multisig #[allow(clippy::result_large_err)] pub fn create_message_signature_p2wsh( - to_spend_tx: &Transaction, to_sign: &Psbt, private_keys: &[PrivateKey], witness_script: &ScriptBuf, + prevout: &TxOut, + input_index: usize, ) -> Result { let secp = Secp256k1::new(); let sighash_type = EcdsaSighashType::All; let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx.clone()); let sighash = sighash_cache - .p2wsh_signature_hash(0, witness_script, to_spend_tx.output[0].value, sighash_type) + .p2wsh_signature_hash(input_index, witness_script, prevout.value, sighash_type) .expect("signature hash should compute"); let message = secp256k1::Message::from_digest_slice(sighash.as_ref()) @@ -383,12 +594,13 @@ pub fn create_message_signature_p2sh_multisig( to_sign: &mut Psbt, private_keys: &[PrivateKey], redeem_script: &ScriptBuf, + input_index: usize, ) -> Result { let secp = Secp256k1::new(); let sighash_type = EcdsaSighashType::All; let sighash = SighashCache::new(to_sign.unsigned_tx.clone()) - .legacy_signature_hash(0, redeem_script, sighash_type.to_u32()) + .legacy_signature_hash(input_index, redeem_script, sighash_type.to_u32()) .expect("signature hash should compute"); let message = secp256k1::Message::from_digest_slice(sighash.as_ref()) @@ -396,13 +608,14 @@ pub fn create_message_signature_p2sh_multisig( let signatures = ordered_multisig_signatures(&secp, redeem_script, private_keys, &message)?; + // OP_0 .. let mut builder = ScriptBuf::builder().push_opcode(opcodes::OP_0); for signature in signatures { builder = builder.push_slice(push_bytes(&signature)); } - to_sign.inputs[0].final_script_sig = Some( + to_sign.inputs[input_index].final_script_sig = Some( builder .push_slice(push_bytes(redeem_script.as_bytes())) .into_script(), @@ -411,38 +624,24 @@ pub fn create_message_signature_p2sh_multisig( Ok(Witness::new()) } -#[allow(clippy::result_large_err)] -fn single_key(private_keys: &[PrivateKey]) -> Result<&PrivateKey> { - if private_keys.len() != 1 { - return Err(Error::SignatureCount { - required: 1, - provided: private_keys.len(), - }); - } - - Ok(&private_keys[0]) -} - /// Sign for p2pkh #[allow(clippy::result_large_err)] pub fn create_message_signature_p2pkh( - to_spend_tx: &Transaction, to_sign: &mut Psbt, private_key: &PrivateKey, + prevout: &TxOut, + input_index: usize, ) -> Result { let secp = Secp256k1::new(); let sighash_type = EcdsaSighashType::All; let pub_key = private_key.public_key(&secp); - if to_spend_tx.output[0].script_pubkey != ScriptBuf::new_p2pkh(&pub_key.pubkey_hash()) { + + if prevout.script_pubkey != ScriptBuf::new_p2pkh(&pub_key.pubkey_hash()) { return Err(Error::PublicKeyMismatch); } let sighash = SighashCache::new(to_sign.unsigned_tx.clone()) - .legacy_signature_hash( - 0, - &to_spend_tx.output[0].script_pubkey, - sighash_type.to_u32(), - ) + .legacy_signature_hash(input_index, &prevout.script_pubkey, sighash_type.to_u32()) .expect("signature hash should compute"); let msg = secp256k1::Message::from_digest_slice(sighash.as_ref()) .expect("should be cryptographically secure hash"); @@ -453,7 +652,7 @@ pub fn create_message_signature_p2pkh( } .to_vec(); - to_sign.inputs[0].final_script_sig = Some( + to_sign.inputs[input_index].final_script_sig = Some( ScriptBuf::builder() .push_slice(push_bytes(&sig_bytes)) .push_slice(push_bytes(&pub_key.to_bytes())) @@ -462,3 +661,15 @@ pub fn create_message_signature_p2pkh( Ok(Witness::new()) } + +#[allow(clippy::result_large_err)] +fn single_key(private_keys: &[PrivateKey]) -> Result<&PrivateKey> { + if private_keys.len() != 1 { + return Err(Error::SignatureCount { + required: 1, + provided: private_keys.len(), + }); + } + + Ok(&private_keys[0]) +} diff --git a/src/util.rs b/src/util.rs index fb72b0c..a9026d3 100644 --- a/src/util.rs +++ b/src/util.rs @@ -2,6 +2,10 @@ use super::*; pub const BIP322_TAG: &str = "BIP0322-signed-message"; +/// PSBT global key type for the BIP-322 generic signed message +/// (PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE). +pub const PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE: u8 = 0x09; + /// Create the tagged message hash. pub fn tagged_hash(tag: &str, message: impl AsRef<[u8]>) -> [u8; 32] { let tag_hash = Sha256::new().chain_update(tag).finalize(); diff --git a/src/verify.rs b/src/verify.rs index ed55727..e8f3f49 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -89,7 +89,33 @@ pub fn verify_full_encoded(address: &str, message: &str, to_sign: &str) -> Resul verify_full(&address, message, to_sign) } -/// Verifies the BIP-322 simple from proper Rust types. +/// Verifies a BIP-322 full proof of funds from a spec-compliant string encoding. +/// +/// See [`verify_pof`] for the expected contents of `prevouts`. +#[allow(clippy::result_large_err)] +pub fn verify_pof_encoded( + address: &str, + message: &str, + to_sign: &str, + prevouts: &[TxOut], +) -> Result<()> { + let address = Address::from_str(address) + .context(error::AddressParse { address })? + .assume_checked(); + + let bytes = + general_purpose::STANDARD + .decode(to_sign) + .context(error::TransactionBase64Decode { + transaction: to_sign, + })?; + + let psbt = Psbt::deserialize(&bytes).map_err(|_| Error::ToSignInvalid)?; + + verify_pof(&address, message, psbt, prevouts) +} + +/// Verifies the BIP-322 simple format. #[allow(clippy::result_large_err)] pub fn verify_simple( address: &Address, @@ -105,59 +131,23 @@ pub fn verify_simple( ) } -/// Verifies the BIP-322 full from proper Rust types. +/// Verifies the BIP-322 full format. #[allow(clippy::result_large_err)] pub fn verify_full( address: &Address, message: impl AsRef<[u8]>, to_sign: Transaction, ) -> Result<()> { - match address.to_address_data() { - AddressData::Segwit { witness_program } - if witness_program.version().to_num() == 1 && witness_program.program().len() == 32 => - { - let pub_key = XOnlyPublicKey::from_slice(witness_program.program().as_bytes()) - .map_err(|_| Error::InvalidPublicKey)?; - - verify_full_p2tr(address, message, to_sign, pub_key) - } - AddressData::Segwit { witness_program } - if witness_program.version().to_num() == 0 - && witness_program.program().len() == 32 - && !to_sign.input.is_empty() - && to_sign.input[0].witness.len() > 2 => - { - verify_full_p2wsh(address, message, to_sign) - } - AddressData::Segwit { witness_program } - if witness_program.version().to_num() == 0 - && witness_program.program().len() == 20 - && !to_sign.input.is_empty() - && to_sign.input[0].witness.len() > 1 => - { - let pub_key = - PublicKey::from_slice(&to_sign.input[0].witness[1]).map_err(|_| Error::InvalidPublicKey)?; - - verify_full_p2wpkh(address, message, to_sign, pub_key, false) - } - AddressData::P2sh { script_hash: _ } => { - let input = to_sign.input.first().ok_or(Error::ToSignInvalid)?; - match input.witness.len() { - 0 => verify_full_p2sh_multisig(address, message, to_sign), - 2 => { - let pub_key = - PublicKey::from_slice(&input.witness[1]).map_err(|_| Error::InvalidPublicKey)?; - verify_full_p2wpkh(address, message, to_sign, pub_key, true) - } - n if n > 2 => verify_full_p2wsh(address, message, to_sign), - _ => Err(Error::InvalidWitness), - } - } - AddressData::P2pkh { pubkey_hash: _ } => verify_full_p2pkh(address, message, to_sign), - _ => Err(Error::UnsupportedAddress { - address: address.to_string(), - }), - } + let to_spend = create_to_spend(address, &message)?; + + check_to_sign(&to_spend, &to_sign)?; + + let challenge_prevout = TxOut { + value: Amount::ZERO, + script_pubkey: to_spend.output[0].script_pubkey.clone(), + }; + + verify_input(&to_sign, &[challenge_prevout], 0) } #[allow(clippy::result_large_err)] @@ -175,7 +165,7 @@ fn check_to_sign(to_spend: &Transaction, to_sign: &Transaction) -> Result<()> { || to_sign.input.len() != 1 || to_sign.input[0].previous_output != to_spend_outpoint || to_sign.output.len() != 1 - || to_sign.output[0].value != Amount::from_sat(0) + || to_sign.output[0].value != Amount::ZERO || to_sign.output[0].script_pubkey != op_return { return Err(Error::ToSignInvalid); @@ -184,19 +174,127 @@ fn check_to_sign(to_spend: &Transaction, to_sign: &Transaction) -> Result<()> { Ok(()) } +/// Verifies a BIP-322 full proof of funds. +/// +/// The caller is expected to supply the UTXO set being proven: `prevouts` +/// must hold the previous output for every input of the PSBT's unsigned +/// transaction except the challenge input (input 0), in order. The PSBT's own +/// `witness_utxo` and `non_witness_utxo` fields are cross-checked against +/// `prevouts` when present, but are not required. #[allow(clippy::result_large_err)] -fn verify_full_p2wpkh( +pub fn verify_pof( address: &Address, message: impl AsRef<[u8]>, - to_sign: Transaction, - pub_key: PublicKey, - is_p2sh: bool, + psbt: Psbt, + prevouts: &[TxOut], ) -> Result<()> { - let to_spend = create_to_spend(address, message)?; + let msg_key = bitcoin::psbt::raw::Key { + type_value: PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE, + key: vec![], + }; + match psbt.unknown.get(&msg_key) { + Some(val) if val == message.as_ref() => {} + _ => return Err(Error::ToSignInvalid), + } - check_to_sign(&to_spend, &to_sign)?; + let to_spend = create_to_spend(address, &message)?; + let to_spend_outpoint = OutPoint { + txid: to_spend.compute_txid(), + vout: 0, + }; + + let unsigned_tx = &psbt.unsigned_tx; + + if !matches!(unsigned_tx.version, Version(0) | Version(2)) { + return Err(Error::ToSignInvalid); + } + if unsigned_tx.input.len() < 2 { + return Err(Error::ToSignInvalid); + } + if unsigned_tx.input[0].previous_output != to_spend_outpoint { + return Err(Error::ToSignInvalid); + } + if prevouts.len() != unsigned_tx.input.len() - 1 { + return Err(Error::ToSignInvalid); + } + + if unsigned_tx.output.len() != 1 + || !unsigned_tx.output[0].script_pubkey.is_op_return() + || unsigned_tx.output[0].value != Amount::ZERO + { + return Err(Error::ToSignInvalid); + } + + let mut all_prevouts = Vec::with_capacity(unsigned_tx.input.len()); + all_prevouts.push(TxOut { + value: Amount::from_sat(0), + script_pubkey: to_spend.output[0].script_pubkey.clone(), + }); + all_prevouts.extend_from_slice(prevouts); + + for (index, psbt_input) in psbt.inputs.iter().enumerate() { + if let Some(txout) = &psbt_input.witness_utxo { + if *txout != all_prevouts[index] { + return Err(Error::ToSignInvalid); + } + } + if let Some(tx) = &psbt_input.non_witness_utxo { + let outpoint = psbt.unsigned_tx.input[index].previous_output; + if tx.compute_txid() != outpoint.txid + || tx.output.get(outpoint.vout as usize) != Some(&all_prevouts[index]) + { + return Err(Error::ToSignInvalid); + } + } + } + + let to_sign = psbt.extract_tx_unchecked_fee_rate(); + + for input_index in 0..to_sign.input.len() { + verify_input(&to_sign, &all_prevouts, input_index)?; + } + + Ok(()) +} - let witness = to_sign.input[0].witness.clone(); +/// Verifies input. +#[allow(clippy::result_large_err)] +fn verify_input(to_sign: &Transaction, prevouts: &[TxOut], input_index: usize) -> Result<()> { + let prevout = &prevouts[input_index]; + let spk = &prevout.script_pubkey; + + if spk.is_p2tr() { + verify_full_p2tr(to_sign, prevouts, input_index) + } else if spk.is_p2wsh() { + verify_full_p2wsh(to_sign, prevout, input_index) + } else if spk.is_p2wpkh() { + verify_full_p2wpkh(to_sign, prevout, input_index, false) + } else if spk.is_p2sh() { + let witness = &to_sign.input[input_index].witness; + + match witness.len() { + 0 => verify_full_p2sh_multisig(to_sign, prevout, input_index), + 2 => verify_full_p2wpkh(to_sign, prevout, input_index, true), + n if n > 2 => verify_full_p2wsh(to_sign, prevout, input_index), + _ => Err(Error::InvalidWitness), + } + } else if spk.is_p2pkh() { + verify_full_p2pkh(to_sign, prevout, input_index) + } else { + Err(Error::UnsupportedAddress { + address: spk.to_string(), + }) + } +} + +#[allow(clippy::result_large_err)] +fn verify_full_p2wpkh( + to_sign: &Transaction, + prevout: &TxOut, + input_index: usize, + is_p2sh: bool, +) -> Result<()> { + let witness = to_sign.input[input_index].witness.clone(); if witness.is_empty() { return Err(Error::WitnessEmpty); @@ -209,9 +307,7 @@ fn verify_full_p2wpkh( let encoded_signature = witness.to_vec()[0].clone(); let witness_pub_key = &witness.to_vec()[1]; - if &pub_key.to_bytes() != witness_pub_key { - return Err(Error::PublicKeyMismatch); - } + let pub_key = PublicKey::from_slice(witness_pub_key).map_err(|_| Error::InvalidPublicKey)?; let p2wpkh_script = ScriptBuf::new_p2wpkh( &pub_key @@ -225,11 +321,16 @@ fn verify_full_p2wpkh( p2wpkh_script.clone() }; - if address.script_pubkey() != expected_script_pubkey { + if prevout.script_pubkey != expected_script_pubkey { return Err(Error::PublicKeyMismatch); } - if !is_p2sh && !to_sign.input[0].script_sig.is_empty() { + let script_sig = &to_sign.input[input_index].script_sig; + if is_p2sh { + if !script_sig.is_empty() && *script_sig != push_only_script(&p2wpkh_script) { + return Err(Error::ToSignInvalid); + } + } else if !script_sig.is_empty() { return Err(Error::ToSignInvalid); } @@ -260,7 +361,7 @@ fn verify_full_p2wpkh( let mut sighash_cache = SighashCache::new(to_sign); let sighash = sighash_cache - .p2wpkh_signature_hash(0, &p2wpkh_script, to_spend.output[0].value, sighash_type) + .p2wpkh_signature_hash(input_index, &p2wpkh_script, prevout.value, sighash_type) .expect("signature hash should compute"); let message = @@ -274,21 +375,17 @@ fn verify_full_p2wpkh( } #[allow(clippy::result_large_err)] -fn verify_full_p2tr( - address: &Address, - message: impl AsRef<[u8]>, - to_sign: Transaction, - pub_key: XOnlyPublicKey, -) -> Result<()> { - let to_spend = create_to_spend(address, message)?; +fn verify_full_p2tr(to_sign: &Transaction, prevouts: &[TxOut], input_index: usize) -> Result<()> { + let prevout = &prevouts[input_index]; - check_to_sign(&to_spend, &to_sign)?; + let pub_key = XOnlyPublicKey::from_slice(&prevout.script_pubkey.as_bytes()[2..]) + .map_err(|_| Error::InvalidPublicKey)?; - if !to_sign.input[0].script_sig.is_empty() { + if !to_sign.input[input_index].script_sig.is_empty() { return Err(Error::ToSignInvalid); } - let witness = to_sign.input[0].witness.clone(); + let witness = to_sign.input[input_index].witness.clone(); if witness.is_empty() { return Err(Error::WitnessEmpty); @@ -324,14 +421,7 @@ fn verify_full_p2tr( let mut sighash_cache = SighashCache::new(to_sign); let sighash = sighash_cache - .taproot_key_spend_signature_hash( - 0, - &sighash::Prevouts::All(&[TxOut { - value: Amount::from_sat(0), - script_pubkey: to_spend.output[0].clone().script_pubkey, - }]), - sighash_type, - ) + .taproot_key_spend_signature_hash(input_index, &sighash::Prevouts::All(prevouts), sighash_type) .expect("signature hash should compute"); let message = @@ -344,54 +434,46 @@ fn verify_full_p2tr( /// Verify a BIP-322 proof for a P2WSH #[allow(clippy::result_large_err)] -fn verify_full_p2wsh( - address: &Address, - message: impl AsRef<[u8]>, - to_sign: Transaction, -) -> Result<()> { - let to_spend = create_to_spend(address, message)?; - - check_to_sign(&to_spend, &to_sign)?; +fn verify_full_p2wsh(to_sign: &Transaction, prevout: &TxOut, input_index: usize) -> Result<()> { + let witness_items = to_sign.input[input_index].witness.to_vec(); - let items = to_sign.input[0].witness.to_vec(); - - if items.len() < 3 { + if witness_items.len() < 3 { return Err(Error::InvalidWitness); } - if !items[0].is_empty() { + if !witness_items[0].is_empty() { return Err(Error::InvalidWitness); } - let witness_script = ScriptBuf::from_bytes(items[items.len() - 1].clone()); + let witness_script = ScriptBuf::from_bytes(witness_items[witness_items.len() - 1].clone()); let program = ScriptBuf::new_p2wsh(&witness_script.wscript_hash()); - let spk = address.script_pubkey(); + let script_sig = &to_sign.input[input_index].script_sig; - let expected_script_sig = if spk == ScriptBuf::new_p2sh(&program.script_hash()) { - push_only_script(&program) - } else if spk == program { - ScriptBuf::new() + if prevout.script_pubkey == program { + if !script_sig.is_empty() { + return Err(Error::ToSignInvalid); + } + } else if prevout.script_pubkey == ScriptBuf::new_p2sh(&program.script_hash()) { + if *script_sig != push_only_script(&program) { + return Err(Error::ToSignInvalid); + } } else { return Err(Error::ToSignInvalid); - }; - - if to_sign.input[0].script_sig != expected_script_sig { - return Err(Error::ToSignInvalid); } - let (required, pubkeys) = parse_multisig(&witness_script)?; + let (required_signatures, pubkeys) = parse_multisig(&witness_script)?; - let signatures = &items[1..items.len() - 1]; - if signatures.len() != required { + let signatures = &witness_items[1..witness_items.len() - 1]; + if signatures.len() != required_signatures { return Err(Error::InvalidWitness); } - let sighash = SighashCache::new(&to_sign) + let sighash = SighashCache::new(to_sign) .p2wsh_signature_hash( - 0, + input_index, &witness_script, - to_spend.output[0].value, + prevout.value, EcdsaSighashType::All, ) .expect("signature hash should compute"); @@ -401,6 +483,7 @@ fn verify_full_p2wsh( let secp = Secp256k1::verification_only(); + // CHECKMULTISIG: signatures must appear in the same order as pubkeys let mut sig_index = 0usize; for pub_key in &pubkeys { if sig_index == signatures.len() { @@ -444,16 +527,12 @@ fn verify_full_p2wsh( /// Verify a BIP-322 proof for a P2SH multisig address #[allow(clippy::result_large_err)] fn verify_full_p2sh_multisig( - address: &Address, - message: impl AsRef<[u8]>, - to_sign: Transaction, + to_sign: &Transaction, + prevout: &TxOut, + input_index: usize, ) -> Result<()> { - let to_spend = create_to_spend(address, message)?; - - check_to_sign(&to_spend, &to_sign)?; - let mut pushes: Vec> = Vec::new(); - for instruction in to_sign.input[0].script_sig.instructions() { + for instruction in to_sign.input[input_index].script_sig.instructions() { match instruction.map_err(|_| Error::InvalidWitness)? { Instruction::PushBytes(b) => pushes.push(b.as_bytes().to_vec()), _ => return Err(Error::InvalidWitness), @@ -465,7 +544,7 @@ fn verify_full_p2sh_multisig( }; let redeem_script = ScriptBuf::from_bytes(redeem_bytes.clone()); - if address.script_pubkey() != ScriptBuf::new_p2sh(&redeem_script.script_hash()) { + if prevout.script_pubkey != ScriptBuf::new_p2sh(&redeem_script.script_hash()) { return Err(Error::ToSignInvalid); } @@ -482,8 +561,8 @@ fn verify_full_p2sh_multisig( return Err(Error::InvalidWitness); } - let sighash = SighashCache::new(&to_sign) - .legacy_signature_hash(0, &redeem_script, EcdsaSighashType::All.to_u32()) + let sighash = SighashCache::new(to_sign) + .legacy_signature_hash(input_index, &redeem_script, EcdsaSighashType::All.to_u32()) .expect("signature hash should compute"); let message = Message::from_digest_slice(sighash.as_ref()).expect("should be cryptographically secure hash"); @@ -522,20 +601,13 @@ fn verify_full_p2sh_multisig( /// Verify a BIP-322 proof for a P2PKH #[allow(clippy::result_large_err)] -fn verify_full_p2pkh( - address: &Address, - message: impl AsRef<[u8]>, - to_sign: Transaction, -) -> Result<()> { - let to_spend = create_to_spend(address, message)?; - - check_to_sign(&to_spend, &to_sign)?; - - if !to_sign.input[0].witness.is_empty() { +fn verify_full_p2pkh(to_sign: &Transaction, prevout: &TxOut, input_index: usize) -> Result<()> { + if !to_sign.input[input_index].witness.is_empty() { return Err(Error::InvalidWitness); } - let mut instructions = to_sign.input[0].script_sig.instructions(); + // scriptSig: + let mut instructions = to_sign.input[input_index].script_sig.instructions(); let signature_bytes = match instructions.next() { Some(Ok(Instruction::PushBytes(b))) => b.as_bytes(), _ => return Err(Error::InvalidWitness), @@ -550,7 +622,7 @@ fn verify_full_p2pkh( let pub_key = PublicKey::from_slice(pubkey_bytes).map_err(|_| Error::InvalidPublicKey)?; - if address.script_pubkey() != ScriptBuf::new_p2pkh(&pub_key.pubkey_hash()) { + if prevout.script_pubkey != ScriptBuf::new_p2pkh(&pub_key.pubkey_hash()) { return Err(Error::PublicKeyMismatch); } @@ -567,10 +639,10 @@ fn verify_full_p2pkh( let signature = bitcoin::secp256k1::ecdsa::Signature::from_der(der).context(error::SignatureInvalid)?; - let sighash = SighashCache::new(&to_sign) + let sighash = SighashCache::new(to_sign) .legacy_signature_hash( - 0, - &to_spend.output[0].script_pubkey, + input_index, + &prevout.script_pubkey, EcdsaSighashType::All.to_u32(), ) .expect("signature hash should compute");