From 43f70a87fe70a5bbfb9e4d6f0e0cd9711e749c52 Mon Sep 17 00:00:00 2001 From: diehuxx Date: Wed, 24 Jul 2024 16:41:31 -0500 Subject: [PATCH 1/3] Full Bip322 signatures --- src/lib.rs | 262 +++++++++++++++--------------------------------- src/sign.rs | 94 +++++++++++++++++ src/verifier.rs | 134 +++++++++++++++++++++++++ 3 files changed, 308 insertions(+), 182 deletions(-) create mode 100644 src/sign.rs create mode 100644 src/verifier.rs diff --git a/src/lib.rs b/src/lib.rs index 90ee1cd..5757253 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,23 +1,19 @@ use { - base64::{engine::general_purpose, Engine}, bitcoin::{ - absolute::LockTime, - blockdata::script, - consensus::{Decodable, Encodable}, - key::{Keypair, TapTweak}, - opcodes, - psbt::Psbt, - script::PushBytes, - secp256k1::{self, schnorr::Signature, Message, Secp256k1, XOnlyPublicKey}, - sighash::{self, SighashCache, TapSighashType}, - transaction::Version, - Address, Amount, Network, OutPoint, PrivateKey, PublicKey, ScriptBuf, Sequence, - Transaction, TxIn, TxOut, Witness, + absolute::LockTime, blockdata::script, opcodes, script::PushBytes, secp256k1::Secp256k1, + transaction::Version, Address, Amount, Network, OutPoint, PrivateKey, PublicKey, ScriptBuf, + Sequence, Transaction, TxIn, TxOut, Witness, }, bitcoin_hashes::{sha256, Hash}, - std::{io::Cursor, str}, }; +mod sign; +use bitcoin::Psbt; +pub use sign::{full_sign, simple_sign}; + +mod verifier; +pub use verifier::{full_verify, simple_verify}; + pub struct Wallet { pub btc_address: Address, pub descriptor: miniscript::Descriptor, @@ -57,7 +53,7 @@ fn message_hash(message: &str) -> Vec { .to_vec() } -fn to_spend(address: &Address, message: &str) -> Transaction { +fn create_to_spend(address: &Address, message: &str) -> Transaction { Transaction { version: Version(0), lock_time: LockTime::ZERO, @@ -82,153 +78,36 @@ fn to_spend(address: &Address, message: &str) -> Transaction { } } -fn to_sign(to_spend_tx: Transaction) -> Transaction { - Transaction { +fn create_to_sign(to_spend: &Transaction) -> Psbt { + let inputs = vec![TxIn { + previous_output: OutPoint { + txid: to_spend.txid(), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence(0), + witness: Witness::new(), + }]; + + let to_sign = Transaction { version: Version(0), lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint { - txid: to_spend_tx.txid(), - vout: 0, - }, - script_sig: ScriptBuf::new(), - sequence: Sequence(0), - witness: Witness::new(), - }], + input: inputs, output: vec![TxOut { value: Amount::from_sat(0), script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) .into_script(), }], - } -} + }; -// #[allow(unused)] -fn to_sign_psbt( - to_spend_tx: Transaction, - to_sign_tx: Transaction, -) -> Result { - let mut psbt = Psbt::from_unsigned_tx(to_sign_tx)?; + let mut psbt = Psbt::from_unsigned_tx(to_sign).unwrap(); psbt.inputs[0].witness_utxo = Some(TxOut { value: Amount::from_sat(0), - script_pubkey: to_spend_tx.output[0].script_pubkey.clone(), + script_pubkey: to_spend.output[0].script_pubkey.clone(), }); - Ok(psbt) -} - -pub fn sign(address: &Address, message: &str, wallet: &Wallet) -> String { - let to_spend_tx = to_spend(address, message); - let to_sign_tx = to_sign(to_spend_tx.clone()); - let mut psbt = to_sign_psbt(to_spend_tx.clone(), to_sign_tx).unwrap(); - - let secp = Secp256k1::new(); - let private_key = wallet.private_key; - let key_pair = Keypair::from_secret_key(&secp, &private_key.inner); - let (x_only_public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair); - - psbt.inputs[0].tap_internal_key = Some(x_only_public_key); - - let sighash_type = TapSighashType::All; - - let mut sighash_cache = SighashCache::new(psbt.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, - ) - .expect("signature hash should compute"); - - let key_pair = key_pair - .tap_tweak(&secp, psbt.inputs[0].tap_merkle_root) - .to_inner(); - - let sig = secp.sign_schnorr_no_aux_rand( - &secp256k1::Message::from_digest_slice(sighash.as_ref()) - .expect("should be cryptographically secure hash"), - &key_pair, - ); - - let witness = sighash_cache - .witness_mut(0) - .expect("getting mutable witness reference should work"); - - witness.push( - bitcoin::taproot::Signature { - sig, - hash_ty: sighash_type, - } - .to_vec(), - ); - - let mut buffer = Vec::new(); - witness.consensus_encode(&mut buffer).unwrap(); - - general_purpose::STANDARD.encode(buffer) -} - -pub fn verify(address: &Address, message: &str, signature: &str) -> bool { - let to_spend_tx = to_spend(address, message); - let to_sign_tx = to_sign(to_spend_tx.clone()); - - let mut cursor = Cursor::new(general_purpose::STANDARD.decode(signature).unwrap()); - - let witness = match Witness::consensus_decode_from_finite_reader(&mut cursor) { - Ok(witness) => witness, - Err(_) => return false, - }; - - let encoded_signature = &witness.to_vec()[0]; - - let (signature, sighash_type) = if encoded_signature.len() == 65 { - ( - Signature::from_slice(&encoded_signature.as_slice()[..64]).unwrap(), - TapSighashType::from_consensus_u8(encoded_signature[64]).unwrap(), - ) - } else if encoded_signature.len() == 64 { - ( - Signature::from_slice(encoded_signature.as_slice()).unwrap(), - TapSighashType::Default, - ) - } else { - return false; - }; - - let pub_key = - if let bitcoin::address::Payload::WitnessProgram(witness_program) = address.payload() { - if witness_program.version().to_num() == 1 && witness_program.program().len() == 32 { - XOnlyPublicKey::from_slice(witness_program.program().as_bytes()).unwrap() - } else { - return false; - } - } else { - return false; - }; - - let mut sighash_cache = SighashCache::new(to_sign_tx); - - 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, - ) - .expect("signature hash should compute"); - - let message = Message::from_digest_slice(sighash.as_ref()).unwrap(); - - Secp256k1::verification_only() - .verify_schnorr(&signature, &message, &pub_key) - .is_ok() + psbt } #[cfg(test)] @@ -259,7 +138,7 @@ mod tests { #[test] fn to_spend_txids_correct() { assert_eq!( - to_spend( + create_to_spend( &Address::from_str(SEGWIT_ADDRESS).unwrap().assume_checked(), "" ) @@ -269,7 +148,7 @@ mod tests { ); assert_eq!( - to_spend( + create_to_spend( &Address::from_str(SEGWIT_ADDRESS).unwrap().assume_checked(), "Hello World" ) @@ -281,66 +160,85 @@ mod tests { #[test] fn to_sign_txids_correct() { + let to_spend = create_to_spend( + &Address::from_str(SEGWIT_ADDRESS).unwrap().assume_checked(), + "", + ); + let to_sign = create_to_sign(&to_spend); assert_eq!( - to_sign(to_spend( - &Address::from_str(SEGWIT_ADDRESS).unwrap().assume_checked(), - "" - )) - .txid() - .to_string(), + to_sign.unsigned_tx.txid().to_string(), "1e9654e951a5ba44c8604c4de6c67fd78a27e81dcadcfe1edf638ba3aaebaed6" ); + let to_spend = create_to_spend( + &Address::from_str(SEGWIT_ADDRESS).unwrap().assume_checked(), + "Hello World", + ); + let to_sign = create_to_sign(&to_spend); assert_eq!( - to_sign(to_spend( - &Address::from_str(SEGWIT_ADDRESS).unwrap().assume_checked(), - "Hello World" - )) - .txid() - .to_string(), + to_sign.unsigned_tx.txid().to_string(), "88737ae86f2077145f93cc4b153ae9a1cb8d56afa511988c149c5c8c9d93bddf" ); } #[test] - fn verify_and_falsify_taproot() { - assert!(verify( - &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), - "Hello World", - "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" - ),); + fn simple_verify_and_falsify_taproot() { + assert!( + simple_verify( + &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), + "Hello World", + "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" + ) + ); - assert!(!verify( - &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), - "Hello World -- This should fail", - "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" - ),); + assert!( + !simple_verify( + &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), + "Hello World -- This should fail", + "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" + ) + ); } #[test] - fn sign_taproot() { + fn simple_sign_taproot() { let wallet = Wallet::new(WIF_PRIVATE_KEY); - let signature = sign( + let signature = simple_sign( &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), "Hello World", &wallet, ); assert_eq!( - signature, - "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" - ); + signature, + "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" + ); + } + + #[test] + fn roundtrip_taproot_simple() { + let wallet = Wallet::new(WIF_PRIVATE_KEY); + + assert!(simple_verify( + &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), + "Hello World", + &simple_sign( + &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), + "Hello World", + &wallet + ) + )); } #[test] - fn roundtrip_taproot() { + fn roundtrip_taproot_full() { let wallet = Wallet::new(WIF_PRIVATE_KEY); - assert!(verify( + assert!(full_verify( &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), "Hello World", - &sign( + &full_sign( &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), "Hello World", &wallet diff --git a/src/sign.rs b/src/sign.rs new file mode 100644 index 0000000..f467af4 --- /dev/null +++ b/src/sign.rs @@ -0,0 +1,94 @@ +use { + crate::{create_to_sign, create_to_spend, Wallet}, + base64::{engine::general_purpose, Engine}, + bitcoin::{ + consensus::Encodable, + key::{Keypair, TapTweak}, + psbt::Psbt, + secp256k1::{self, Secp256k1, XOnlyPublicKey}, + sighash::{self, SighashCache, TapSighashType}, + Address, Amount, PrivateKey, Transaction, TxOut, Witness, + }, +}; + +fn create_message_signature( + to_spend_tx: &Transaction, + to_sign: &Psbt, + private_key: PrivateKey, +) -> Witness { + let mut to_sign = to_sign.clone(); + + let secp = Secp256k1::new(); + let private_key = private_key; + 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); + + 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, + ) + .expect("signature hash should compute"); + + let key_pair = key_pair + .tap_tweak(&secp, to_sign.inputs[0].tap_merkle_root) + .to_inner(); + + let sig = secp.sign_schnorr_no_aux_rand( + &secp256k1::Message::from_digest_slice(sighash.as_ref()) + .expect("should be cryptographically secure hash"), + &key_pair, + ); + + let witness = sighash_cache + .witness_mut(0) + .expect("getting mutable witness reference should work"); + + witness.push( + bitcoin::taproot::Signature { + sig, + hash_ty: sighash_type, + } + .to_vec(), + ); + + witness.to_owned() +} + +pub fn simple_sign(address: &Address, message: &str, wallet: &Wallet) -> String { + let to_spend = create_to_spend(address, message); + let to_sign = create_to_sign(&to_spend); + + let witness = create_message_signature(&to_spend, &to_sign, wallet.private_key); + + let mut buffer = Vec::new(); + witness.consensus_encode(&mut buffer).unwrap(); + + general_purpose::STANDARD.encode(buffer) +} + +pub fn full_sign(address: &Address, message: &str, wallet: &Wallet) -> String { + let to_spend = create_to_spend(address, message); + let mut to_sign = create_to_sign(&to_spend); + + let witness = create_message_signature(&to_spend, &to_sign, wallet.private_key); + to_sign.inputs[0].final_script_witness = Some(witness); + + let mut buffer = Vec::new(); + to_sign + .extract_tx() + .unwrap() + .consensus_encode(&mut buffer) + .unwrap(); + + general_purpose::STANDARD.encode(buffer) +} diff --git a/src/verifier.rs b/src/verifier.rs new file mode 100644 index 0000000..61f01e6 --- /dev/null +++ b/src/verifier.rs @@ -0,0 +1,134 @@ +use { + crate::{create_to_sign, create_to_spend}, + base64::{engine::general_purpose, Engine}, + bitcoin::{ + consensus::Decodable, + secp256k1::{schnorr::Signature, Message, Secp256k1, XOnlyPublicKey}, + sighash::{self, SighashCache, TapSighashType}, + Address, Amount, OutPoint, Transaction, TxOut, Witness, + }, + std::io::Cursor, +}; + +pub fn simple_verify(address: &Address, message: &str, signature: &str) -> bool { + let to_spend = create_to_spend(address, message); + let to_sign = create_to_sign(&to_spend); + + let mut cursor = Cursor::new(general_purpose::STANDARD.decode(signature).unwrap()); + + let witness = match Witness::consensus_decode_from_finite_reader(&mut cursor) { + Ok(witness) => witness, + Err(_) => return false, + }; + + let encoded_signature = &witness.to_vec()[0]; + + let (signature, sighash_type) = if encoded_signature.len() == 65 { + ( + Signature::from_slice(&encoded_signature.as_slice()[..64]).unwrap(), + TapSighashType::from_consensus_u8(encoded_signature[64]).unwrap(), + ) + } else if encoded_signature.len() == 64 { + ( + Signature::from_slice(encoded_signature.as_slice()).unwrap(), + TapSighashType::Default, + ) + } else { + return false; + }; + + let pub_key = + if let bitcoin::address::Payload::WitnessProgram(witness_program) = address.payload() { + if witness_program.version().to_num() == 1 && witness_program.program().len() == 32 { + XOnlyPublicKey::from_slice(witness_program.program().as_bytes()).unwrap() + } else { + return false; + } + } else { + return false; + }; + + let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx); + + 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, + ) + .expect("signature hash should compute"); + + let message = Message::from_digest_slice(sighash.as_ref()).unwrap(); + + Secp256k1::verification_only() + .verify_schnorr(&signature, &message, &pub_key) + .is_ok() +} + +pub fn full_verify(address: &Address, message: &str, to_sign: &str) -> bool { + let to_spend = create_to_spend(address, message); + + let mut cursor = Cursor::new(general_purpose::STANDARD.decode(to_sign).unwrap()); + let to_sign_tx = match Transaction::consensus_decode_from_finite_reader(&mut cursor) { + Ok(to_sign) => to_sign, + Err(_) => return false, + }; + + let to_spend_out_point = OutPoint { + txid: to_spend.txid(), + vout: 0, + }; + + if to_spend_out_point != to_sign_tx.input[0].previous_output { + return false; + } + + let encoded_signature = &to_sign_tx.input[0].witness.to_vec()[0]; + + let (signature, sighash_type) = if encoded_signature.len() == 65 { + ( + Signature::from_slice(&encoded_signature.as_slice()[..64]).unwrap(), + TapSighashType::from_consensus_u8(encoded_signature[64]).unwrap(), + ) + } else if encoded_signature.len() == 64 { + ( + Signature::from_slice(encoded_signature.as_slice()).unwrap(), + TapSighashType::Default, + ) + } else { + return false; + }; + + let pub_key = + if let bitcoin::address::Payload::WitnessProgram(witness_program) = address.payload() { + if witness_program.version().to_num() == 1 && witness_program.program().len() == 32 { + XOnlyPublicKey::from_slice(witness_program.program().as_bytes()).unwrap() + } else { + return false; + } + } else { + return false; + }; + + let mut sighash_cache = SighashCache::new(to_sign_tx); + + 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, + ) + .expect("signature hash should compute"); + + let message = Message::from_digest_slice(sighash.as_ref()).unwrap(); + + Secp256k1::verification_only() + .verify_schnorr(&signature, &message, &pub_key) + .is_ok() +} From c8f8b5003cd743f60637da36a1de98d71bbdcde6 Mon Sep 17 00:00:00 2001 From: diehuxx Date: Wed, 31 Jul 2024 12:55:03 -0700 Subject: [PATCH 2/3] Lint --- src/lib.rs | 12 +++--------- src/sign.rs | 1 - 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5c3755b..25302cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,8 @@ use { bitcoin::{ - absolute::LockTime, - blockdata::script, - opcodes, - psbt::Psbt, - script::PushBytes, - secp256k1::Secp256k1, - transaction::Version, - Address, Amount, Network, OutPoint, PrivateKey, PublicKey, ScriptBuf, Sequence, Transaction, - TxIn, TxOut, Witness, + absolute::LockTime, blockdata::script, opcodes, psbt::Psbt, script::PushBytes, + secp256k1::Secp256k1, transaction::Version, Address, Amount, Network, OutPoint, PrivateKey, + PublicKey, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, }, bitcoin_hashes::{sha256, Hash}, }; diff --git a/src/sign.rs b/src/sign.rs index f4542a3..719b350 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -19,7 +19,6 @@ fn create_message_signature( let mut to_sign = to_sign.clone(); let secp = Secp256k1::new(); - let private_key = private_key; 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); From 7e7cbb670064b2676b2be2c4c95ae574af77c06c Mon Sep 17 00:00:00 2001 From: raphjaph Date: Fri, 2 Aug 2024 13:33:54 -0400 Subject: [PATCH 3/3] Small nits --- src/lib.rs | 38 ++++++++++++++++++---------------- src/{verifier.rs => verify.rs} | 0 2 files changed, 20 insertions(+), 18 deletions(-) rename src/{verifier.rs => verify.rs} (100%) diff --git a/src/lib.rs b/src/lib.rs index 25302cf..2757fcc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,10 +8,12 @@ use { }; mod sign; -pub use sign::{full_sign, simple_sign}; +mod verify; -mod verifier; -pub use verifier::{full_verify, simple_verify}; +pub use { + sign::{full_sign, simple_sign}, + verify::{full_verify, simple_verify}, +}; pub struct Wallet { pub btc_address: Address, @@ -183,20 +185,20 @@ mod tests { #[test] fn simple_verify_and_falsify_taproot() { assert!( - simple_verify( - &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), - "Hello World", - "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" - ) - ); + simple_verify( + &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), + "Hello World", + "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" + ) + ); assert!( - !simple_verify( - &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), - "Hello World -- This should fail", - "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" - ) - ); + !simple_verify( + &Address::from_str(TAPROOT_ADDRESS).unwrap().assume_checked(), + "Hello World -- This should fail", + "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" + ) + ); } #[test] @@ -210,9 +212,9 @@ mod tests { ); assert_eq!( - signature, - "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" - ); + signature, + "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==" + ); } #[test] diff --git a/src/verifier.rs b/src/verify.rs similarity index 100% rename from src/verifier.rs rename to src/verify.rs