Skip to content

Commit

Permalink
bitcoind-tests: Add basic test
Browse files Browse the repository at this point in the history
Add infrastructure for testing and add a `basic` test that uses the PSBT
v2 to create a transaction ready to hand off to a signer.
  • Loading branch information
tcharding committed Feb 7, 2024
1 parent d462630 commit 3e465fb
Show file tree
Hide file tree
Showing 11 changed files with 336 additions and 107 deletions.
11 changes: 7 additions & 4 deletions bitcoind-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ authors = ["Tobin C. Harding <[email protected]>"]
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]

185 changes: 185 additions & 0 deletions bitcoind-tests/src/client.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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<GetBlockchainInfoResult> {
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<Address> {
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<Amount> {
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<Txid> {
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<Transaction> {
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<Txid> {
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)
}
7 changes: 7 additions & 0 deletions bitcoind-tests/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Tools to help with testing against Bitcoin Core using [`bitcoind`] and [`bitcoincore-rpc`].
//!
//! [`bitcoind`]: <https://github.com/rust-bitcoin/bitcoind>
//! [`bitcoincore-rpc`]: <https://github.com/rust-bitcoin/rust-bitcoincore-rpc/>

/// A wrapper around the `bitcoind` client.
pub mod client;
108 changes: 108 additions & 0 deletions bitcoind-tests/tests/basic.rs
Original file line number Diff line number Diff line change
@@ -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::<Modifiable>::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")
}
}
70 changes: 0 additions & 70 deletions bitcoind-tests/tests/client.rs

This file was deleted.

Loading

0 comments on commit 3e465fb

Please sign in to comment.