Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bitcoind-tests: Add basic test #22

Merged
merged 1 commit into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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