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 cae6c2b
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 30 deletions.
4 changes: 3 additions & 1 deletion bitcoind-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ edition = "2021"

[dev-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"] }
107 changes: 107 additions & 0 deletions bitcoind-tests/tests/basic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//! A basic PSBT test, a single entity using PSBTv2 to create and sign a transaction.

mod client;

use core::str::FromStr;

use bitcoin::{Address, Amount, Network, OutPoint, PublicKey, Script, Transaction, TxOut};
use client::Client;
use psbt::v2::{Constructor, InputBuilder, Modifiable, OutputBuilder};
use psbt_v2 as psbt;

const NETWORK: Network = Network::Regtest;
const TWO_BTC: Amount = Amount::from_sat(200_000_000);
const ONE_BTC: Amount = Amount::ONE_BTC;
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 of a key Alice controls.
pub fn address(&self) -> Address {
Address::p2wpkh(&self.public_key, NETWORK).expect("uncompressed key")
}
}
113 changes: 108 additions & 5 deletions bitcoind-tests/tests/client.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,54 @@
#![allow(dead_code)]

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;
use psbt_v2::bitcoin::{Address, Amount, Network, Txid};

const NETWORK: Network = Network::Regtest;

/// A custom bitcoind client.
pub struct Client {
bitcoind: BitcoinD,
pub balance: BalanceTracker,
}

impl Client {
/// Creates a new [`Client`].
pub fn new() -> anyhow::Result<Self> {
let bitcoind = BitcoinD::from_downloaded()?;
let client = Client { bitcoind };
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;
Expand All @@ -34,17 +64,26 @@ impl Client {
Ok(address)
}

/// Funds the bitcoind wallet with a spendable 50 BTC utxo.
pub fn fund(&self) -> anyhow::Result<()> {
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(101, &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;

Expand All @@ -65,6 +104,70 @@ impl Client {
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)
}
}

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_bitcoin() }

/// 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_bitcoin() - 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 fifty_bitcoin() -> Amount { Amount::from_btc(50.0).expect("valid amount") }

fn floor(x: Amount) -> Amount {
let one = 100_000_000;
let sats = x.to_sat();
Amount::from_sat(sats / one * one)
}
32 changes: 14 additions & 18 deletions bitcoind-tests/tests/infrastructure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,32 @@ 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
}

#[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);
}
6 changes: 3 additions & 3 deletions src/v2/map/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -370,7 +370,7 @@ impl Input {
pub(in crate::v2) fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, DecodeError> {
// 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) {
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/v2/miniscript/finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -129,6 +127,8 @@ impl Finalizer {
};

let witness = Witness::from_slice(&witness);
println!("{:#?}", script_sig);
println!("{:#?}", witness);
Ok((script_sig, witness))
}

Expand Down
2 changes: 1 addition & 1 deletion src/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ pub fn combine(this: Psbt, that: Psbt) -> Result<Psbt, CombineError> { 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<Modifiable>::default()`.
/// If not use the [`Constructor`] to carry out both roles e.g., `Constructor::<Modifiable>::default()`.
///
/// See `examples/v2-separate-creator-constructor.rs`.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
Expand Down

0 comments on commit cae6c2b

Please sign in to comment.