diff --git a/src/lib.rs b/src/lib.rs index 90ee1cd..2c60555 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,23 +1,18 @@ 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; +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, @@ -82,7 +77,7 @@ fn to_spend(address: &Address, message: &str) -> Transaction { } } -fn to_sign(to_spend_tx: Transaction) -> Transaction { +fn blank_to_sign(to_spend_tx: Transaction) -> Transaction { Transaction { version: Version(0), lock_time: LockTime::ZERO, @@ -104,133 +99,6 @@ fn to_sign(to_spend_tx: Transaction) -> Transaction { } } -// #[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)?; - psbt.inputs[0].witness_utxo = Some(TxOut { - value: Amount::from_sat(0), - script_pubkey: to_spend_tx.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() -} - #[cfg(test)] mod tests { use {super::*, std::str::FromStr}; @@ -282,7 +150,7 @@ mod tests { #[test] fn to_sign_txids_correct() { assert_eq!( - to_sign(to_spend( + blank_to_sign(to_spend( &Address::from_str(SEGWIT_ADDRESS).unwrap().assume_checked(), "" )) @@ -292,7 +160,7 @@ mod tests { ); assert_eq!( - to_sign(to_spend( + blank_to_sign(to_spend( &Address::from_str(SEGWIT_ADDRESS).unwrap().assume_checked(), "Hello World" )) @@ -304,43 +172,62 @@ mod tests { #[test] fn verify_and_falsify_taproot() { - assert!(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", + "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() { 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..3b3b672 --- /dev/null +++ b/src/sign.rs @@ -0,0 +1,100 @@ +use { + crate::{blank_to_sign, 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 to_sign_psbt( + to_spend_tx: Transaction, + to_sign_tx: Transaction, +) -> Result { + let mut psbt = Psbt::from_unsigned_tx(to_sign_tx)?; + psbt.inputs[0].witness_utxo = Some(TxOut { + value: Amount::from_sat(0), + script_pubkey: to_spend_tx.output[0].script_pubkey.clone(), + }); + + Ok(psbt) +} + +fn sig_witness(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_tx = to_spend(address, message); + let to_sign_tx = blank_to_sign(to_spend_tx.clone()); + let psbt = to_sign_psbt(to_spend_tx.clone(), to_sign_tx).unwrap(); + + let witness = sig_witness(to_spend_tx, psbt, 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_tx = to_spend(address, message); + let mut to_sign_tx = blank_to_sign(to_spend_tx.clone()); + let psbt = to_sign_psbt(to_spend_tx.clone(), to_sign_tx.clone()).unwrap(); + + to_sign_tx.input[0].witness = sig_witness(to_spend_tx, psbt, wallet.private_key); + + let mut buffer = Vec::new(); + to_sign_tx.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..6cbacfb --- /dev/null +++ b/src/verifier.rs @@ -0,0 +1,134 @@ +use { + crate::{blank_to_sign, 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_tx = to_spend(address, message); + let to_sign_tx = blank_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() +} + +pub fn full_verify(address: &Address, message: &str, to_sign: &str) -> bool { + let to_spend_tx = 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_tx.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_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() +}