diff --git a/bitcoind-tests/Cargo.toml b/bitcoind-tests/Cargo.toml index 5a68bda..a94c143 100644 --- a/bitcoind-tests/Cargo.toml +++ b/bitcoind-tests/Cargo.toml @@ -5,9 +5,12 @@ authors = ["Tobin C. Harding "] license = "CC0-1.0" edition = "2021" -# This crate is only used for testing, no features or dependencies. - -[dev-dependencies] +[dependencies] anyhow = "1" +bitcoin = { version = "0.31.0", features = ["rand-std"] } bitcoind = { version = "0.34.1", features = ["25_1"] } -psbt-v2 = { path = "..", features = ["std"] } +secp256k1 = { version = "0.28.2", features = ["global-context", "rand-std"] } +psbt-v2 = { path = "..", features = ["std", "miniscript-std"] } + +[dev-dependencies] + diff --git a/bitcoind-tests/src/client.rs b/bitcoind-tests/src/client.rs new file mode 100644 index 0000000..f562c3a --- /dev/null +++ b/bitcoind-tests/src/client.rs @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Implements a client that wraps the `bitcoind` client (which uses the `bitcoincore-rpc` client). +//! +//! Adds balance tracking that is specific to how Bitcoin Core works on regtest. + +// We depend upon and import directly from bitcoin because this module is not concerned with PSBT +// i.e., it is lower down the stack than the psbt_v2 crate. +use bitcoin::{consensus, Address, Amount, Network, Transaction, Txid}; +use bitcoind::bitcoincore_rpc::bitcoincore_rpc_json::{AddressType, GetBlockchainInfoResult}; +use bitcoind::bitcoincore_rpc::RpcApi; +use bitcoind::BitcoinD; + +const NETWORK: Network = Network::Regtest; +const FIFTY_BTC: Amount = Amount::from_int_btc(50); + +/// A custom bitcoind client. +pub struct Client { + /// Handle for the regtest `bitcoind` instance. + bitcoind: BitcoinD, + /// This is public so we don't have to handle the complexity of know if send/receives are + /// to/from the Core controlled wallet or somewhere else. User is required to manage this. + pub balance: BalanceTracker, +} + +impl Client { + /// Creates a new [`Client`]. + pub fn new() -> anyhow::Result { + let bitcoind = BitcoinD::from_downloaded()?; + let balance = BalanceTracker::zero(); + + let client = Client { bitcoind, balance }; + + // Sanity check. + assert_eq!(0, client.get_blockchain_info().unwrap().blocks); + + client.mine_blocks(100)?; + assert_eq!(100, client.get_blockchain_info().unwrap().blocks); + + client.assert_balance_is_as_expected()?; // Sanity check. + + Ok(client) + } + + /// Mines a block to a new address controlled by the currently loaded Bitcoin Core wallet. + pub fn mine_a_block(&mut self) -> anyhow::Result<()> { + self.mine_blocks(1)?; + self.balance.mine_a_block(); + Ok(()) + } + + /// Returns the amount in the balance tracker. + pub fn tracked_balance(&self) -> Amount { self.balance.balance } + + #[track_caller] + pub fn assert_balance_is_as_expected(&self) -> anyhow::Result<()> { + let balance = self.balance()?; + self.balance.assert(balance); + Ok(()) + } + + /// Calls through to bitcoincore_rpc client. + pub fn get_blockchain_info(&self) -> anyhow::Result { + let client = &self.bitcoind.client; + Ok(client.get_blockchain_info()?) + } + + /// Gets an address controlled by the currently loaded Bitcoin Core wallet (via `bitcoind`). + pub fn core_wallet_controlled_address(&self) -> anyhow::Result
{ + let client = &self.bitcoind.client; + let label = None; + let address_type = Some(AddressType::Bech32m); + let address = client.get_new_address(label, address_type)?.require_network(NETWORK)?; + Ok(address) + } + + pub fn balance(&self) -> anyhow::Result { + let client = &self.bitcoind.client; + let minconf = None; // What is this? + let include_watchonly = None; + Ok(client.get_balance(minconf, include_watchonly)?) + } + + /// Mines `n` blocks to a new address controlled by the currently loaded Bitcoin Core wallet. + fn mine_blocks(&self, n: u64) -> anyhow::Result<()> { + let client = &self.bitcoind.client; + // Generate to an address controlled by the bitcoind wallet and wait for funds to mature. + let address = self.core_wallet_controlled_address()?; + let _ = client.generate_to_address(n, &address)?; + + Ok(()) + } + + /// Send `amount` to `address` setting all other `bitcoincore_prc::send_to_address` args to `None`. + /// + /// Caller required to update balance (ie, call self.balance.send()). + pub fn send(&self, amount: Amount, address: &Address) -> anyhow::Result { + let client = &self.bitcoind.client; + + let comment = None; + let comment_to = None; + let subtract_fee = None; + let replacable = None; + let confirmation_target = None; + let estimate_mode = None; + + let txid = client.send_to_address( + address, + amount, + comment, + comment_to, + subtract_fee, + replacable, + confirmation_target, + estimate_mode, + )?; + + Ok(txid) + } + + pub fn get_transaction(&self, txid: &Txid) -> anyhow::Result { + let client = &self.bitcoind.client; + let include_watchonly = None; + let res = client.get_transaction(txid, include_watchonly)?; + let tx: Transaction = consensus::encode::deserialize(&res.hex)?; + Ok(tx) + } + + pub fn send_raw_transaction(&self, tx: &Transaction) -> anyhow::Result { + let client = &self.bitcoind.client; + let hex = consensus::encode::serialize_hex(&tx); + let txid = client.send_raw_transaction(hex)?; + Ok(txid) + } +} + +/// Tracks the amount we expect the Core controlled wallet to hold. +/// +/// We are sending whole bitcoin amounts back and forth, as a rough check that the transactions have +/// been mined we test against the integer floor of the amount, this allows us to not track fees. +pub struct BalanceTracker { + balance: Amount, +} + +impl BalanceTracker { + /// Creates a new `BalanceTracker`. + fn zero() -> Self { Self { balance: Amount::ZERO } } + + /// Everytime we mine a block we release another coinbase reward. + fn mine_a_block(&mut self) { self.balance += FIFTY_BTC } + + /// Mimic sending, deduct token fee amount. + pub fn send_to_self(&mut self) { self.send(Amount::ZERO) } + + /// Update balance by sending `amount`. + pub fn send(&mut self, amount: Amount) { + // 1000 mimics some fee amount, the exact amount is not important + // because we ignore everything after the decimal place. + self.balance = self.balance - amount - Amount::from_sat(1000); + } + + /// Update balance by receiving `amount`. + pub fn receive(&mut self, amount: Amount) { + // 1000 mimics some fee amount, the exact amount is not important + // because we ignore everything after the decimal place. + self.balance = self.balance + amount + FIFTY_BTC - Amount::from_sat(1000) + } + + /// Asserts balance against `want` ignoring everything except + /// whole bitcoin, this allows us to ignore fees. + #[track_caller] + fn assert(&self, want: Amount) { + let got = floor(self.balance); + let floor_want = floor(want); + if got != floor_want { + panic!("We have {} but were expecting to have {} ({})", got, floor_want, want); + } + } +} + +fn floor(x: Amount) -> Amount { + let one = 100_000_000; + let sats = x.to_sat(); + Amount::from_sat(sats / one * one) +} diff --git a/bitcoind-tests/src/lib.rs b/bitcoind-tests/src/lib.rs new file mode 100644 index 0000000..4992c64 --- /dev/null +++ b/bitcoind-tests/src/lib.rs @@ -0,0 +1,7 @@ +//! Tools to help with testing against Bitcoin Core using [`bitcoind`] and [`bitcoincore-rpc`]. +//! +//! [`bitcoind`]: +//! [`bitcoincore-rpc`]: + +/// A wrapper around the `bitcoind` client. +pub mod client; diff --git a/bitcoind-tests/tests/basic.rs b/bitcoind-tests/tests/basic.rs new file mode 100644 index 0000000..039e4d4 --- /dev/null +++ b/bitcoind-tests/tests/basic.rs @@ -0,0 +1,108 @@ +//! A basic PSBT test, a single entity using PSBTv2 to create and sign a transaction. + +use core::str::FromStr; + +// Only depend on `psbt` (and `bitcoind_tests`) because we are explicitly testing the `psbt_v2` crate. +use bitcoind_tests::client::Client; +use psbt::bitcoin::{Address, Amount, Network, OutPoint, PublicKey, Script, Transaction, TxOut}; +use psbt::v2::{Constructor, InputBuilder, Modifiable, OutputBuilder}; +// The `psbt_v2` crate, as we expect downstream to use it +// E.g., in manifest file `use psbt = { package = "psbt_v2" ... }` +use psbt_v2 as psbt; + +const NETWORK: Network = Network::Regtest; +const TWO_BTC: Amount = Amount::from_int_btc(2); +const ONE_BTC: Amount = Amount::from_int_btc(1); +const FEE: Amount = Amount::from_sat(1000); // Arbitrary fee. + +#[test] +fn basic() -> anyhow::Result<()> { + // Create the RPC client and a wallet controlled by Bitcoin Core. + let mut client = Client::new()?; + // Fund the wallet with 50 BTC. + client.mine_a_block()?; + + // Create an entity who wishes to use PSBTs to create a transaction. + let alice = Alice::new(); + let alice_address = alice.address(); + + // Send coin from the Core controlled wallet to Alice. + let txid = client.send(TWO_BTC, &alice.address())?; + client.balance.send(TWO_BTC); + client.mine_a_block()?; + client.assert_balance_is_as_expected()?; + + // Get the chain data for Alice's UTXO shew wishes to spend from. + let tx = client.get_transaction(&txid)?; + let utxos = tx.outputs_encumbered_by(&alice_address.script_pubkey()); + assert_eq!(utxos.len(), 1); + let (out_point, fund) = utxos[0]; + + let receiver = client.core_wallet_controlled_address()?; + let spend_amount = ONE_BTC; + let change_amount = fund.value - spend_amount - FEE; + + let constructor = Constructor::::default(); + + let spend_output = TxOut { value: spend_amount, script_pubkey: receiver.script_pubkey() }; + let change_output = TxOut { + value: change_amount, + // Since this is a basic example, just send back to same address. + script_pubkey: alice_address.script_pubkey(), + }; + + let input = InputBuilder::new(&out_point).segwit_fund(fund.clone()).build(); + let spend = OutputBuilder::new(spend_output).build(); + let change = OutputBuilder::new(change_output).build(); + + let psbt = constructor.input(input).output(spend).output(change).psbt()?; + psbt.determine_lock_time()?; + + // Serialize and pass to hardware wallet to sign. + println!("PSBTv2 ready for signing\n{:#?}", psbt); + + Ok(()) +} + +pub trait TransactionExt { + /// Returns a list of UTXOs in this transaction that are encumbered by `script_pubkey`. + fn outputs_encumbered_by(&self, script_pubkey: &Script) -> Vec<(OutPoint, &TxOut)>; +} + +impl TransactionExt for Transaction { + fn outputs_encumbered_by(&self, script_pubkey: &Script) -> Vec<(OutPoint, &TxOut)> { + let mut utxos = vec![]; + for (index, utxo) in self.output.iter().enumerate() { + if &utxo.script_pubkey == script_pubkey { + let out_point = OutPoint { txid: self.txid(), vout: index as u32 }; + + utxos.push((out_point, utxo)); + } + } + utxos + } +} + +/// A super basic entity with a single public key. +pub struct Alice { + /// The single public key. + public_key: PublicKey, +} + +impl Alice { + /// Creates a new Alice. + pub fn new() -> Self { + // An arbitrary public key, assume the secret key is held by another entity. + let public_key = PublicKey::from_str( + "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af", + ) + .unwrap(); + + Alice { public_key } + } + + /// Returns a bech32m address from a key Alice controls. + pub fn address(&self) -> Address { + Address::p2wpkh(&self.public_key, NETWORK).expect("uncompressed key") + } +} diff --git a/bitcoind-tests/tests/client.rs b/bitcoind-tests/tests/client.rs deleted file mode 100644 index 073964d..0000000 --- a/bitcoind-tests/tests/client.rs +++ /dev/null @@ -1,70 +0,0 @@ -use bitcoind::bitcoincore_rpc::bitcoincore_rpc_json::{AddressType, GetBlockchainInfoResult}; -use bitcoind::bitcoincore_rpc::RpcApi; -use bitcoind::BitcoinD; -use psbt_v2::bitcoin::{Address, Amount, Network, Txid}; - -const NETWORK: Network = Network::Regtest; - -/// A custom bitcoind client. -pub struct Client { - bitcoind: BitcoinD, -} - -impl Client { - /// Creates a new [`Client`]. - pub fn new() -> anyhow::Result { - let bitcoind = BitcoinD::from_downloaded()?; - let client = Client { bitcoind }; - - Ok(client) - } - - /// Calls through to bitcoincore_rpc client. - pub fn get_blockchain_info(&self) -> anyhow::Result { - let client = &self.bitcoind.client; - Ok(client.get_blockchain_info()?) - } - - /// Gets an address controlled by the currently loaded Bitcoin Core wallet (via `bitcoind`). - pub fn core_wallet_controlled_address(&self) -> anyhow::Result
{ - let client = &self.bitcoind.client; - let label = None; - let address_type = Some(AddressType::Bech32m); - let address = client.get_new_address(label, address_type)?.require_network(NETWORK)?; - Ok(address) - } - - /// Funds the bitcoind wallet with a spendable 50 BTC utxo. - pub fn fund(&self) -> anyhow::Result<()> { - let client = &self.bitcoind.client; - // Generate to an address controlled by the bitcoind wallet and wait for funds to mature. - let address = self.core_wallet_controlled_address()?; - let _ = client.generate_to_address(101, &address)?; - - Ok(()) - } - - /// Send `amount` to `address` setting all other `bitcoincore_prc::send_to_address` args to `None`. - pub fn send(&self, amount: Amount, address: &Address) -> anyhow::Result { - let client = &self.bitcoind.client; - - let comment = None; - let comment_to = None; - let subtract_fee = None; - let replacable = None; - let confirmation_target = None; - let estimate_mode = None; - - let txid = client.send_to_address( - address, - amount, - comment, - comment_to, - subtract_fee, - replacable, - confirmation_target, - estimate_mode, - )?; - Ok(txid) - } -} diff --git a/bitcoind-tests/tests/infrastructure.rs b/bitcoind-tests/tests/infrastructure.rs index 5b1ccd5..1c6a956 100644 --- a/bitcoind-tests/tests/infrastructure.rs +++ b/bitcoind-tests/tests/infrastructure.rs @@ -1,40 +1,36 @@ //! Test the bitcoind infrastructure. -mod client; - -use client::Client; -use psbt_v2::bitcoin::Amount; - -#[track_caller] -fn client() -> Client { - let client = Client::new().expect("failed to create client"); - // Sanity check. - assert_eq!(0, client.get_blockchain_info().unwrap().blocks); - - client.fund().expect("failed to fund client"); - client -} +// Depend directly on `bitcoin` (and `bitcoind_tests`) because we are explicitly +// testing the `bitcoind_tests` crate. +use bitcoin::Amount; +use bitcoind_tests::client::Client; #[test] fn bitcoind_get_core_wallet_controlled_address() { - let client = client(); + let client = Client::new().expect("failed to create client"); let address = client.core_wallet_controlled_address().expect("get_new_address failed"); println!("address: {}", address); } -#[test] -fn bitcoind_fund_core_controlled_wallet() { - let client = client(); - assert!(client.fund().is_ok()) -} - #[test] fn bitcoind_send() { - let client = client(); + let mut client = Client::new().expect("failed to create client"); + assert_eq!(client.tracked_balance(), Amount::ZERO); + + // Mine a block to release initial funds (coinbase reward). + client.mine_a_block().expect("initial mine_a_block failed"); + // Sanity check, we should have 50 BTC. + assert_eq!(client.tracked_balance(), Amount::from_btc(50.0).unwrap()); + client.assert_balance_is_as_expected().expect("incorrect balance"); let address = client.core_wallet_controlled_address().expect("get_new_address failed"); let amount = Amount::ONE_BTC; let txid = client.send(amount, &address).expect("send failed"); + client.balance.send_to_self(); + + client.mine_a_block().expect("mine_a_block failed"); + + client.assert_balance_is_as_expected().expect("incorrect balance"); println!("txid: {}", txid); } diff --git a/examples/v2-separate-creator-constructor.rs b/examples/v2-separate-creator-constructor.rs index 55ef4b1..4baf8d4 100644 --- a/examples/v2-separate-creator-constructor.rs +++ b/examples/v2-separate-creator-constructor.rs @@ -17,7 +17,7 @@ fn main() -> anyhow::Result<()> { let psbt = Psbt::deserialize(&ser)?; let in_0 = dummy_out_point(); let ser = Constructor::::new(psbt)? - .input(InputBuilder::new(in_0).build()) + .input(InputBuilder::new(&in_0).build()) .psbt() .expect("valid lock time combination") .serialize(); @@ -26,7 +26,7 @@ fn main() -> anyhow::Result<()> { let psbt = Psbt::deserialize(&ser)?; let in_1 = dummy_out_point(); let ser = Constructor::::new(psbt)? - .input(InputBuilder::new(in_1).build()) + .input(InputBuilder::new(&in_1).build()) .no_more_inputs() .psbt() .expect("valid lock time combination") diff --git a/examples/v2.rs b/examples/v2.rs index 06f7b84..7b7a8f8 100644 --- a/examples/v2.rs +++ b/examples/v2.rs @@ -57,12 +57,12 @@ fn main() -> anyhow::Result<()> { let constructor = Constructor::::default(); - let input_a = InputBuilder::new(previous_output_a) + let input_a = InputBuilder::new(&previous_output_a) .minimum_required_height_based_lock_time(min_required_height) .build(); // If no lock time is required we can just create the `Input` directly. - let input_b = InputBuilder::new(previous_output_b) + let input_b = InputBuilder::new(&previous_output_b) // .segwit_fund(txout); TODO: Add funding utxo. .build(); diff --git a/src/v2/map/input.rs b/src/v2/map/input.rs index 5a42d96..1bb4a3a 100644 --- a/src/v2/map/input.rs +++ b/src/v2/map/input.rs @@ -124,7 +124,7 @@ pub struct Input { impl Input { /// Creates a new `Input` that spends the `previous_output`. - pub fn new(previous_output: OutPoint) -> Self { + pub fn new(previous_output: &OutPoint) -> Self { Input { previous_txid: previous_output.txid, spent_output_index: previous_output.vout, @@ -370,7 +370,7 @@ impl Input { pub(in crate::v2) fn decode(r: &mut R) -> Result { // These are placeholder values that never exist in a encode `Input`. let invalid = OutPoint { txid: Txid::all_zeros(), vout: u32::MAX }; - let mut rv = Self::new(invalid); + let mut rv = Self::new(&invalid); loop { match raw::Pair::decode(r) { @@ -749,7 +749,7 @@ pub struct InputBuilder(Input); impl InputBuilder { /// Creates a new builder that can be used to build an [`Input`] that spends `previous_output`. - pub fn new(previous_output: OutPoint) -> Self { Self(Input::new(previous_output)) } + pub fn new(previous_output: &OutPoint) -> Self { Self(Input::new(previous_output)) } /// Sets the [`Input::min_time`] field. pub fn minimum_required_time_based_lock_time(mut self, lock: absolute::Time) -> Self { @@ -1010,7 +1010,7 @@ mod test { #[test] #[cfg(feature = "std")] fn serialize_roundtrip() { - let input = Input::new(out_point()); + let input = Input::new(&out_point()); let ser = input.serialize_map(); let mut d = std::io::Cursor::new(ser); diff --git a/src/v2/miniscript/finalize.rs b/src/v2/miniscript/finalize.rs index 7550aeb..53197b3 100644 --- a/src/v2/miniscript/finalize.rs +++ b/src/v2/miniscript/finalize.rs @@ -81,8 +81,6 @@ impl Finalizer { } /// Returns the final script_sig and final witness for this input. - /// - /// Note if this is a legacy input the returned `Witness` will be empty. // TODO: Think harder about this. // // Input finalizer should only set script sig and witness iff one is required @@ -129,6 +127,8 @@ impl Finalizer { }; let witness = Witness::from_slice(&witness); + println!("{:#?}", script_sig); + println!("{:#?}", witness); Ok((script_sig, witness)) } diff --git a/src/v2/mod.rs b/src/v2/mod.rs index 01cdbaf..ca99c4e 100644 --- a/src/v2/mod.rs +++ b/src/v2/mod.rs @@ -85,7 +85,7 @@ pub fn combine(this: Psbt, that: Psbt) -> Result { this.comb /// - You need to set the fallback lock time. /// - You need to set the sighash single flag. /// -/// If not use the [`Constructor`] to carry out both roles e.g., `Constructor::default()`. +/// If not use the [`Constructor`] to carry out both roles e.g., `Constructor::::default()`. /// /// See `examples/v2-separate-creator-constructor.rs`. #[derive(Debug, Clone, PartialEq, Eq, Hash)]