From 956e99f4d534f56cb70a23223d17c1a5fa768088 Mon Sep 17 00:00:00 2001 From: djordon Date: Sat, 11 May 2024 23:57:07 -0400 Subject: [PATCH] Break up the utxo_package test file into the tests and a general regtest file --- signer/tests/regtest/mod.rs | 362 +++++++++++++++++++++++++ signer/tests/utxo_packaging.rs | 482 ++++++--------------------------- 2 files changed, 445 insertions(+), 399 deletions(-) create mode 100644 signer/tests/regtest/mod.rs diff --git a/signer/tests/regtest/mod.rs b/signer/tests/regtest/mod.rs new file mode 100644 index 000000000..38ec18652 --- /dev/null +++ b/signer/tests/regtest/mod.rs @@ -0,0 +1,362 @@ +use bitcoin::absolute::LockTime; +use bitcoin::key::TapTweak; +use bitcoin::sighash::Prevouts; +use bitcoin::sighash::SighashCache; +use bitcoin::transaction::Version; +use bitcoin::Address; +use bitcoin::AddressType; +use bitcoin::Amount; +use bitcoin::CompressedPublicKey; +use bitcoin::EcdsaSighashType; +use bitcoin::Network; +use bitcoin::OutPoint; +use bitcoin::PublicKey; +use bitcoin::ScriptBuf; +use bitcoin::Sequence; +use bitcoin::TapLeafHash; +use bitcoin::TapNodeHash; +use bitcoin::TapSighashType; +use bitcoin::Transaction; +use bitcoin::TxIn; +use bitcoin::TxOut; +use bitcoin::Witness; +use bitcoincore_rpc::json::ImportDescriptors; +use bitcoincore_rpc::json::ListUnspentQueryOptions; +use bitcoincore_rpc::json::ListUnspentResultEntry; +use bitcoincore_rpc::json::Timestamp; +use bitcoincore_rpc::jsonrpc::error::Error as JsonRpcError; +use bitcoincore_rpc::jsonrpc::error::RpcError; +use bitcoincore_rpc::Auth; +use bitcoincore_rpc::Client; +use bitcoincore_rpc::Error as BtcRpcError; +use bitcoincore_rpc::RpcApi; +use secp256k1::Keypair; +use secp256k1::Message; +use secp256k1::SECP256K1; +use std::sync::OnceLock; + +/// These must match the username and password in bitcoin.conf +const BITCOIN_CORE_RPC_USERNAME: &str = "alice"; +const BITCOIN_CORE_RPC_PASSWORD: &str = "pw"; + +pub const BITCOIN_CORE_FALLBACK_FEE: Amount = Amount::from_sat(1000); + +/// The name of our wallet on bitcoin-core +const BITCOIN_CORE_WALLET_NAME: &str = "integration-tests-wallet"; + +/// The faucet has a fixed secret key so that any mined amounts are +/// preseved between test runs. +const FAUCET_SECRET_KEY: &str = "0000000000000000000000000000000000000000000000000000000000000001"; + +const FAUCET_LABEL: Option<&str> = Some("faucet"); + +static BTC_CLIENT: OnceLock<(Client, Recipient)> = OnceLock::new(); + +/// Initializes a blockchain and wallet on bitcoin-core. It can be called +/// multiple times (even concurrently) but only generates the client and +/// recipient once. +/// +/// This function does the following: +/// * Creates an RPC client to bitcoin-core +/// * Creates or loads a watch-only wallet on bitcoin-core +/// * Loads a "faucet" private-public key pair with a P2WPKH address. +/// * Has the bitcoin-core wallet watch the generated address. +/// * Ensures that the faucet has at least 1 bitcoin spent to its address. +pub fn initialize_blockchain() -> &'static (Client, Recipient) { + BTC_CLIENT.get_or_init(|| { + let username = BITCOIN_CORE_RPC_USERNAME.to_string(); + let password = BITCOIN_CORE_RPC_PASSWORD.to_string(); + let auth = Auth::UserPass(username, password); + let rpc = Client::new("http://localhost:18443", auth).unwrap(); + + get_or_create_wallet(&rpc, BITCOIN_CORE_WALLET_NAME); + let faucet = Recipient::from_key(FAUCET_SECRET_KEY, AddressType::P2wpkh); + faucet.track_address(&rpc, FAUCET_LABEL); + + let amount = rpc + .get_received_by_address(&faucet.address, Some(1)) + .unwrap(); + + if amount < Amount::from_int_btc(1) { + faucet.generate_blocks(&rpc, 101); + } + + (rpc, faucet) + }) +} + +fn get_or_create_wallet(rpc: &Client, wallet: &str) { + match rpc.load_wallet(wallet) { + // Success + Ok(_) => (), + // This happens if the wallet has already been loaded. + Err(BtcRpcError::JsonRpc(JsonRpcError::Rpc(RpcError { code: -35, .. }))) => (), + // The wallet probably hasn't been created yet, so lets do that + Err(_) => { + // We want a wallet that is watch only, since we manage keys + let disable_private_keys = Some(true); + rpc.create_wallet(wallet, disable_private_keys, None, None, None) + .unwrap(); + } + }; +} + +/// Helper struct for representing an address on bitcoin. +pub struct Recipient { + pub keypair: Keypair, + pub address: Address, +} + +fn descriptor_base(public_key: &PublicKey, kind: AddressType) -> String { + match kind { + AddressType::P2wpkh => format!("wpkh({public_key})"), + AddressType::P2tr => format!("tr({public_key})"), + AddressType::P2pkh => format!("pkh({public_key})"), + // We're missing pay-to-script-hash (P2SH) and + // pay-to-witness-script-hash (P2WSH) + _ => unimplemented!(""), + } +} + +impl Recipient { + /// Generate a new public-private key pair and address of the given + /// kind. + pub fn new(kind: AddressType) -> Self { + let keypair = Keypair::new_global(&mut rand::rngs::OsRng); + let pk = keypair.public_key(); + let address = match kind { + AddressType::P2wpkh => Address::p2wpkh(&CompressedPublicKey(pk), Network::Regtest), + AddressType::P2pkh => Address::p2pkh(PublicKey::new(pk), Network::Regtest), + AddressType::P2tr => { + let (internal_key, _) = pk.x_only_public_key(); + Address::p2tr(SECP256K1, internal_key, None, Network::Regtest) + } + _ => unimplemented!(), + }; + + Recipient { keypair, address } + } + + fn from_key(secret_key: &str, kind: AddressType) -> Self { + let keypair = Keypair::from_seckey_str_global(secret_key).unwrap(); + let pk = keypair.public_key(); + let address = match kind { + AddressType::P2wpkh => Address::p2wpkh(&CompressedPublicKey(pk), Network::Regtest), + AddressType::P2pkh => Address::p2pkh(PublicKey::new(pk), Network::Regtest), + AddressType::P2tr => { + let (internal_key, _) = pk.x_only_public_key(); + Address::p2tr(SECP256K1, internal_key, None, Network::Regtest) + } + _ => unimplemented!(), + }; + + Recipient { keypair, address } + } + + /// Tell bitcoin core to track transactions associated with this address, + /// + /// Note: this is needed in order for get_utxos and get_balance to work + /// as expected. + pub fn track_address(&self, rpc: &Client, label: Option<&str>) { + let public_key = PublicKey::new(self.keypair.public_key()); + let kind = self.address.address_type().unwrap(); + + let desc = descriptor_base(&public_key, kind); + let descriptor_info = rpc.get_descriptor_info(&desc).unwrap(); + + let req = ImportDescriptors { + descriptor: descriptor_info.descriptor, + label: label.map(ToString::to_string), + internal: Some(false), + timestamp: Timestamp::Time(0), + active: None, + next_index: None, + range: None, + }; + let response = rpc.import_descriptors(req).unwrap(); + response.into_iter().for_each(|item| assert!(item.success)); + } + + /// Generate num_blocks blocks with coinbase rewards being sent to this + /// recipient. + pub fn generate_blocks(&self, rpc: &Client, num_blocks: u64) { + rpc.generate_to_address(num_blocks, &self.address).unwrap(); + } + + /// Return all UTXOs for this recipient where the amount is greater + /// than or equal to the given amount. The address must be tracked by + /// the bitcoin-core wallet. + pub fn get_utxos(&self, rpc: &Client, amount: Option) -> Vec { + let query_options = amount.map(|sats| ListUnspentQueryOptions { + minimum_amount: Some(Amount::from_sat(sats)), + ..Default::default() + }); + rpc.list_unspent(None, None, Some(&[&self.address]), None, query_options) + .unwrap() + } + + /// Get the total amount of UTXOs controlled by the recipient. + pub fn get_balance(&self, rpc: &Client) -> Amount { + self.get_utxos(rpc, None) + .into_iter() + .map(|x| x.amount) + .sum() + } + + /// Send the specified amount to the specific address. + /// + /// Note: only P2TR and P2WPKH addresses are supported. + pub fn send_to(&self, rpc: &Client, amount: u64, address: &Address) { + let fee = BITCOIN_CORE_FALLBACK_FEE.to_sat(); + let utxo = self.get_utxos(&rpc, Some(amount + fee)).pop().unwrap(); + + let tx = Transaction { + version: Version::ONE, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::new(utxo.txid, utxo.vout), + sequence: Sequence::ZERO, + script_sig: ScriptBuf::new(), + witness: Witness::new(), + }], + output: vec![ + TxOut { + value: Amount::from_sat(amount), + script_pubkey: address.script_pubkey(), + }, + TxOut { + value: utxo.amount.unchecked_sub(Amount::from_sat(amount + fee)), + script_pubkey: self.address.script_pubkey(), + }, + ], + }; + + let input_index = 0; + let tx = match self.address.address_type().unwrap() { + AddressType::P2wpkh => p2wpkh_sign_transaction(tx, input_index, &utxo, &self.keypair), + AddressType::P2tr => { + let leaf = Either::Left(None); + let (mut tx, signature) = + p2tr_signature(tx, input_index, &[utxo], self.keypair, leaf); + tx.input[input_index].witness = Witness::p2tr_key_spend(&signature); + tx + } + _ => unimplemented!(), + }; + rpc.send_raw_transaction(&tx).unwrap(); + } +} + +pub trait Utxo { + fn amount(&self) -> Amount; + fn script_pubkey(&self) -> &ScriptBuf; + fn to_tx_out(&self) -> TxOut { + TxOut { + value: self.amount(), + script_pubkey: self.script_pubkey().clone(), + } + } +} + +impl Utxo for ListUnspentResultEntry { + fn amount(&self) -> Amount { + self.amount + } + + fn script_pubkey(&self) -> &ScriptBuf { + &self.script_pub_key + } +} + +impl Utxo for TxOut { + fn amount(&self) -> Amount { + self.value + } + + fn script_pubkey(&self) -> &ScriptBuf { + &self.script_pubkey + } +} + +pub fn p2wpkh_sign_transaction( + tx: Transaction, + input_index: usize, + utxo: &U, + keys: &Keypair, +) -> Transaction +where + U: Utxo, +{ + let sighash_type = EcdsaSighashType::All; + let mut sighasher = SighashCache::new(tx); + let sighash = sighasher + .p2wpkh_signature_hash( + input_index, + utxo.script_pubkey().as_script(), + utxo.amount(), + sighash_type, + ) + .expect("failed to create sighash"); + + let msg = Message::from(sighash); + let signature = SECP256K1.sign_ecdsa(&msg, &keys.secret_key()); + + let signature = bitcoin::ecdsa::Signature { signature, sighash_type }; + *sighasher.witness_mut(input_index).unwrap() = Witness::p2wpkh(&signature, &keys.public_key()); + + sighasher.into_transaction() +} + +/// The enum `Either` with variants `Left` and `Right` is a general purpose +/// sum type with two cases. +pub enum Either { + /// A value of type `L`. + Left(L), + /// A value of type `R`. + Right(R), +} + +pub fn p2tr_signature( + tx: Transaction, + input_index: usize, + utxos: &[U], + keypair: Keypair, + leaf_hash: Either, TapLeafHash>, +) -> (Transaction, bitcoin::taproot::Signature) +where + U: Utxo, +{ + let tx_outs: Vec = utxos.iter().map(Utxo::to_tx_out).collect(); + let prevouts = Prevouts::All(tx_outs.as_slice()); + let sighash_type = TapSighashType::Default; + let mut sighasher = SighashCache::new(tx); + + let (sighash, keypair) = match leaf_hash { + Either::Left(merkle_root) => { + let sighash = sighasher + .taproot_key_spend_signature_hash(input_index, &prevouts, sighash_type) + .expect("failed to create taproot key-spend sighash"); + let tweaked = keypair.tap_tweak(SECP256K1, merkle_root); + + (sighash, tweaked.to_inner()) + } + Either::Right(leaf_hash) => { + let sighash = sighasher + .taproot_script_spend_signature_hash( + input_index, + &prevouts, + leaf_hash, + sighash_type, + ) + .expect("failed to create taproot key-spend sighash"); + + (sighash, keypair) + } + }; + + let msg = Message::from(sighash); + let signature = SECP256K1.sign_schnorr(&msg, &keypair); + let signature = bitcoin::taproot::Signature { signature, sighash_type }; + + (sighasher.into_transaction(), signature) +} diff --git a/signer/tests/utxo_packaging.rs b/signer/tests/utxo_packaging.rs index 8d693f5f8..65f0287c9 100644 --- a/signer/tests/utxo_packaging.rs +++ b/signer/tests/utxo_packaging.rs @@ -1,374 +1,110 @@ use bitcoin::absolute::LockTime; use bitcoin::blockdata::opcodes; use bitcoin::hashes::Hash; -use bitcoin::key::TapTweak; -use bitcoin::sighash::Prevouts; -use bitcoin::sighash::SighashCache; use bitcoin::taproot::LeafVersion; use bitcoin::taproot::NodeInfo; use bitcoin::taproot::TaprootSpendInfo; use bitcoin::transaction::Version; -use bitcoin::Address; use bitcoin::AddressType; use bitcoin::Amount; -use bitcoin::CompressedPublicKey; -use bitcoin::EcdsaSighashType; -use bitcoin::Network; use bitcoin::OutPoint; use bitcoin::PubkeyHash; -use bitcoin::PublicKey; use bitcoin::ScriptBuf; use bitcoin::Sequence; use bitcoin::TapLeafHash; -use bitcoin::TapNodeHash; -use bitcoin::TapSighashType; use bitcoin::Transaction; use bitcoin::TxIn; use bitcoin::TxOut; use bitcoin::Witness; -use bitcoincore_rpc::json::ImportDescriptors; -use bitcoincore_rpc::json::ListUnspentQueryOptions; +use bitcoin::XOnlyPublicKey; use bitcoincore_rpc::json::ListUnspentResultEntry; -use bitcoincore_rpc::json::Timestamp; -use bitcoincore_rpc::jsonrpc::error::Error as JsonRpcError; -use bitcoincore_rpc::jsonrpc::error::RpcError; -use bitcoincore_rpc::Auth; -use bitcoincore_rpc::Client; -use bitcoincore_rpc::Error as BtcRpcError; use bitcoincore_rpc::RpcApi; -use secp256k1::Keypair; -use secp256k1::Message; +use secp256k1::SECP256K1; use signer::utxo::DepositRequest; +use signer::utxo::SbtcRequests; use signer::utxo::SignerBtcState; use signer::utxo::SignerUtxo; -// use signer::utxo::WithdrawalRequest; -use signer::utxo::SbtcRequests; -// use secp256k1::Keypair; -use secp256k1::SECP256K1; -use std::sync::OnceLock; -/// These must match the username and password in bitcoin.conf -const BITCOIN_CORE_RPC_USERNAME: &str = "alice"; -const BITCOIN_CORE_RPC_PASSWORD: &str = "pw"; - -const BITCOIN_CORE_FALLBACK_FEE: Amount = Amount::from_sat(1000); - -/// The name of our wallet on bitcoin-core -const BITCOIN_CORE_WALLET_NAME: &str = "integration-tests-wallet"; - -/// The faucet has a fixed secret key so that any mined amounts are -/// preseved between test runs. -const FAUCET_SECRET_KEY: &str = "0000000000000000000000000000000000000000000000000000000000000001"; +mod regtest; +use regtest::Either; +use regtest::Recipient; const SIGNER_ADDRESS_LABEL: Option<&str> = Some("signers-label"); -const FAUCET_LABEL: Option<&str> = Some("faucet"); const DEPOSITS_LABEL: Option<&str> = Some("deposits"); -static BTC_CLIENT: OnceLock<(Client, Recipient)> = OnceLock::new(); - -/// Initializes a blockchain and wallet on bitcoin-core. It can be called -/// multiple times (even concurrently) but only generates the client and -/// recipient once. -/// -/// This function does the following: -/// * Creates an RPC client to bitcoin-core -/// * Creates a watch-only wallet on bitcoin-core -/// * Loads a "faucet" private-public key pair with a P2WPKH address. -/// * Has the bitcoin-core wallet watch the generated address. -/// * Ensures that the faucet has at least 1 bitcoin spent to its address. -fn initialize_blockchain() -> &'static (Client, Recipient) { - BTC_CLIENT.get_or_init(|| { - let username = BITCOIN_CORE_RPC_USERNAME.to_string(); - let password = BITCOIN_CORE_RPC_PASSWORD.to_string(); - let auth = Auth::UserPass(username, password); - let rpc = Client::new("http://localhost:18443", auth).unwrap(); - - get_or_create_wallet(&rpc, BITCOIN_CORE_WALLET_NAME); - let faucet = Recipient::from_key(FAUCET_SECRET_KEY, AddressType::P2wpkh); - faucet.track_address(&rpc, FAUCET_LABEL); - - let amount = rpc - .get_received_by_address(&faucet.address, Some(1)) - .unwrap(); +fn make_deposit( + depositor: &Recipient, + amount: u64, + utxo: &ListUnspentResultEntry, + signers_public_key: XOnlyPublicKey, + faucet_public_key: XOnlyPublicKey, +) -> (Transaction, DepositRequest) { + let fee = regtest::BITCOIN_CORE_FALLBACK_FEE.to_sat(); + let deposit_script = ScriptBuf::builder() + .push_slice([1, 2, 3, 4]) + .push_opcode(opcodes::all::OP_DROP) + .push_opcode(opcodes::all::OP_DUP) + .push_opcode(opcodes::all::OP_HASH160) + .push_slice(PubkeyHash::hash(&signers_public_key.serialize())) + .push_opcode(opcodes::all::OP_EQUALVERIFY) + .push_opcode(opcodes::all::OP_CHECKSIG) + .into_script(); - if amount < Amount::from_int_btc(1) { - faucet.generate_blocks(&rpc, 101); - } + let redeem_script = ScriptBuf::new_op_return([0u8, 1, 2, 3]); + let ver = LeafVersion::TapScript; + let leaf1 = NodeInfo::new_leaf_with_ver(deposit_script.clone(), ver); + let leaf2 = NodeInfo::new_leaf_with_ver(redeem_script.clone(), ver); - (rpc, faucet) - }) -} + let node = NodeInfo::combine(leaf1, leaf2).unwrap(); + let taproot = TaprootSpendInfo::from_node_info(SECP256K1, faucet_public_key, node); + let merkle_root = taproot.merkle_root(); -fn get_or_create_wallet(rpc: &Client, wallet: &str) { - match rpc.load_wallet(wallet) { - // Success - Ok(_) => (), - // This happens if the wallet has already been loaded. - Err(BtcRpcError::JsonRpc(JsonRpcError::Rpc(RpcError { code: -35, .. }))) => (), - // The wallet probably hasn't been created yet, so lets do that - Err(_) => { - // We want a wallet that is watch only, since we manage keys - let disable_private_keys = Some(true); - rpc.create_wallet(wallet, disable_private_keys, None, None, None) - .unwrap(); - } + let deposit_tx = Transaction { + version: Version::ONE, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::new(utxo.txid, utxo.vout), + sequence: Sequence::ZERO, + script_sig: ScriptBuf::new(), + witness: Witness::new(), + }], + output: vec![ + TxOut { + value: Amount::from_sat(amount), + script_pubkey: ScriptBuf::new_p2tr(SECP256K1, faucet_public_key, merkle_root), + }, + TxOut { + value: utxo.amount - Amount::from_sat(amount + fee), + script_pubkey: depositor.address.script_pubkey(), + }, + ], }; -} - -struct Recipient { - keypair: Keypair, - address: Address, -} - -fn descriptor_base(public_key: &PublicKey, kind: AddressType) -> String { - match kind { - AddressType::P2wpkh => format!("wpkh({public_key})"), - AddressType::P2tr => format!("tr({public_key})"), - AddressType::P2pkh => format!("pkh({public_key})"), - // We're missing pay-to-script-hash (P2SH) and - // pay-to-witness-script-hash (P2WSH) - _ => unimplemented!(""), - } -} - -impl Recipient { - /// Generate a new public-private key pair and address of the given - /// kind. - fn new(kind: AddressType) -> Self { - let keys = Keypair::new_global(&mut rand::rngs::OsRng); - let pk = keys.public_key(); - let address = match kind { - AddressType::P2wpkh => Address::p2wpkh(&CompressedPublicKey(pk), Network::Regtest), - AddressType::P2pkh => Address::p2pkh(PublicKey::new(pk), Network::Regtest), - AddressType::P2tr => { - let (internal_key, _) = pk.x_only_public_key(); - Address::p2tr(SECP256K1, internal_key, None, Network::Regtest) - } - _ => unimplemented!(), - }; - - Recipient { keypair: keys, address } - } - - fn from_key(secret_key: &str, kind: AddressType) -> Self { - let keys = Keypair::from_seckey_str_global(secret_key).unwrap(); - let pk = keys.public_key(); - let address = match kind { - AddressType::P2wpkh => Address::p2wpkh(&CompressedPublicKey(pk), Network::Regtest), - AddressType::P2pkh => Address::p2pkh(PublicKey::new(pk), Network::Regtest), - AddressType::P2tr => { - let (internal_key, _) = pk.x_only_public_key(); - Address::p2tr(SECP256K1, internal_key, None, Network::Regtest) - } - _ => unimplemented!(), - }; - - Recipient { keypair: keys, address } - } - - fn track_address(&self, rpc: &Client, label: Option<&str>) { - let public_key = PublicKey::new(self.keypair.public_key()); - let kind = self.address.address_type().unwrap(); - - let desc = descriptor_base(&public_key, kind); - let descriptor_info = rpc.get_descriptor_info(&desc).unwrap(); - - let req = ImportDescriptors { - descriptor: descriptor_info.descriptor, - label: label.map(ToString::to_string), - internal: Some(false), - timestamp: Timestamp::Time(0), - active: None, - next_index: None, - range: None, - }; - let response = rpc.import_descriptors(req).unwrap(); - response.into_iter().for_each(|item| assert!(item.success)); - } - - /// Generate num_blocks blocks with coinbase rewards being sent to this - /// recipient. - fn generate_blocks(&self, rpc: &Client, num_blocks: u64) { - rpc.generate_to_address(num_blocks, &self.address).unwrap(); - } - - /// Return all UTXOs for this recipient where the amount is greater - /// than or equal to the given amount. The address must be tracked by - /// the bitcoin-core wallet. - fn get_utxos(&self, rpc: &Client, amount: Option) -> Vec { - let query_options = amount.map(|sats| ListUnspentQueryOptions { - minimum_amount: Some(Amount::from_sat(sats)), - ..Default::default() - }); - rpc.list_unspent(None, None, Some(&[&self.address]), None, query_options) - .unwrap() - } - - fn get_balance(&self, rpc: &Client) -> Amount { - self.get_utxos(rpc, None) - .into_iter() - .map(|x| x.amount) - .sum() - } - - fn send_to(&self, rpc: &Client, amount: u64, address: &Address) { - let fee = BITCOIN_CORE_FALLBACK_FEE.to_sat(); - let utxo = self.get_utxos(&rpc, Some(amount + fee)).pop().unwrap(); - - let tx = Transaction { - version: Version::ONE, - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint::new(utxo.txid, utxo.vout), - sequence: Sequence::ZERO, - script_sig: ScriptBuf::new(), - witness: Witness::new(), - }], - output: vec![ - TxOut { - value: Amount::from_sat(amount), - script_pubkey: address.script_pubkey(), - }, - TxOut { - value: utxo.amount.unchecked_sub(Amount::from_sat(amount + fee)), - script_pubkey: self.address.script_pubkey(), - }, - ], - }; - - let input_index = 0; - let tx = match self.address.address_type().unwrap() { - AddressType::P2wpkh => p2wpkh_sign_transaction(tx, input_index, &utxo, &self.keypair), - AddressType::P2tr => { - let leaf = Either::Left(None); - let (mut tx, signature) = - p2tr_signature(tx, input_index, &[utxo], self.keypair, leaf); - tx.input[input_index].witness = Witness::p2tr_key_spend(&signature); - tx - } - _ => unimplemented!(), - }; - rpc.send_raw_transaction(&tx).unwrap(); - } -} - -trait Utxo { - fn amount(&self) -> Amount; - fn script_pubkey(&self) -> &ScriptBuf; - fn to_tx_out(&self) -> TxOut { - TxOut { - value: self.amount(), - script_pubkey: self.script_pubkey().clone(), - } - } -} - -impl Utxo for ListUnspentResultEntry { - fn amount(&self) -> Amount { - self.amount - } - fn script_pubkey(&self) -> &ScriptBuf { - &self.script_pub_key - } -} - -impl Utxo for TxOut { - fn amount(&self) -> Amount { - self.value - } - - fn script_pubkey(&self) -> &ScriptBuf { - &self.script_pubkey - } -} - -fn p2wpkh_sign_transaction( - tx: Transaction, - input_index: usize, - utxo: &U, - keys: &Keypair, -) -> Transaction -where - U: Utxo, -{ - let sighash_type = EcdsaSighashType::All; - let mut sighasher = SighashCache::new(tx); - let sighash = sighasher - .p2wpkh_signature_hash( - input_index, - utxo.script_pubkey().as_script(), - utxo.amount(), - sighash_type, - ) - .expect("failed to create sighash"); - - let msg = Message::from(sighash); - let signature = SECP256K1.sign_ecdsa(&msg, &keys.secret_key()); - - let signature = bitcoin::ecdsa::Signature { signature, sighash_type }; - *sighasher.witness_mut(input_index).unwrap() = Witness::p2wpkh(&signature, &keys.public_key()); - - sighasher.into_transaction() -} - -/// The enum `Either` with variants `Left` and `Right` is a general purpose -/// sum type with two cases. -pub enum Either { - /// A value of type `L`. - Left(L), - /// A value of type `R`. - Right(R), -} - -fn p2tr_signature( - tx: Transaction, - input_index: usize, - utxos: &[U], - keypair: Keypair, - leaf_hash: Either, TapLeafHash>, -) -> (Transaction, bitcoin::taproot::Signature) -where - U: Utxo, -{ - let tx_outs: Vec = utxos.iter().map(Utxo::to_tx_out).collect(); - let prevouts = Prevouts::All(tx_outs.as_slice()); - let sighash_type = TapSighashType::Default; - let mut sighasher = SighashCache::new(tx); - - let (sighash, keypair) = match leaf_hash { - Either::Left(merkle_root) => { - let sighash = sighasher - .taproot_key_spend_signature_hash(input_index, &prevouts, sighash_type) - .expect("failed to create taproot key-spend sighash"); - let tweaked = keypair.tap_tweak(SECP256K1, merkle_root); - - (sighash, tweaked.to_inner()) - } - Either::Right(leaf_hash) => { - let sighash = sighasher - .taproot_script_spend_signature_hash( - input_index, - &prevouts, - leaf_hash, - sighash_type, - ) - .expect("failed to create taproot key-spend sighash"); - - (sighash, keypair) - } + let (mut tx, signature) = regtest::p2tr_signature( + deposit_tx, + 0, + &[utxo.clone()], + depositor.keypair, + Either::Left(None), + ); + tx.input[0].witness = Witness::p2tr_key_spend(&signature); + + let req = DepositRequest { + outpoint: OutPoint::new(tx.compute_txid(), 0), + max_fee: fee, + signer_bitmap: Vec::new(), + amount: 25_000_000, + deposit_script: deposit_script.clone(), + redeem_script: redeem_script.clone(), + taproot_public_key: faucet_public_key, + signers_public_key, }; - - let msg = Message::from(sighash); - let signature = SECP256K1.sign_schnorr(&msg, &keypair); - let signature = bitcoin::taproot::Signature { signature, sighash_type }; - - (sighasher.into_transaction(), signature) + (tx, req) } #[test] fn submit_transaction() { - let (rpc, faucet) = initialize_blockchain(); + let (rpc, faucet) = regtest::initialize_blockchain(); let signer = Recipient::new(AddressType::P2tr); signer.track_address(&rpc, SIGNER_ADDRESS_LABEL); @@ -385,12 +121,12 @@ fn submit_transaction() { #[test] fn deposits_add_to_controlled_amounts() { - let (rpc, faucet) = initialize_blockchain(); - let fee = BITCOIN_CORE_FALLBACK_FEE.to_sat(); + let (rpc, faucet) = regtest::initialize_blockchain(); + let fee = regtest::BITCOIN_CORE_FALLBACK_FEE.to_sat(); let signer = Recipient::new(AddressType::P2tr); let depositor = Recipient::new(AddressType::P2tr); - let signers_public_key = PublicKey::new(signer.keypair.public_key()); + let signers_public_key = signer.keypair.x_only_public_key().0; signer.track_address(&rpc, SIGNER_ADDRESS_LABEL); depositor.track_address(rpc, DEPOSITS_LABEL); @@ -401,77 +137,24 @@ fn deposits_add_to_controlled_amounts() { assert_eq!(signer.get_balance(rpc).to_sat(), 100_000_000); assert_eq!(depositor.get_balance(rpc).to_sat(), 50_000_000); - let internal_key = faucet.keypair.x_only_public_key().0; - let depositor_utxo = depositor.get_utxos(rpc, None).pop().unwrap(); let signer_utxo = signer.get_utxos(rpc, None).pop().unwrap(); - let x_signers_public_key = signers_public_key.inner.x_only_public_key().0; - let deposit_script = ScriptBuf::builder() - .push_slice([1, 2, 3, 4]) - .push_opcode(opcodes::all::OP_DROP) - .push_opcode(opcodes::all::OP_DUP) - .push_opcode(opcodes::all::OP_HASH160) - .push_slice(PubkeyHash::hash(&x_signers_public_key.serialize())) - .push_opcode(opcodes::all::OP_EQUALVERIFY) - .push_opcode(opcodes::all::OP_CHECKSIG) - .into_script(); - - let redeem_script = ScriptBuf::new_op_return([0u8, 1, 2, 3]); - let ver = LeafVersion::TapScript; - let leaf1 = NodeInfo::new_leaf_with_ver(deposit_script.clone(), ver); - let leaf2 = NodeInfo::new_leaf_with_ver(redeem_script.clone(), ver); - - let node = NodeInfo::combine(leaf1, leaf2).unwrap(); - let taproot = TaprootSpendInfo::from_node_info(SECP256K1, internal_key, node); - let merkle_root = taproot.merkle_root(); - let deposit_amount = 25_000_000; - let deposit_tx = Transaction { - version: Version::ONE, - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint::new(depositor_utxo.txid, depositor_utxo.vout), - sequence: Sequence::ZERO, - script_sig: ScriptBuf::new(), - witness: Witness::new(), - }], - output: vec![ - TxOut { - value: Amount::from_sat(deposit_amount), - script_pubkey: ScriptBuf::new_p2tr(SECP256K1, internal_key, merkle_root), - }, - TxOut { - value: depositor_utxo.amount - Amount::from_sat(deposit_amount + fee), - script_pubkey: depositor.address.script_pubkey(), - }, - ], - }; - - let (mut deposit_tx, signature) = p2tr_signature( - deposit_tx, - 0, - &[depositor_utxo.clone()], - depositor.keypair, - Either::Left(None), + let (deposit_tx, deposit_request) = make_deposit( + &depositor, + deposit_amount, + &depositor_utxo, + signers_public_key, + faucet.keypair.x_only_public_key().0, ); - deposit_tx.input[0].witness = Witness::p2tr_key_spend(&signature); rpc.send_raw_transaction(&deposit_tx).unwrap(); faucet.generate_blocks(rpc, 1); let requests = SbtcRequests { - deposits: vec![DepositRequest { - outpoint: OutPoint::new(deposit_tx.compute_txid(), 0), - max_fee: 100_000_000, - signer_bitmap: std::iter::repeat(true).take(7).collect(), - amount: 25_000_000, - deposit_script: deposit_script.clone(), - redeem_script: redeem_script.clone(), - taproot_public_key: PublicKey::new(faucet.keypair.public_key()), - signers_public_key, - }], + deposits: vec![deposit_request], withdrawals: Vec::new(), signer_state: SignerBtcState { utxo: SignerUtxo { @@ -494,11 +177,12 @@ fn deposits_add_to_controlled_amounts() { }; let utxos = [signer_utxo_2, deposit_tx.output[0].clone()]; - let leaf_hash = TapLeafHash::from_script(deposit_script.as_script(), ver); + let deposit_script = requests.deposits[0].deposit_script.as_script(); + let leaf_hash = TapLeafHash::from_script(deposit_script, LeafVersion::TapScript); let (tx, signature1) = - p2tr_signature(unsigned.tx, 0, &utxos, signer.keypair, Either::Left(None)); + regtest::p2tr_signature(unsigned.tx, 0, &utxos, signer.keypair, Either::Left(None)); let (mut tx, signature2) = - p2tr_signature(tx, 1, &utxos, signer.keypair, Either::Right(leaf_hash)); + regtest::p2tr_signature(tx, 1, &utxos, signer.keypair, Either::Right(leaf_hash)); tx.input[0].witness = Witness::p2tr_key_spend(&signature1); tx.input[1].witness = requests.deposits[0].construct_witness_data(signature2);