From 465ef6e67470776cb1e5c2951aeb8f750940253e Mon Sep 17 00:00:00 2001 From: LLFourn Date: Sun, 16 May 2021 15:07:55 +1000 Subject: [PATCH 01/10] Roll blockchain tests proc macro into normal macro This means one less crate in the repo. Had to do a Default on TestClient to satisfy clippy. --- Cargo.toml | 6 +- src/blockchain/electrum.rs | 15 +- src/lib.rs | 2 - testutils-macros/Cargo.toml | 25 -- testutils-macros/src/lib.rs | 553 ------------------------------ testutils/src/blockchain_tests.rs | 491 ++++++++++++++++++++++++++ testutils/src/lib.rs | 21 +- 7 files changed, 517 insertions(+), 596 deletions(-) delete mode 100644 testutils-macros/Cargo.toml delete mode 100644 testutils-macros/src/lib.rs create mode 100644 testutils/src/blockchain_tests.rs diff --git a/Cargo.toml b/Cargo.toml index 687e016c..52176e5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,13 +54,11 @@ all-keys = ["keys-bip39"] keys-bip39 = ["tiny-bip39"] # Debug/Test features -debug-proc-macros = ["bdk-macros/debug", "bdk-testutils-macros/debug"] test-electrum = ["electrum"] test-md-docs = ["electrum"] [dev-dependencies] -bdk-testutils = "0.4" -bdk-testutils-macros = "0.6" +bdk-testutils = { path = "./testutils" } serial_test = "0.4" lazy_static = "1.4" env_logger = "0.7" @@ -79,7 +77,7 @@ path = "examples/compiler.rs" required-features = ["compiler"] [workspace] -members = ["macros", "testutils", "testutils-macros"] +members = ["macros", "testutils"] # Generate docs with nightly to add the "features required" badge # https://stackoverflow.com/questions/61417452/how-to-get-a-feature-requirement-tag-in-the-documentation-generated-by-cargo-do diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index 926155a3..2e103307 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -45,13 +45,6 @@ use crate::FeeRate; /// See the [`blockchain::electrum`](crate::blockchain::electrum) module for a usage example. pub struct ElectrumBlockchain(Client); -#[cfg(test)] -#[cfg(feature = "test-electrum")] -#[bdk_blockchain_tests(crate)] -fn local_electrs() -> ElectrumBlockchain { - ElectrumBlockchain::from(Client::new(&testutils::get_electrum_url()).unwrap()) -} - impl std::convert::From for ElectrumBlockchain { fn from(client: Client) -> Self { ElectrumBlockchain(client) @@ -175,3 +168,11 @@ impl ConfigurableBlockchain for ElectrumBlockchain { )?)) } } + +#[cfg(all(feature = "test-electrum", test))] +testutils::bdk_blockchain_tests! { + bdk => crate, + fn test_instance() -> ElectrumBlockchain { + ElectrumBlockchain::from(Client::new(&testutils::get_electrum_url()).unwrap()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 0e7f8287..a4670b5c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -234,8 +234,6 @@ pub extern crate sled; extern crate testutils; #[allow(unused_imports)] #[cfg(test)] -#[macro_use] -extern crate testutils_macros; #[allow(unused_imports)] #[cfg(test)] #[macro_use] diff --git a/testutils-macros/Cargo.toml b/testutils-macros/Cargo.toml deleted file mode 100644 index f78b7f9d..00000000 --- a/testutils-macros/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "bdk-testutils-macros" -version = "0.6.0" -authors = ["Alekos Filini "] -edition = "2018" -homepage = "https://bitcoindevkit.org" -repository = "https://github.com/bitcoindevkit/bdk" -documentation = "https://docs.rs/bdk-testutils-macros" -description = "Supporting testing macros for `bdk`" -keywords = ["bdk"] -license = "MIT OR Apache-2.0" - -[lib] -proc-macro = true -name = "testutils_macros" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -syn = { version = "1.0", features = ["parsing", "full"] } -proc-macro2 = "1.0" -quote = "1.0" - -[features] -debug = ["syn/extra-traits"] diff --git a/testutils-macros/src/lib.rs b/testutils-macros/src/lib.rs deleted file mode 100644 index db01e1ed..00000000 --- a/testutils-macros/src/lib.rs +++ /dev/null @@ -1,553 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -#[macro_use] -extern crate quote; - -use proc_macro::TokenStream; - -use syn::spanned::Spanned; -use syn::{parse, parse2, Ident, ReturnType}; - -#[proc_macro_attribute] -pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream { - let root_ident = if !attr.is_empty() { - match parse::(attr) { - Ok(parsed) => parsed, - Err(e) => { - let error_string = e.to_string(); - return (quote! { - compile_error!("Invalid crate path: {:?}", #error_string) - }) - .into(); - } - } - } else { - parse2::(quote! { bdk }).unwrap() - }; - - match parse::(item) { - Err(_) => (quote! { - compile_error!("#[bdk_blockchain_tests] can only be used on `fn`s") - }) - .into(), - Ok(parsed) => { - let parsed_sig_ident = parsed.sig.ident.clone(); - let mod_name = Ident::new( - &format!("generated_tests_{}", parsed_sig_ident.to_string()), - parsed.span(), - ); - - let return_type = match parsed.sig.output { - ReturnType::Type(_, ref t) => t.clone(), - ReturnType::Default => { - return (quote! { - compile_error!("The tagged function must return a type that impl `Blockchain`") - }).into(); - } - }; - - let output = quote! { - - #parsed - - mod #mod_name { - use bitcoin::Network; - - use miniscript::Descriptor; - - use testutils::{TestClient, serial}; - - use #root_ident::blockchain::{Blockchain, noop_progress}; - use #root_ident::descriptor::ExtendedDescriptor; - use #root_ident::database::MemoryDatabase; - use #root_ident::types::KeychainKind; - use #root_ident::{Wallet, TxBuilder, FeeRate}; - use #root_ident::wallet::AddressIndex::New; - - use super::*; - - fn get_blockchain() -> #return_type { - #parsed_sig_ident() - } - - fn get_wallet_from_descriptors(descriptors: &(String, Option)) -> Wallet<#return_type, MemoryDatabase> { - Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new(), get_blockchain()).unwrap() - } - - fn init_single_sig() -> (Wallet<#return_type, MemoryDatabase>, (String, Option), TestClient) { - let descriptors = testutils! { - @descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) ) - }; - - let test_client = TestClient::new(); - let wallet = get_wallet_from_descriptors(&descriptors); - - (wallet, descriptors, test_client) - } - - #[test] - #[serial] - fn test_sync_simple() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - - let tx = testutils! { - @tx ( (@external descriptors, 0) => 50_000 ) - }; - println!("{:?}", tx); - let txid = test_client.receive(tx); - - wallet.sync(noop_progress(), None).unwrap(); - - assert_eq!(wallet.get_balance().unwrap(), 50_000); - assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External); - - let list_tx_item = &wallet.list_transactions(false).unwrap()[0]; - assert_eq!(list_tx_item.txid, txid); - assert_eq!(list_tx_item.received, 50_000); - assert_eq!(list_tx_item.sent, 0); - assert_eq!(list_tx_item.height, None); - } - - #[test] - #[serial] - fn test_sync_stop_gap_20() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - - test_client.receive(testutils! { - @tx ( (@external descriptors, 5) => 50_000 ) - }); - test_client.receive(testutils! { - @tx ( (@external descriptors, 25) => 50_000 ) - }); - - wallet.sync(noop_progress(), None).unwrap(); - - assert_eq!(wallet.get_balance().unwrap(), 100_000); - assert_eq!(wallet.list_transactions(false).unwrap().len(), 2); - } - - #[test] - #[serial] - fn test_sync_before_and_after_receive() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 0); - - test_client.receive(testutils! { - @tx ( (@external descriptors, 0) => 50_000 ) - }); - - wallet.sync(noop_progress(), None).unwrap(); - - assert_eq!(wallet.get_balance().unwrap(), 50_000); - assert_eq!(wallet.list_transactions(false).unwrap().len(), 1); - } - - #[test] - #[serial] - fn test_sync_multiple_outputs_same_tx() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - - let txid = test_client.receive(testutils! { - @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000, (@external descriptors, 5) => 30_000 ) - }); - - wallet.sync(noop_progress(), None).unwrap(); - - assert_eq!(wallet.get_balance().unwrap(), 105_000); - assert_eq!(wallet.list_transactions(false).unwrap().len(), 1); - assert_eq!(wallet.list_unspent().unwrap().len(), 3); - - let list_tx_item = &wallet.list_transactions(false).unwrap()[0]; - assert_eq!(list_tx_item.txid, txid); - assert_eq!(list_tx_item.received, 105_000); - assert_eq!(list_tx_item.sent, 0); - assert_eq!(list_tx_item.height, None); - } - - #[test] - #[serial] - fn test_sync_receive_multi() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - - test_client.receive(testutils! { - @tx ( (@external descriptors, 0) => 50_000 ) - }); - test_client.receive(testutils! { - @tx ( (@external descriptors, 5) => 25_000 ) - }); - - wallet.sync(noop_progress(), None).unwrap(); - - assert_eq!(wallet.get_balance().unwrap(), 75_000); - assert_eq!(wallet.list_transactions(false).unwrap().len(), 2); - assert_eq!(wallet.list_unspent().unwrap().len(), 2); - } - - #[test] - #[serial] - fn test_sync_address_reuse() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - - test_client.receive(testutils! { - @tx ( (@external descriptors, 0) => 50_000 ) - }); - - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000); - - test_client.receive(testutils! { - @tx ( (@external descriptors, 0) => 25_000 ) - }); - - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 75_000); - } - - #[test] - #[serial] - fn test_sync_receive_rbf_replaced() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - - let txid = test_client.receive(testutils! { - @tx ( (@external descriptors, 0) => 50_000 ) ( @replaceable true ) - }); - - wallet.sync(noop_progress(), None).unwrap(); - - assert_eq!(wallet.get_balance().unwrap(), 50_000); - assert_eq!(wallet.list_transactions(false).unwrap().len(), 1); - assert_eq!(wallet.list_unspent().unwrap().len(), 1); - - let list_tx_item = &wallet.list_transactions(false).unwrap()[0]; - assert_eq!(list_tx_item.txid, txid); - assert_eq!(list_tx_item.received, 50_000); - assert_eq!(list_tx_item.sent, 0); - assert_eq!(list_tx_item.height, None); - - let new_txid = test_client.bump_fee(&txid); - - wallet.sync(noop_progress(), None).unwrap(); - - assert_eq!(wallet.get_balance().unwrap(), 50_000); - assert_eq!(wallet.list_transactions(false).unwrap().len(), 1); - assert_eq!(wallet.list_unspent().unwrap().len(), 1); - - let list_tx_item = &wallet.list_transactions(false).unwrap()[0]; - assert_eq!(list_tx_item.txid, new_txid); - assert_eq!(list_tx_item.received, 50_000); - assert_eq!(list_tx_item.sent, 0); - assert_eq!(list_tx_item.height, None); - } - - #[test] - #[serial] - fn test_sync_reorg_block() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - - let txid = test_client.receive(testutils! { - @tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 1 ) ( @replaceable true ) - }); - - wallet.sync(noop_progress(), None).unwrap(); - - assert_eq!(wallet.get_balance().unwrap(), 50_000); - assert_eq!(wallet.list_transactions(false).unwrap().len(), 1); - assert_eq!(wallet.list_unspent().unwrap().len(), 1); - - let list_tx_item = &wallet.list_transactions(false).unwrap()[0]; - assert_eq!(list_tx_item.txid, txid); - assert!(list_tx_item.height.is_some()); - - // Invalidate 1 block - test_client.invalidate(1); - - wallet.sync(noop_progress(), None).unwrap(); - - assert_eq!(wallet.get_balance().unwrap(), 50_000); - - let list_tx_item = &wallet.list_transactions(false).unwrap()[0]; - assert_eq!(list_tx_item.txid, txid); - assert_eq!(list_tx_item.height, None); - } - - #[test] - #[serial] - fn test_sync_after_send() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - println!("{}", descriptors.0); - let node_addr = test_client.get_node_address(None); - - test_client.receive(testutils! { - @tx ( (@external descriptors, 0) => 50_000 ) - }); - - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000); - - let mut builder = wallet.build_tx(); - builder.add_recipient(node_addr.script_pubkey(), 25_000); - let (mut psbt, details) = builder.finish().unwrap(); - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized, "Cannot finalize transaction"); - let tx = psbt.extract_tx(); - println!("{}", bitcoin::consensus::encode::serialize_hex(&tx)); - wallet.broadcast(tx).unwrap(); - - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), details.received); - - assert_eq!(wallet.list_transactions(false).unwrap().len(), 2); - assert_eq!(wallet.list_unspent().unwrap().len(), 1); - } - - #[test] - #[serial] - fn test_sync_outgoing_from_scratch() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - let node_addr = test_client.get_node_address(None); - - let received_txid = test_client.receive(testutils! { - @tx ( (@external descriptors, 0) => 50_000 ) - }); - - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000); - - let mut builder = wallet.build_tx(); - builder.add_recipient(node_addr.script_pubkey(), 25_000); - let (mut psbt, details) = builder.finish().unwrap(); - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized, "Cannot finalize transaction"); - let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap(); - - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), details.received); - - // empty wallet - let wallet = get_wallet_from_descriptors(&descriptors); - wallet.sync(noop_progress(), None).unwrap(); - - let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::>(); - - let received = tx_map.get(&received_txid).unwrap(); - assert_eq!(received.received, 50_000); - assert_eq!(received.sent, 0); - - let sent = tx_map.get(&sent_txid).unwrap(); - assert_eq!(sent.received, details.received); - assert_eq!(sent.sent, details.sent); - assert_eq!(sent.fees, details.fees); - } - - #[test] - #[serial] - fn test_sync_long_change_chain() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - let node_addr = test_client.get_node_address(None); - - test_client.receive(testutils! { - @tx ( (@external descriptors, 0) => 50_000 ) - }); - - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000); - - let mut total_sent = 0; - for _ in 0..5 { - let mut builder = wallet.build_tx(); - builder.add_recipient(node_addr.script_pubkey(), 5_000); - let (mut psbt, details) = builder.finish().unwrap(); - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized, "Cannot finalize transaction"); - wallet.broadcast(psbt.extract_tx()).unwrap(); - - wallet.sync(noop_progress(), None).unwrap(); - - total_sent += 5_000 + details.fees; - } - - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent); - - // empty wallet - let wallet = get_wallet_from_descriptors(&descriptors); - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent); - } - - #[test] - #[serial] - fn test_sync_bump_fee() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - let node_addr = test_client.get_node_address(None); - - test_client.receive(testutils! { - @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1) - }); - - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000); - - let mut builder = wallet.build_tx(); - builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf(); - let (mut psbt, details) = builder.finish().unwrap(); - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized, "Cannot finalize transaction"); - wallet.broadcast(psbt.extract_tx()).unwrap(); - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fees - 5_000); - assert_eq!(wallet.get_balance().unwrap(), details.received); - - let mut builder = wallet.build_fee_bump(details.txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb(2.1)); - let (mut new_psbt, new_details) = builder.finish().unwrap(); - let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap(); - assert!(finalized, "Cannot finalize transaction"); - wallet.broadcast(new_psbt.extract_tx()).unwrap(); - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fees - 5_000); - assert_eq!(wallet.get_balance().unwrap(), new_details.received); - - assert!(new_details.fees > details.fees); - } - - #[test] - #[serial] - fn test_sync_bump_fee_remove_change() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - let node_addr = test_client.get_node_address(None); - - test_client.receive(testutils! { - @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1) - }); - - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000); - - let mut builder = wallet.build_tx(); - builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf(); - let (mut psbt, details) = builder.finish().unwrap(); - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized, "Cannot finalize transaction"); - wallet.broadcast(psbt.extract_tx()).unwrap(); - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fees); - assert_eq!(wallet.get_balance().unwrap(), details.received); - - let mut builder = wallet.build_fee_bump(details.txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb(5.0)); - let (mut new_psbt, new_details) = builder.finish().unwrap(); - let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap(); - assert!(finalized, "Cannot finalize transaction"); - wallet.broadcast(new_psbt.extract_tx()).unwrap(); - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 0); - assert_eq!(new_details.received, 0); - - assert!(new_details.fees > details.fees); - } - - #[test] - #[serial] - fn test_sync_bump_fee_add_input() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - let node_addr = test_client.get_node_address(None); - - test_client.receive(testutils! { - @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1) - }); - - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 75_000); - - let mut builder = wallet.build_tx(); - builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf(); - let (mut psbt, details) = builder.finish().unwrap(); - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized, "Cannot finalize transaction"); - wallet.broadcast(psbt.extract_tx()).unwrap(); - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees); - assert_eq!(details.received, 1_000 - details.fees); - - let mut builder = wallet.build_fee_bump(details.txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb(10.0)); - let (mut new_psbt, new_details) = builder.finish().unwrap(); - let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap(); - assert!(finalized, "Cannot finalize transaction"); - wallet.broadcast(new_psbt.extract_tx()).unwrap(); - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(new_details.sent, 75_000); - assert_eq!(wallet.get_balance().unwrap(), new_details.received); - } - - #[test] - #[serial] - fn test_sync_bump_fee_add_input_no_change() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - let node_addr = test_client.get_node_address(None); - - test_client.receive(testutils! { - @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1) - }); - - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 75_000); - - let mut builder = wallet.build_tx(); - builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf(); - let (mut psbt, details) = builder.finish().unwrap(); - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized, "Cannot finalize transaction"); - wallet.broadcast(psbt.extract_tx()).unwrap(); - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees); - assert_eq!(details.received, 1_000 - details.fees); - - let mut builder = wallet.build_fee_bump(details.txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb(123.0)); - let (mut new_psbt, new_details) = builder.finish().unwrap(); - println!("{:#?}", new_details); - - let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap(); - assert!(finalized, "Cannot finalize transaction"); - wallet.broadcast(new_psbt.extract_tx()).unwrap(); - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(new_details.sent, 75_000); - assert_eq!(wallet.get_balance().unwrap(), 0); - assert_eq!(new_details.received, 0); - } - - #[test] - #[serial] - fn test_sync_receive_coinbase() { - let (wallet, descriptors, mut test_client) = init_single_sig(); - let wallet_addr = wallet.get_address(New).unwrap(); - - wallet.sync(noop_progress(), None).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 0); - - test_client.generate(1, Some(wallet_addr)); - - wallet.sync(noop_progress(), None).unwrap(); - assert!(wallet.get_balance().unwrap() > 0); - } - } - - }; - - output.into() - } - } -} diff --git a/testutils/src/blockchain_tests.rs b/testutils/src/blockchain_tests.rs new file mode 100644 index 00000000..95ebcc4b --- /dev/null +++ b/testutils/src/blockchain_tests.rs @@ -0,0 +1,491 @@ +/// This macro runs blockchain tests against a `Blockchain` implementation. It requires access to a +/// Bitcoin core wallet via RPC. At the moment you have to dig into the code yourself and look at +/// the setup required to run the tests yourself. +#[macro_export] +macro_rules! bdk_blockchain_tests { + (bdk => $bdk:ident, + fn test_instance() -> $blockchain:ty $block:block) => { + mod bdk_blockchain_tests { + use $bdk::bitcoin::Network; + use $bdk::miniscript::Descriptor; + use $crate::{TestClient, serial}; + use $bdk::blockchain::{Blockchain, noop_progress}; + use $bdk::descriptor::ExtendedDescriptor; + use $bdk::database::MemoryDatabase; + use $bdk::types::KeychainKind; + use $bdk::{Wallet, TxBuilder, FeeRate}; + use $bdk::wallet::AddressIndex::New; + + use super::*; + + fn get_blockchain() -> $blockchain { + $block + } + + fn get_wallet_from_descriptors(descriptors: &(String, Option)) -> Wallet<$blockchain, MemoryDatabase> { + Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new(), get_blockchain()).unwrap() + } + + fn init_single_sig() -> (Wallet<$blockchain, MemoryDatabase>, (String, Option), TestClient) { + let descriptors = testutils! { + @descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) ) + }; + + let test_client = TestClient::default(); + let wallet = get_wallet_from_descriptors(&descriptors); + + (wallet, descriptors, test_client) + } + + #[test] + #[serial] + fn test_sync_simple() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + + let tx = testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) + }; + println!("{:?}", tx); + let txid = test_client.receive(tx); + + wallet.sync(noop_progress(), None).unwrap(); + + assert_eq!(wallet.get_balance().unwrap(), 50_000); + assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External); + + let list_tx_item = &wallet.list_transactions(false).unwrap()[0]; + assert_eq!(list_tx_item.txid, txid); + assert_eq!(list_tx_item.received, 50_000); + assert_eq!(list_tx_item.sent, 0); + assert_eq!(list_tx_item.height, None); + } + + #[test] + #[serial] + fn test_sync_stop_gap_20() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 5) => 50_000 ) + }); + test_client.receive(testutils! { + @tx ( (@external descriptors, 25) => 50_000 ) + }); + + wallet.sync(noop_progress(), None).unwrap(); + + assert_eq!(wallet.get_balance().unwrap(), 100_000); + assert_eq!(wallet.list_transactions(false).unwrap().len(), 2); + } + + #[test] + #[serial] + fn test_sync_before_and_after_receive() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 0); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) + }); + + wallet.sync(noop_progress(), None).unwrap(); + + assert_eq!(wallet.get_balance().unwrap(), 50_000); + assert_eq!(wallet.list_transactions(false).unwrap().len(), 1); + } + + #[test] + #[serial] + fn test_sync_multiple_outputs_same_tx() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + + let txid = test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000, (@external descriptors, 5) => 30_000 ) + }); + + wallet.sync(noop_progress(), None).unwrap(); + + assert_eq!(wallet.get_balance().unwrap(), 105_000); + assert_eq!(wallet.list_transactions(false).unwrap().len(), 1); + assert_eq!(wallet.list_unspent().unwrap().len(), 3); + + let list_tx_item = &wallet.list_transactions(false).unwrap()[0]; + assert_eq!(list_tx_item.txid, txid); + assert_eq!(list_tx_item.received, 105_000); + assert_eq!(list_tx_item.sent, 0); + assert_eq!(list_tx_item.height, None); + } + + #[test] + #[serial] + fn test_sync_receive_multi() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) + }); + test_client.receive(testutils! { + @tx ( (@external descriptors, 5) => 25_000 ) + }); + + wallet.sync(noop_progress(), None).unwrap(); + + assert_eq!(wallet.get_balance().unwrap(), 75_000); + assert_eq!(wallet.list_transactions(false).unwrap().len(), 2); + assert_eq!(wallet.list_unspent().unwrap().len(), 2); + } + + #[test] + #[serial] + fn test_sync_address_reuse() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) + }); + + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 25_000 ) + }); + + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 75_000); + } + + #[test] + #[serial] + fn test_sync_receive_rbf_replaced() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + + let txid = test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) ( @replaceable true ) + }); + + wallet.sync(noop_progress(), None).unwrap(); + + assert_eq!(wallet.get_balance().unwrap(), 50_000); + assert_eq!(wallet.list_transactions(false).unwrap().len(), 1); + assert_eq!(wallet.list_unspent().unwrap().len(), 1); + + let list_tx_item = &wallet.list_transactions(false).unwrap()[0]; + assert_eq!(list_tx_item.txid, txid); + assert_eq!(list_tx_item.received, 50_000); + assert_eq!(list_tx_item.sent, 0); + assert_eq!(list_tx_item.height, None); + + let new_txid = test_client.bump_fee(&txid); + + wallet.sync(noop_progress(), None).unwrap(); + + assert_eq!(wallet.get_balance().unwrap(), 50_000); + assert_eq!(wallet.list_transactions(false).unwrap().len(), 1); + assert_eq!(wallet.list_unspent().unwrap().len(), 1); + + let list_tx_item = &wallet.list_transactions(false).unwrap()[0]; + assert_eq!(list_tx_item.txid, new_txid); + assert_eq!(list_tx_item.received, 50_000); + assert_eq!(list_tx_item.sent, 0); + assert_eq!(list_tx_item.height, None); + } + + #[test] + #[serial] + fn test_sync_reorg_block() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + + let txid = test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 1 ) ( @replaceable true ) + }); + + wallet.sync(noop_progress(), None).unwrap(); + + assert_eq!(wallet.get_balance().unwrap(), 50_000); + assert_eq!(wallet.list_transactions(false).unwrap().len(), 1); + assert_eq!(wallet.list_unspent().unwrap().len(), 1); + + let list_tx_item = &wallet.list_transactions(false).unwrap()[0]; + assert_eq!(list_tx_item.txid, txid); + assert!(list_tx_item.height.is_some()); + + // Invalidate 1 block + test_client.invalidate(1); + + wallet.sync(noop_progress(), None).unwrap(); + + assert_eq!(wallet.get_balance().unwrap(), 50_000); + + let list_tx_item = &wallet.list_transactions(false).unwrap()[0]; + assert_eq!(list_tx_item.txid, txid); + assert_eq!(list_tx_item.height, None); + } + + #[test] + #[serial] + fn test_sync_after_send() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + println!("{}", descriptors.0); + let node_addr = test_client.get_node_address(None); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) + }); + + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000); + + let mut builder = wallet.build_tx(); + builder.add_recipient(node_addr.script_pubkey(), 25_000); + let (mut psbt, details) = builder.finish().unwrap(); + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + let tx = psbt.extract_tx(); + println!("{}", bitcoin::consensus::encode::serialize_hex(&tx)); + wallet.broadcast(tx).unwrap(); + + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), details.received); + + assert_eq!(wallet.list_transactions(false).unwrap().len(), 2); + assert_eq!(wallet.list_unspent().unwrap().len(), 1); + } + + #[test] + #[serial] + fn test_sync_outgoing_from_scratch() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + let node_addr = test_client.get_node_address(None); + + let received_txid = test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) + }); + + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000); + + let mut builder = wallet.build_tx(); + builder.add_recipient(node_addr.script_pubkey(), 25_000); + let (mut psbt, details) = builder.finish().unwrap(); + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap(); + + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), details.received); + + // empty wallet + let wallet = get_wallet_from_descriptors(&descriptors); + wallet.sync(noop_progress(), None).unwrap(); + + let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::>(); + + let received = tx_map.get(&received_txid).unwrap(); + assert_eq!(received.received, 50_000); + assert_eq!(received.sent, 0); + + let sent = tx_map.get(&sent_txid).unwrap(); + assert_eq!(sent.received, details.received); + assert_eq!(sent.sent, details.sent); + assert_eq!(sent.fees, details.fees); + } + + #[test] + #[serial] + fn test_sync_long_change_chain() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + let node_addr = test_client.get_node_address(None); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) + }); + + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000); + + let mut total_sent = 0; + for _ in 0..5 { + let mut builder = wallet.build_tx(); + builder.add_recipient(node_addr.script_pubkey(), 5_000); + let (mut psbt, details) = builder.finish().unwrap(); + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(psbt.extract_tx()).unwrap(); + + wallet.sync(noop_progress(), None).unwrap(); + + total_sent += 5_000 + details.fees; + } + + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent); + + // empty wallet + let wallet = get_wallet_from_descriptors(&descriptors); + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent); + } + + #[test] + #[serial] + fn test_sync_bump_fee() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + let node_addr = test_client.get_node_address(None); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1) + }); + + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000); + + let mut builder = wallet.build_tx(); + builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf(); + let (mut psbt, details) = builder.finish().unwrap(); + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(psbt.extract_tx()).unwrap(); + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fees - 5_000); + assert_eq!(wallet.get_balance().unwrap(), details.received); + + let mut builder = wallet.build_fee_bump(details.txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb(2.1)); + let (mut new_psbt, new_details) = builder.finish().unwrap(); + let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(new_psbt.extract_tx()).unwrap(); + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fees - 5_000); + assert_eq!(wallet.get_balance().unwrap(), new_details.received); + + assert!(new_details.fees > details.fees); + } + + #[test] + #[serial] + fn test_sync_bump_fee_remove_change() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + let node_addr = test_client.get_node_address(None); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1) + }); + + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000); + + let mut builder = wallet.build_tx(); + builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf(); + let (mut psbt, details) = builder.finish().unwrap(); + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(psbt.extract_tx()).unwrap(); + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fees); + assert_eq!(wallet.get_balance().unwrap(), details.received); + + let mut builder = wallet.build_fee_bump(details.txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb(5.0)); + let (mut new_psbt, new_details) = builder.finish().unwrap(); + let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(new_psbt.extract_tx()).unwrap(); + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 0); + assert_eq!(new_details.received, 0); + + assert!(new_details.fees > details.fees); + } + + #[test] + #[serial] + fn test_sync_bump_fee_add_input() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + let node_addr = test_client.get_node_address(None); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1) + }); + + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 75_000); + + let mut builder = wallet.build_tx(); + builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf(); + let (mut psbt, details) = builder.finish().unwrap(); + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(psbt.extract_tx()).unwrap(); + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees); + assert_eq!(details.received, 1_000 - details.fees); + + let mut builder = wallet.build_fee_bump(details.txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb(10.0)); + let (mut new_psbt, new_details) = builder.finish().unwrap(); + let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(new_psbt.extract_tx()).unwrap(); + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(new_details.sent, 75_000); + assert_eq!(wallet.get_balance().unwrap(), new_details.received); + } + + #[test] + #[serial] + fn test_sync_bump_fee_add_input_no_change() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + let node_addr = test_client.get_node_address(None); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1) + }); + + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 75_000); + + let mut builder = wallet.build_tx(); + builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf(); + let (mut psbt, details) = builder.finish().unwrap(); + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(psbt.extract_tx()).unwrap(); + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees); + assert_eq!(details.received, 1_000 - details.fees); + + let mut builder = wallet.build_fee_bump(details.txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb(123.0)); + let (mut new_psbt, new_details) = builder.finish().unwrap(); + println!("{:#?}", new_details); + + let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(new_psbt.extract_tx()).unwrap(); + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(new_details.sent, 75_000); + assert_eq!(wallet.get_balance().unwrap(), 0); + assert_eq!(new_details.received, 0); + } + + #[test] + #[serial] + fn test_sync_receive_coinbase() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + let wallet_addr = wallet.get_address(New).unwrap(); + + wallet.sync(noop_progress(), None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 0); + + test_client.generate(1, Some(wallet_addr)); + + wallet.sync(noop_progress(), None).unwrap(); + assert!(wallet.get_balance().unwrap() > 0); + } + } + } +} diff --git a/testutils/src/lib.rs b/testutils/src/lib.rs index 29e43a41..333af54f 100644 --- a/testutils/src/lib.rs +++ b/testutils/src/lib.rs @@ -11,6 +11,7 @@ #[macro_use] extern crate serde_json; +mod blockchain_tests; pub use serial_test::serial; @@ -297,11 +298,12 @@ where } impl TestClient { - pub fn new() -> Self { - let url = env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string()); - let wallet = env::var("BDK_RPC_WALLET").unwrap_or_else(|_| "bdk-test".to_string()); - let client = - RpcClient::new(format!("http://{}/wallet/{}", url, wallet), get_auth()).unwrap(); + pub fn new(rpc_host_and_wallet: String, rpc_wallet_name: String) -> Self { + let client = RpcClient::new( + format!("http://{}/wallet/{}", rpc_host_and_wallet, rpc_wallet_name), + get_auth(), + ) + .unwrap(); let electrum = ElectrumClient::new(&get_electrum_url()).unwrap(); TestClient { client, electrum } @@ -562,3 +564,12 @@ impl Deref for TestClient { &self.client } } + +impl Default for TestClient { + fn default() -> Self { + let rpc_host_and_port = + env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string()); + let wallet = env::var("BDK_RPC_WALLET").unwrap_or_else(|_| "bdk-test".to_string()); + Self::new(rpc_host_and_port, wallet) + } +} From fcae5adabdb0bcb285e4757390b98dedbf8bdfe4 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Tue, 18 May 2021 15:21:48 +1000 Subject: [PATCH 02/10] Run blockchain tests on esplora They were only being run on electrum before. --- .github/workflows/cont_integration.yml | 22 ++++++++++++++++++---- Cargo.toml | 1 + ci/start-core.sh | 4 ---- src/blockchain/esplora.rs | 8 ++++++++ testutils/src/blockchain_tests.rs | 3 +++ 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 3a92f052..dd899cd4 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -73,10 +73,19 @@ jobs: - name: Test run: cargo test --features test-md-docs --no-default-features -- doctest::ReadmeDoctests - test-electrum: - name: Test electrum + test-blockchains: + name: test ${{ matrix.blockchain.name }} runs-on: ubuntu-16.04 - container: bitcoindevkit/electrs:0.2.0 + strategy: + matrix: + blockchain: + - name: electrum + container: bitcoindevkit/electrs + start: /root/electrs --network regtest --jsonrpc-import + - name: esplora + container: bitcoindevkit/esplora + start: /root/electrs --network regtest -vvv --cookie admin:passw --jsonrpc-import --electrum-rpc-addr=0.0.0.0:60401 --http-addr 0.0.0.0:3002 + container: ${{ matrix.blockchain.container }} env: BDK_RPC_AUTH: USER_PASS BDK_RPC_USER: admin @@ -84,6 +93,7 @@ jobs: BDK_RPC_URL: 127.0.0.1:18443 BDK_RPC_WALLET: bdk-test BDK_ELECTRUM_URL: tcp://127.0.0.1:60401 + BDK_ESPLORA_URL: http://127.0.0.1:3002 steps: - name: Checkout uses: actions/checkout@v2 @@ -95,6 +105,8 @@ jobs: ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} + - name: get pkg-config # running eslpora tests seems to need this + run: apt update && apt install -y --fix-missing pkg-config libssl-dev - name: Install rustup run: curl https://sh.rustup.rs -sSf | sh -s -- -y - name: Set default toolchain @@ -105,8 +117,10 @@ jobs: run: $HOME/.cargo/bin/rustup update - name: Start core run: ./ci/start-core.sh + - name: start ${{ matrix.blockchain.name }} + run: nohup ${{ matrix.blockchain.start }} & sleep 5 - name: Test - run: $HOME/.cargo/bin/cargo test --features test-electrum --no-default-features + run: $HOME/.cargo/bin/cargo test --features test-${{ matrix.blockchain.name }} --no-default-features ${{ matrix.blockchain.name }}::bdk_blockchain_tests check-wasm: name: Check WASM diff --git a/Cargo.toml b/Cargo.toml index 52176e5d..44a5623c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ keys-bip39 = ["tiny-bip39"] # Debug/Test features test-electrum = ["electrum"] +test-esplora = ["esplora"] test-md-docs = ["electrum"] [dev-dependencies] diff --git a/ci/start-core.sh b/ci/start-core.sh index 59c024e1..455442b4 100755 --- a/ci/start-core.sh +++ b/ci/start-core.sh @@ -11,7 +11,3 @@ done echo "Generating 150 bitcoin blocks." ADDR=$(/root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS -rpcwallet=$BDK_RPC_WALLET getnewaddress) /root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS generatetoaddress 150 $ADDR - -echo "Starting electrs node." -nohup /root/electrs --network regtest --jsonrpc-import & -sleep 5 diff --git a/src/blockchain/esplora.rs b/src/blockchain/esplora.rs index 793da96b..5214fcec 100644 --- a/src/blockchain/esplora.rs +++ b/src/blockchain/esplora.rs @@ -414,3 +414,11 @@ impl_error!(reqwest::Error, Reqwest, EsploraError); impl_error!(std::num::ParseIntError, Parsing, EsploraError); impl_error!(consensus::encode::Error, BitcoinEncoding, EsploraError); impl_error!(bitcoin::hashes::hex::Error, Hex, EsploraError); + +#[cfg(all(feature = "test-esplora", test))] +testutils::bdk_blockchain_tests! { + bdk => crate, + fn test_instance() -> EsploraBlockchain { + EsploraBlockchain::new(std::env::var("BDK_ESPLORA_URL").unwrap_or("127.0.0.1:3002".into()).as_str(), None) + } +} diff --git a/testutils/src/blockchain_tests.rs b/testutils/src/blockchain_tests.rs index 95ebcc4b..07953a3d 100644 --- a/testutils/src/blockchain_tests.rs +++ b/testutils/src/blockchain_tests.rs @@ -193,6 +193,9 @@ macro_rules! bdk_blockchain_tests { assert_eq!(list_tx_item.height, None); } + // FIXME: I would like this to be cfg_attr(not(feature = "test-esplora"), ignore) but it + // doesn't work for some reason. + #[cfg(not(feature = "test-esplora"))] #[test] #[serial] fn test_sync_reorg_block() { From d60c5003bf8b324727fa31c8d104a53e17d7a8c3 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Wed, 19 May 2021 13:04:32 +1000 Subject: [PATCH 03/10] Merge testutils crate into the main crate This avoids having to keep the apis in sync between the macros and the main project. --- .github/workflows/cont_integration.yml | 2 +- Cargo.toml | 16 +++++----- src/blockchain/electrum.rs | 5 ++- src/blockchain/esplora.rs | 5 ++- src/lib.rs | 9 +++--- .../src => src/testutils}/blockchain_tests.rs | 25 ++++++++------- testutils/src/lib.rs => src/testutils/mod.rs | 32 +++++++++++-------- src/wallet/address_validator.rs | 2 +- src/wallet/mod.rs | 1 + testutils/.gitignore | 2 -- testutils/Cargo.toml | 26 --------------- 11 files changed, 50 insertions(+), 75 deletions(-) rename {testutils/src => src/testutils}/blockchain_tests.rs (97%) rename testutils/src/lib.rs => src/testutils/mod.rs (95%) delete mode 100644 testutils/.gitignore delete mode 100644 testutils/Cargo.toml diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index dd899cd4..9d6fe420 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -120,7 +120,7 @@ jobs: - name: start ${{ matrix.blockchain.name }} run: nohup ${{ matrix.blockchain.start }} & sleep 5 - name: Test - run: $HOME/.cargo/bin/cargo test --features test-${{ matrix.blockchain.name }} --no-default-features ${{ matrix.blockchain.name }}::bdk_blockchain_tests + run: $HOME/.cargo/bin/cargo test --features ${{ matrix.blockchain.name }},test-blockchains --no-default-features ${{ matrix.blockchain.name }}::bdk_blockchain_tests check-wasm: name: Check WASM diff --git a/Cargo.toml b/Cargo.toml index 44a5623c..dda31216 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,10 @@ socks = { version = "0.3", optional = true } lazy_static = { version = "1.4", optional = true } tiny-bip39 = { version = "^0.8", optional = true } +# Needed by bdk_blockchain_tests macro +bitcoincore-rpc = { version = "0.13", optional = true } +serial_test = { version = "0.4", optional = true } + # Platform-specific dependencies [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1", features = ["rt"] } @@ -54,17 +58,16 @@ all-keys = ["keys-bip39"] keys-bip39 = ["tiny-bip39"] # Debug/Test features -test-electrum = ["electrum"] -test-esplora = ["esplora"] +testutils = [] +test-blockchains = ["testutils", "bitcoincore-rpc", "electrum-client"] test-md-docs = ["electrum"] [dev-dependencies] -bdk-testutils = { path = "./testutils" } -serial_test = "0.4" lazy_static = "1.4" env_logger = "0.7" base64 = "^0.11" clap = "2.33" +serial_test = "0.4" [[example]] name = "address_validator" @@ -78,10 +81,7 @@ path = "examples/compiler.rs" required-features = ["compiler"] [workspace] -members = ["macros", "testutils"] - -# Generate docs with nightly to add the "features required" badge -# https://stackoverflow.com/questions/61417452/how-to-get-a-feature-requirement-tag-in-the-documentation-generated-by-cargo-do +members = ["macros"] [package.metadata.docs.rs] features = ["compiler", "electrum", "esplora", "compact_filters", "key-value-db", "all-keys"] # defines the configuration attribute `docsrs` diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index 2e103307..a37f0556 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -169,9 +169,8 @@ impl ConfigurableBlockchain for ElectrumBlockchain { } } -#[cfg(all(feature = "test-electrum", test))] -testutils::bdk_blockchain_tests! { - bdk => crate, +#[cfg(feature = "test-blockchains")] +crate::bdk_blockchain_tests! { fn test_instance() -> ElectrumBlockchain { ElectrumBlockchain::from(Client::new(&testutils::get_electrum_url()).unwrap()) } diff --git a/src/blockchain/esplora.rs b/src/blockchain/esplora.rs index 5214fcec..ff85f22f 100644 --- a/src/blockchain/esplora.rs +++ b/src/blockchain/esplora.rs @@ -415,9 +415,8 @@ impl_error!(std::num::ParseIntError, Parsing, EsploraError); impl_error!(consensus::encode::Error, BitcoinEncoding, EsploraError); impl_error!(bitcoin::hashes::hex::Error, Hex, EsploraError); -#[cfg(all(feature = "test-esplora", test))] -testutils::bdk_blockchain_tests! { - bdk => crate, +#[cfg(feature = "test-blockchains")] +crate::bdk_blockchain_tests! { fn test_instance() -> EsploraBlockchain { EsploraBlockchain::new(std::env::var("BDK_ESPLORA_URL").unwrap_or("127.0.0.1:3002".into()).as_str(), None) } diff --git a/src/lib.rs b/src/lib.rs index a4670b5c..722f680a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -228,16 +228,12 @@ pub extern crate reqwest; #[cfg(feature = "key-value-db")] pub extern crate sled; -#[allow(unused_imports)] -#[cfg(test)] -#[macro_use] -extern crate testutils; #[allow(unused_imports)] #[cfg(test)] #[allow(unused_imports)] #[cfg(test)] #[macro_use] -extern crate serial_test; +pub extern crate serial_test; #[macro_use] pub(crate) mod error; @@ -265,3 +261,6 @@ pub use wallet::Wallet; pub fn version() -> &'static str { env!("CARGO_PKG_VERSION", "unknown") } + +#[cfg(any(feature = "testutils", test))] +pub mod testutils; diff --git a/testutils/src/blockchain_tests.rs b/src/testutils/blockchain_tests.rs similarity index 97% rename from testutils/src/blockchain_tests.rs rename to src/testutils/blockchain_tests.rs index 07953a3d..1c470f05 100644 --- a/testutils/src/blockchain_tests.rs +++ b/src/testutils/blockchain_tests.rs @@ -3,18 +3,19 @@ /// the setup required to run the tests yourself. #[macro_export] macro_rules! bdk_blockchain_tests { - (bdk => $bdk:ident, + ( fn test_instance() -> $blockchain:ty $block:block) => { + #[cfg(test)] mod bdk_blockchain_tests { - use $bdk::bitcoin::Network; - use $bdk::miniscript::Descriptor; - use $crate::{TestClient, serial}; - use $bdk::blockchain::{Blockchain, noop_progress}; - use $bdk::descriptor::ExtendedDescriptor; - use $bdk::database::MemoryDatabase; - use $bdk::types::KeychainKind; - use $bdk::{Wallet, TxBuilder, FeeRate}; - use $bdk::wallet::AddressIndex::New; + use $crate::bitcoin::Network; + use $crate::testutils::{TestClient}; + use $crate::blockchain::noop_progress; + use $crate::database::MemoryDatabase; + use $crate::types::KeychainKind; + use $crate::{Wallet, FeeRate}; + use $crate::wallet::AddressIndex::New; + use $crate::testutils; + use $crate::serial_test::serial; use super::*; @@ -195,7 +196,7 @@ macro_rules! bdk_blockchain_tests { // FIXME: I would like this to be cfg_attr(not(feature = "test-esplora"), ignore) but it // doesn't work for some reason. - #[cfg(not(feature = "test-esplora"))] + #[cfg(not(feature = "esplora"))] #[test] #[serial] fn test_sync_reorg_block() { @@ -478,7 +479,7 @@ macro_rules! bdk_blockchain_tests { #[test] #[serial] fn test_sync_receive_coinbase() { - let (wallet, descriptors, mut test_client) = init_single_sig(); + let (wallet, _, mut test_client) = init_single_sig(); let wallet_addr = wallet.get_address(New).unwrap(); wallet.sync(noop_progress(), None).unwrap(); diff --git a/testutils/src/lib.rs b/src/testutils/mod.rs similarity index 95% rename from testutils/src/lib.rs rename to src/testutils/mod.rs index 333af54f..4771c946 100644 --- a/testutils/src/lib.rs +++ b/src/testutils/mod.rs @@ -8,12 +8,11 @@ // , at your option. // You may not use this file except in accordance with one or both of these // licenses. +#![allow(missing_docs)] -#[macro_use] -extern crate serde_json; mod blockchain_tests; -pub use serial_test::serial; +// pub use serial_test::serial; use std::collections::HashMap; use std::env; @@ -138,13 +137,14 @@ impl TranslateDescriptor for Descriptor { } } +#[doc(hidden)] #[macro_export] macro_rules! testutils { ( @external $descriptors:expr, $child:expr ) => ({ use bitcoin::secp256k1::Secp256k1; use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait}; - use $crate::TranslateDescriptor; + use $crate::testutils::TranslateDescriptor; let secp = Secp256k1::new(); @@ -155,7 +155,7 @@ macro_rules! testutils { use bitcoin::secp256k1::Secp256k1; use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait}; - use $crate::TranslateDescriptor; + use $crate::testutils::TranslateDescriptor; let secp = Secp256k1::new(); @@ -167,25 +167,27 @@ macro_rules! testutils { ( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )* $( ( @confirmations $confirmations:expr ) )* $( ( @replaceable $replaceable:expr ) )* ) => ({ let mut outs = Vec::new(); - $( outs.push(testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))); )+ - + $( outs.push($crate::testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))); )+ + #[allow(unused_mut)] let mut locktime = None::; $( locktime = Some($locktime); )* + #[allow(unused_assignments, unused_mut)] let mut min_confirmations = None::; $( min_confirmations = Some($confirmations); )* + #[allow(unused_assignments, unused_mut)] let mut replaceable = None::; $( replaceable = Some($replaceable); )* - testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable) + $crate::testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable) }); ( @literal $key:expr ) => ({ let key = $key.to_string(); (key, None::, None::) }); - ( @generate_xprv $( $external_path:expr )* $( ,$internal_path:expr )* ) => ({ + ( @generate_xprv $( $external_path:expr )? $( ,$internal_path:expr )? ) => ({ use rand::Rng; let mut seed = [0u8; 32]; @@ -196,11 +198,13 @@ macro_rules! testutils { &seed, ); + #[allow(unused_assignments)] let mut external_path = None::; - $( external_path = Some($external_path.to_string()); )* + $( external_path = Some($external_path.to_string()); )? + #[allow(unused_assignments)] let mut internal_path = None::; - $( internal_path = Some($internal_path.to_string()); )* + $( internal_path = Some($internal_path.to_string()); )? (key.unwrap().to_string(), external_path, internal_path) }); @@ -230,11 +234,10 @@ macro_rules! testutils { ( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )* $( ( @keys $( $keys:tt )* ) )* ) => ({ use std::str::FromStr; use std::collections::HashMap; - use std::convert::TryInto; - - use miniscript::descriptor::{Descriptor, DescriptorPublicKey}; + use miniscript::descriptor::Descriptor; use miniscript::TranslatePk; + #[allow(unused_assignments, unused_mut)] let mut keys: HashMap<&'static str, (String, Option, Option)> = HashMap::new(); $( keys = testutils!{ @keys $( $keys )* }; @@ -257,6 +260,7 @@ macro_rules! testutils { }); let external = external.to_string(); + #[allow(unused_assignments, unused_mut)] let mut internal = None::; $( let string_internal: Descriptor = FromStr::from_str($internal_descriptor).unwrap(); diff --git a/src/wallet/address_validator.rs b/src/wallet/address_validator.rs index 4e20ef5e..36e39be1 100644 --- a/src/wallet/address_validator.rs +++ b/src/wallet/address_validator.rs @@ -146,7 +146,7 @@ mod test { let (mut wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); wallet.add_address_validator(Arc::new(TestValidator)); - let addr = testutils!(@external descriptors, 10); + let addr = crate::testutils!(@external descriptors, 10); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); builder.finish().unwrap(); diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 71fc5d7d..28d954e4 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1515,6 +1515,7 @@ pub(crate) mod test { use crate::types::KeychainKind; use super::*; + use crate::testutils; use crate::wallet::AddressIndex::{LastUnused, New, Peek, Reset}; #[test] diff --git a/testutils/.gitignore b/testutils/.gitignore deleted file mode 100644 index 2c96eb1b..00000000 --- a/testutils/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -target/ -Cargo.lock diff --git a/testutils/Cargo.toml b/testutils/Cargo.toml deleted file mode 100644 index 8f7f2618..00000000 --- a/testutils/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "bdk-testutils" -version = "0.4.0" -authors = ["Alekos Filini "] -edition = "2018" -homepage = "https://bitcoindevkit.org" -repository = "https://github.com/bitcoindevkit/bdk" -documentation = "https://docs.rs/bdk-testutils" -description = "Supporting testing utilities for `bdk`" -keywords = ["bdk"] -license = "MIT OR Apache-2.0" - -[lib] -name = "testutils" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -log = "0.4.8" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -serial_test = "0.4" -bitcoin = "0.26" -bitcoincore-rpc = "0.13" -miniscript = "5.1" -electrum-client = "0.6.0" From 38b0470b1437a9154ed0c894dbecd76b3e36ef6f Mon Sep 17 00:00:00 2001 From: LLFourn Date: Wed, 19 May 2021 15:52:51 +1000 Subject: [PATCH 04/10] Move blockchain related stuff to blockchain_tests --- src/blockchain/electrum.rs | 2 +- src/testutils/blockchain_tests.rs | 335 ++++++++++++++++++++++++++++- src/testutils/mod.rs | 341 +----------------------------- 3 files changed, 338 insertions(+), 340 deletions(-) diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index a37f0556..4d8926af 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -172,6 +172,6 @@ impl ConfigurableBlockchain for ElectrumBlockchain { #[cfg(feature = "test-blockchains")] crate::bdk_blockchain_tests! { fn test_instance() -> ElectrumBlockchain { - ElectrumBlockchain::from(Client::new(&testutils::get_electrum_url()).unwrap()) + ElectrumBlockchain::from(Client::new(&testutils::blockchain_tests::get_electrum_url()).unwrap()) } } diff --git a/src/testutils/blockchain_tests.rs b/src/testutils/blockchain_tests.rs index 1c470f05..30127166 100644 --- a/src/testutils/blockchain_tests.rs +++ b/src/testutils/blockchain_tests.rs @@ -1,3 +1,336 @@ +use crate::testutils::TestIncomingTx; +use bitcoin::consensus::encode::{deserialize, serialize}; +use bitcoin::hashes::hex::{FromHex, ToHex}; +use bitcoin::hashes::sha256d; +use bitcoin::{Address, Amount, Script, Transaction, Txid}; +pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType; +pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi}; +use core::str::FromStr; +pub use electrum_client::{Client as ElectrumClient, ElectrumApi}; +#[allow(unused_imports)] +use log::{debug, error, info, trace}; +use std::collections::HashMap; +use std::env; +use std::ops::Deref; +use std::path::PathBuf; +use std::time::Duration; + +pub struct TestClient { + client: RpcClient, + electrum: ElectrumClient, +} + +impl TestClient { + pub fn new(rpc_host_and_wallet: String, rpc_wallet_name: String) -> Self { + let client = RpcClient::new( + format!("http://{}/wallet/{}", rpc_host_and_wallet, rpc_wallet_name), + get_auth(), + ) + .unwrap(); + let electrum = ElectrumClient::new(&get_electrum_url()).unwrap(); + + TestClient { client, electrum } + } + + fn wait_for_tx(&mut self, txid: Txid, monitor_script: &Script) { + // wait for electrs to index the tx + exponential_backoff_poll(|| { + trace!("wait_for_tx {}", txid); + + self.electrum + .script_get_history(monitor_script) + .unwrap() + .iter() + .position(|entry| entry.tx_hash == txid) + }); + } + + fn wait_for_block(&mut self, min_height: usize) { + self.electrum.block_headers_subscribe().unwrap(); + + loop { + let header = exponential_backoff_poll(|| { + self.electrum.ping().unwrap(); + self.electrum.block_headers_pop().unwrap() + }); + if header.height >= min_height { + break; + } + } + } + + pub fn receive(&mut self, meta_tx: TestIncomingTx) -> Txid { + assert!( + !meta_tx.output.is_empty(), + "can't create a transaction with no outputs" + ); + + let mut map = HashMap::new(); + + let mut required_balance = 0; + for out in &meta_tx.output { + required_balance += out.value; + map.insert(out.to_address.clone(), Amount::from_sat(out.value)); + } + + if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) { + panic!("Insufficient funds in bitcoind. Please generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap()); + } + + // FIXME: core can't create a tx with two outputs to the same address + let tx = self + .create_raw_transaction_hex(&[], &map, meta_tx.locktime, meta_tx.replaceable) + .unwrap(); + let tx = self.fund_raw_transaction(tx, None, None).unwrap(); + let mut tx: Transaction = deserialize(&tx.hex).unwrap(); + + if let Some(true) = meta_tx.replaceable { + // for some reason core doesn't set this field right + for input in &mut tx.input { + input.sequence = 0xFFFFFFFD; + } + } + + let tx = self + .sign_raw_transaction_with_wallet(&serialize(&tx), None, None) + .unwrap(); + + // broadcast through electrum so that it caches the tx immediately + let txid = self + .electrum + .transaction_broadcast(&deserialize(&tx.hex).unwrap()) + .unwrap(); + + if let Some(num) = meta_tx.min_confirmations { + self.generate(num, None); + } + + let monitor_script = Address::from_str(&meta_tx.output[0].to_address) + .unwrap() + .script_pubkey(); + self.wait_for_tx(txid, &monitor_script); + + debug!("Sent tx: {}", txid); + + txid + } + + pub fn bump_fee(&mut self, txid: &Txid) -> Txid { + let tx = self.get_raw_transaction_info(txid, None).unwrap(); + assert!( + tx.confirmations.is_none(), + "Can't bump tx {} because it's already confirmed", + txid + ); + + let bumped: serde_json::Value = self.call("bumpfee", &[txid.to_string().into()]).unwrap(); + let new_txid = Txid::from_str(&bumped["txid"].as_str().unwrap().to_string()).unwrap(); + + let monitor_script = + tx.vout[0].script_pub_key.addresses.as_ref().unwrap()[0].script_pubkey(); + self.wait_for_tx(new_txid, &monitor_script); + + debug!("Bumped {}, new txid {}", txid, new_txid); + + new_txid + } + + pub fn generate_manually(&mut self, txs: Vec) -> String { + use bitcoin::blockdata::block::{Block, BlockHeader}; + use bitcoin::blockdata::script::Builder; + use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut}; + use bitcoin::hash_types::{BlockHash, TxMerkleNode}; + + let block_template: serde_json::Value = self + .call("getblocktemplate", &[json!({"rules": ["segwit"]})]) + .unwrap(); + trace!("getblocktemplate: {:#?}", block_template); + + let header = BlockHeader { + version: block_template["version"].as_i64().unwrap() as i32, + prev_blockhash: BlockHash::from_hex( + block_template["previousblockhash"].as_str().unwrap(), + ) + .unwrap(), + merkle_root: TxMerkleNode::default(), + time: block_template["curtime"].as_u64().unwrap() as u32, + bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(), + nonce: 0, + }; + debug!("header: {:#?}", header); + + let height = block_template["height"].as_u64().unwrap() as i64; + let witness_reserved_value: Vec = sha256d::Hash::default().as_ref().into(); + // burn block subsidy and fees, not a big deal + let mut coinbase_tx = Transaction { + version: 1, + lock_time: 0, + input: vec![TxIn { + previous_output: OutPoint::null(), + script_sig: Builder::new().push_int(height).into_script(), + sequence: 0xFFFFFFFF, + witness: vec![witness_reserved_value], + }], + output: vec![], + }; + + let mut txdata = vec![coinbase_tx.clone()]; + txdata.extend_from_slice(&txs); + + let mut block = Block { header, txdata }; + + let witness_root = block.witness_root(); + let witness_commitment = + Block::compute_witness_commitment(&witness_root, &coinbase_tx.input[0].witness[0]); + + // now update and replace the coinbase tx + let mut coinbase_witness_commitment_script = vec![0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed]; + coinbase_witness_commitment_script.extend_from_slice(&witness_commitment); + + coinbase_tx.output.push(TxOut { + value: 0, + script_pubkey: coinbase_witness_commitment_script.into(), + }); + block.txdata[0] = coinbase_tx; + + // set merkle root + let merkle_root = block.merkle_root(); + block.header.merkle_root = merkle_root; + + assert!(block.check_merkle_root()); + assert!(block.check_witness_commitment()); + + // now do PoW :) + let target = block.header.target(); + while block.header.validate_pow(&target).is_err() { + block.header.nonce = block.header.nonce.checked_add(1).unwrap(); // panic if we run out of nonces + } + + let block_hex: String = serialize(&block).to_hex(); + debug!("generated block hex: {}", block_hex); + + self.electrum.block_headers_subscribe().unwrap(); + + let submit_result: serde_json::Value = + self.call("submitblock", &[block_hex.into()]).unwrap(); + debug!("submitblock: {:?}", submit_result); + assert!( + submit_result.is_null(), + "submitblock error: {:?}", + submit_result.as_str() + ); + + self.wait_for_block(height as usize); + + block.header.block_hash().to_hex() + } + + pub fn generate(&mut self, num_blocks: u64, address: Option
) { + let address = address.unwrap_or_else(|| self.get_new_address(None, None).unwrap()); + let hashes = self.generate_to_address(num_blocks, &address).unwrap(); + let best_hash = hashes.last().unwrap(); + let height = self.get_block_info(best_hash).unwrap().height; + + self.wait_for_block(height); + + debug!("Generated blocks to new height {}", height); + } + + pub fn invalidate(&mut self, num_blocks: u64) { + self.electrum.block_headers_subscribe().unwrap(); + + let best_hash = self.get_best_block_hash().unwrap(); + let initial_height = self.get_block_info(&best_hash).unwrap().height; + + let mut to_invalidate = best_hash; + for i in 1..=num_blocks { + trace!( + "Invalidating block {}/{} ({})", + i, + num_blocks, + to_invalidate + ); + + self.invalidate_block(&to_invalidate).unwrap(); + to_invalidate = self.get_best_block_hash().unwrap(); + } + + self.wait_for_block(initial_height - num_blocks as usize); + + debug!( + "Invalidated {} blocks to new height of {}", + num_blocks, + initial_height - num_blocks as usize + ); + } + + pub fn reorg(&mut self, num_blocks: u64) { + self.invalidate(num_blocks); + self.generate(num_blocks, None); + } + + pub fn get_node_address(&self, address_type: Option) -> Address { + Address::from_str( + &self + .get_new_address(None, address_type) + .unwrap() + .to_string(), + ) + .unwrap() + } +} + +pub fn get_electrum_url() -> String { + env::var("BDK_ELECTRUM_URL").unwrap_or_else(|_| "tcp://127.0.0.1:50001".to_string()) +} + +impl Deref for TestClient { + type Target = RpcClient; + + fn deref(&self) -> &Self::Target { + &self.client + } +} + +impl Default for TestClient { + fn default() -> Self { + let rpc_host_and_port = + env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string()); + let wallet = env::var("BDK_RPC_WALLET").unwrap_or_else(|_| "bdk-test".to_string()); + Self::new(rpc_host_and_port, wallet) + } +} + +fn exponential_backoff_poll(mut poll: F) -> T +where + F: FnMut() -> Option, +{ + let mut delay = Duration::from_millis(64); + loop { + match poll() { + Some(data) => break data, + None if delay.as_millis() < 512 => delay = delay.mul_f32(2.0), + None => {} + } + + std::thread::sleep(delay); + } +} + +// TODO: we currently only support env vars, we could also parse a toml file +fn get_auth() -> Auth { + match env::var("BDK_RPC_AUTH").as_ref().map(String::as_ref) { + Ok("USER_PASS") => Auth::UserPass( + env::var("BDK_RPC_USER").unwrap(), + env::var("BDK_RPC_PASS").unwrap(), + ), + _ => Auth::CookieFile(PathBuf::from( + env::var("BDK_RPC_COOKIEFILE") + .unwrap_or_else(|_| "/home/user/.bitcoin/regtest/.cookie".to_string()), + )), + } +} + /// This macro runs blockchain tests against a `Blockchain` implementation. It requires access to a /// Bitcoin core wallet via RPC. At the moment you have to dig into the code yourself and look at /// the setup required to run the tests yourself. @@ -8,7 +341,7 @@ macro_rules! bdk_blockchain_tests { #[cfg(test)] mod bdk_blockchain_tests { use $crate::bitcoin::Network; - use $crate::testutils::{TestClient}; + use $crate::testutils::blockchain_tests::TestClient; use $crate::blockchain::noop_progress; use $crate::database::MemoryDatabase; use $crate::types::KeychainKind; diff --git a/src/testutils/mod.rs b/src/testutils/mod.rs index 4771c946..506c53d5 100644 --- a/src/testutils/mod.rs +++ b/src/testutils/mod.rs @@ -10,57 +10,15 @@ // licenses. #![allow(missing_docs)] -mod blockchain_tests; +#[cfg(feature = "test-blockchains")] +pub mod blockchain_tests; -// pub use serial_test::serial; - -use std::collections::HashMap; -use std::env; -use std::ops::Deref; -use std::path::PathBuf; -use std::str::FromStr; -use std::time::Duration; - -#[allow(unused_imports)] -use log::{debug, error, info, trace}; - -use bitcoin::consensus::encode::{deserialize, serialize}; -use bitcoin::hashes::hex::{FromHex, ToHex}; -use bitcoin::hashes::sha256d; use bitcoin::secp256k1::{Secp256k1, Verification}; -use bitcoin::{Address, Amount, PublicKey, Script, Transaction, Txid}; +use bitcoin::{Address, PublicKey}; use miniscript::descriptor::DescriptorPublicKey; use miniscript::{Descriptor, MiniscriptKey, TranslatePk}; -pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType; -pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi}; - -pub use electrum_client::{Client as ElectrumClient, ElectrumApi}; - -// TODO: we currently only support env vars, we could also parse a toml file -fn get_auth() -> Auth { - match env::var("BDK_RPC_AUTH").as_ref().map(String::as_ref) { - Ok("USER_PASS") => Auth::UserPass( - env::var("BDK_RPC_USER").unwrap(), - env::var("BDK_RPC_PASS").unwrap(), - ), - _ => Auth::CookieFile(PathBuf::from( - env::var("BDK_RPC_COOKIEFILE") - .unwrap_or_else(|_| "/home/user/.bitcoin/regtest/.cookie".to_string()), - )), - } -} - -pub fn get_electrum_url() -> String { - env::var("BDK_ELECTRUM_URL").unwrap_or_else(|_| "tcp://127.0.0.1:50001".to_string()) -} - -pub struct TestClient { - client: RpcClient, - electrum: ElectrumClient, -} - #[derive(Clone, Debug)] pub struct TestIncomingOutput { pub value: u64, @@ -284,296 +242,3 @@ macro_rules! testutils { (external, internal) }) } - -fn exponential_backoff_poll(mut poll: F) -> T -where - F: FnMut() -> Option, -{ - let mut delay = Duration::from_millis(64); - loop { - match poll() { - Some(data) => break data, - None if delay.as_millis() < 512 => delay = delay.mul_f32(2.0), - None => {} - } - - std::thread::sleep(delay); - } -} - -impl TestClient { - pub fn new(rpc_host_and_wallet: String, rpc_wallet_name: String) -> Self { - let client = RpcClient::new( - format!("http://{}/wallet/{}", rpc_host_and_wallet, rpc_wallet_name), - get_auth(), - ) - .unwrap(); - let electrum = ElectrumClient::new(&get_electrum_url()).unwrap(); - - TestClient { client, electrum } - } - - fn wait_for_tx(&mut self, txid: Txid, monitor_script: &Script) { - // wait for electrs to index the tx - exponential_backoff_poll(|| { - trace!("wait_for_tx {}", txid); - - self.electrum - .script_get_history(monitor_script) - .unwrap() - .iter() - .position(|entry| entry.tx_hash == txid) - }); - } - - fn wait_for_block(&mut self, min_height: usize) { - self.electrum.block_headers_subscribe().unwrap(); - - loop { - let header = exponential_backoff_poll(|| { - self.electrum.ping().unwrap(); - self.electrum.block_headers_pop().unwrap() - }); - if header.height >= min_height { - break; - } - } - } - - pub fn receive(&mut self, meta_tx: TestIncomingTx) -> Txid { - assert!( - !meta_tx.output.is_empty(), - "can't create a transaction with no outputs" - ); - - let mut map = HashMap::new(); - - let mut required_balance = 0; - for out in &meta_tx.output { - required_balance += out.value; - map.insert(out.to_address.clone(), Amount::from_sat(out.value)); - } - - if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) { - panic!("Insufficient funds in bitcoind. Please generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap()); - } - - // FIXME: core can't create a tx with two outputs to the same address - let tx = self - .create_raw_transaction_hex(&[], &map, meta_tx.locktime, meta_tx.replaceable) - .unwrap(); - let tx = self.fund_raw_transaction(tx, None, None).unwrap(); - let mut tx: Transaction = deserialize(&tx.hex).unwrap(); - - if let Some(true) = meta_tx.replaceable { - // for some reason core doesn't set this field right - for input in &mut tx.input { - input.sequence = 0xFFFFFFFD; - } - } - - let tx = self - .sign_raw_transaction_with_wallet(&serialize(&tx), None, None) - .unwrap(); - - // broadcast through electrum so that it caches the tx immediately - let txid = self - .electrum - .transaction_broadcast(&deserialize(&tx.hex).unwrap()) - .unwrap(); - - if let Some(num) = meta_tx.min_confirmations { - self.generate(num, None); - } - - let monitor_script = Address::from_str(&meta_tx.output[0].to_address) - .unwrap() - .script_pubkey(); - self.wait_for_tx(txid, &monitor_script); - - debug!("Sent tx: {}", txid); - - txid - } - - pub fn bump_fee(&mut self, txid: &Txid) -> Txid { - let tx = self.get_raw_transaction_info(txid, None).unwrap(); - assert!( - tx.confirmations.is_none(), - "Can't bump tx {} because it's already confirmed", - txid - ); - - let bumped: serde_json::Value = self.call("bumpfee", &[txid.to_string().into()]).unwrap(); - let new_txid = Txid::from_str(&bumped["txid"].as_str().unwrap().to_string()).unwrap(); - - let monitor_script = - tx.vout[0].script_pub_key.addresses.as_ref().unwrap()[0].script_pubkey(); - self.wait_for_tx(new_txid, &monitor_script); - - debug!("Bumped {}, new txid {}", txid, new_txid); - - new_txid - } - - pub fn generate_manually(&mut self, txs: Vec) -> String { - use bitcoin::blockdata::block::{Block, BlockHeader}; - use bitcoin::blockdata::script::Builder; - use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut}; - use bitcoin::hash_types::{BlockHash, TxMerkleNode}; - - let block_template: serde_json::Value = self - .call("getblocktemplate", &[json!({"rules": ["segwit"]})]) - .unwrap(); - trace!("getblocktemplate: {:#?}", block_template); - - let header = BlockHeader { - version: block_template["version"].as_i64().unwrap() as i32, - prev_blockhash: BlockHash::from_hex( - block_template["previousblockhash"].as_str().unwrap(), - ) - .unwrap(), - merkle_root: TxMerkleNode::default(), - time: block_template["curtime"].as_u64().unwrap() as u32, - bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(), - nonce: 0, - }; - debug!("header: {:#?}", header); - - let height = block_template["height"].as_u64().unwrap() as i64; - let witness_reserved_value: Vec = sha256d::Hash::default().as_ref().into(); - // burn block subsidy and fees, not a big deal - let mut coinbase_tx = Transaction { - version: 1, - lock_time: 0, - input: vec![TxIn { - previous_output: OutPoint::null(), - script_sig: Builder::new().push_int(height).into_script(), - sequence: 0xFFFFFFFF, - witness: vec![witness_reserved_value], - }], - output: vec![], - }; - - let mut txdata = vec![coinbase_tx.clone()]; - txdata.extend_from_slice(&txs); - - let mut block = Block { header, txdata }; - - let witness_root = block.witness_root(); - let witness_commitment = - Block::compute_witness_commitment(&witness_root, &coinbase_tx.input[0].witness[0]); - - // now update and replace the coinbase tx - let mut coinbase_witness_commitment_script = vec![0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed]; - coinbase_witness_commitment_script.extend_from_slice(&witness_commitment); - - coinbase_tx.output.push(TxOut { - value: 0, - script_pubkey: coinbase_witness_commitment_script.into(), - }); - block.txdata[0] = coinbase_tx; - - // set merkle root - let merkle_root = block.merkle_root(); - block.header.merkle_root = merkle_root; - - assert!(block.check_merkle_root()); - assert!(block.check_witness_commitment()); - - // now do PoW :) - let target = block.header.target(); - while block.header.validate_pow(&target).is_err() { - block.header.nonce = block.header.nonce.checked_add(1).unwrap(); // panic if we run out of nonces - } - - let block_hex: String = serialize(&block).to_hex(); - debug!("generated block hex: {}", block_hex); - - self.electrum.block_headers_subscribe().unwrap(); - - let submit_result: serde_json::Value = - self.call("submitblock", &[block_hex.into()]).unwrap(); - debug!("submitblock: {:?}", submit_result); - assert!( - submit_result.is_null(), - "submitblock error: {:?}", - submit_result.as_str() - ); - - self.wait_for_block(height as usize); - - block.header.block_hash().to_hex() - } - - pub fn generate(&mut self, num_blocks: u64, address: Option
) { - let address = address.unwrap_or_else(|| self.get_new_address(None, None).unwrap()); - let hashes = self.generate_to_address(num_blocks, &address).unwrap(); - let best_hash = hashes.last().unwrap(); - let height = self.get_block_info(best_hash).unwrap().height; - - self.wait_for_block(height); - - debug!("Generated blocks to new height {}", height); - } - - pub fn invalidate(&mut self, num_blocks: u64) { - self.electrum.block_headers_subscribe().unwrap(); - - let best_hash = self.get_best_block_hash().unwrap(); - let initial_height = self.get_block_info(&best_hash).unwrap().height; - - let mut to_invalidate = best_hash; - for i in 1..=num_blocks { - trace!( - "Invalidating block {}/{} ({})", - i, - num_blocks, - to_invalidate - ); - - self.invalidate_block(&to_invalidate).unwrap(); - to_invalidate = self.get_best_block_hash().unwrap(); - } - - self.wait_for_block(initial_height - num_blocks as usize); - - debug!( - "Invalidated {} blocks to new height of {}", - num_blocks, - initial_height - num_blocks as usize - ); - } - - pub fn reorg(&mut self, num_blocks: u64) { - self.invalidate(num_blocks); - self.generate(num_blocks, None); - } - - pub fn get_node_address(&self, address_type: Option) -> Address { - Address::from_str( - &self - .get_new_address(None, address_type) - .unwrap() - .to_string(), - ) - .unwrap() - } -} - -impl Deref for TestClient { - type Target = RpcClient; - - fn deref(&self) -> &Self::Target { - &self.client - } -} - -impl Default for TestClient { - fn default() -> Self { - let rpc_host_and_port = - env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string()); - let wallet = env::var("BDK_RPC_WALLET").unwrap_or_else(|_| "bdk-test".to_string()); - Self::new(rpc_host_and_port, wallet) - } -} From 00bdf08f2a683e9bae6041b192b6b7a1de4ba5df Mon Sep 17 00:00:00 2001 From: LLFourn Date: Wed, 19 May 2021 16:09:01 +1000 Subject: [PATCH 05/10] Remove testutils feature so doctests worka again I wanted to only conditionally compile testutils but it's needed in doctests which we can't conditionally compile for: https://github.com/rust-lang/rust/issues/67295 --- Cargo.toml | 3 +-- src/database/memory.rs | 2 +- src/lib.rs | 6 +++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dda31216..78ddb116 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,8 +58,7 @@ all-keys = ["keys-bip39"] keys-bip39 = ["tiny-bip39"] # Debug/Test features -testutils = [] -test-blockchains = ["testutils", "bitcoincore-rpc", "electrum-client"] +test-blockchains = ["bitcoincore-rpc", "electrum-client"] test-md-docs = ["electrum"] [dev-dependencies] diff --git a/src/database/memory.rs b/src/database/memory.rs index 465698d5..adf4e20f 100644 --- a/src/database/memory.rs +++ b/src/database/memory.rs @@ -511,7 +511,7 @@ macro_rules! doctest_wallet { () => {{ use $crate::bitcoin::Network; use $crate::database::MemoryDatabase; - use testutils::testutils; + use $crate::testutils; let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"; let descriptors = testutils!(@descriptors (descriptor) (descriptor)); diff --git a/src/lib.rs b/src/lib.rs index 722f680a..77cbef61 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -262,5 +262,9 @@ pub fn version() -> &'static str { env!("CARGO_PKG_VERSION", "unknown") } -#[cfg(any(feature = "testutils", test))] +// We should consider putting this under a feature flag but we need the macro in doctets so we need +// to wait until https://github.com/rust-lang/rust/issues/67295 is fixed. +// +// Stuff in here is too rough to document atm +#[doc(hidden)] pub mod testutils; From 5b194c268d4fd477b688be76b8921ab2e67f6f36 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Wed, 19 May 2021 16:10:06 +1000 Subject: [PATCH 06/10] Fix clippy warnings inside testutils macro Now that it's inside the main repo clippy is having a go at me. --- src/testutils/mod.rs | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/src/testutils/mod.rs b/src/testutils/mod.rs index 506c53d5..5d7146bd 100644 --- a/src/testutils/mod.rs +++ b/src/testutils/mod.rs @@ -123,20 +123,13 @@ macro_rules! testutils { ( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) }); ( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) }); - ( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )* $( ( @confirmations $confirmations:expr ) )* $( ( @replaceable $replaceable:expr ) )* ) => ({ - let mut outs = Vec::new(); - $( outs.push($crate::testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))); )+ - #[allow(unused_mut)] - let mut locktime = None::; - $( locktime = Some($locktime); )* + ( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )? $( ( @confirmations $confirmations:expr ) )? $( ( @replaceable $replaceable:expr ) )? ) => ({ + let outs = vec![$( $crate::testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))),+]; - #[allow(unused_assignments, unused_mut)] - let mut min_confirmations = None::; - $( min_confirmations = Some($confirmations); )* + let locktime = None::$(.or(Some($locktime)))?; - #[allow(unused_assignments, unused_mut)] - let mut replaceable = None::; - $( replaceable = Some($replaceable); )* + let min_confirmations = None::$(.or(Some($confirmations)))?; + let replaceable = None::$(.or(Some($replaceable)))?; $crate::testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable) }); @@ -156,13 +149,8 @@ macro_rules! testutils { &seed, ); - #[allow(unused_assignments)] - let mut external_path = None::; - $( external_path = Some($external_path.to_string()); )? - - #[allow(unused_assignments)] - let mut internal_path = None::; - $( internal_path = Some($internal_path.to_string()); )? + let external_path = None::$(.or(Some($external_path.to_string())))?; + let internal_path = None::$(.or(Some($internal_path.to_string())))?; (key.unwrap().to_string(), external_path, internal_path) }); @@ -189,7 +177,7 @@ macro_rules! testutils { map }); - ( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )* $( ( @keys $( $keys:tt )* ) )* ) => ({ + ( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )? $( ( @keys $( $keys:tt )* ) )* ) => ({ use std::str::FromStr; use std::collections::HashMap; use miniscript::descriptor::Descriptor; @@ -218,9 +206,7 @@ macro_rules! testutils { }); let external = external.to_string(); - #[allow(unused_assignments, unused_mut)] - let mut internal = None::; - $( + let internal = None::$(.or({ let string_internal: Descriptor = FromStr::from_str($internal_descriptor).unwrap(); let string_internal: Descriptor = string_internal.translate_pk_infallible::<_, _>(|k| { @@ -236,8 +222,8 @@ macro_rules! testutils { kh.clone() } }); - internal = Some(string_internal.to_string()); - )* + Some(string_internal.to_string()) + }))?; (external, internal) }) From aba2a05d83143e7dfacded8cbb64980790f71f30 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Wed, 19 May 2021 16:22:11 +1000 Subject: [PATCH 07/10] Add script for running the blockchain tests locally --- run_blockchain_tests.sh | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100755 run_blockchain_tests.sh diff --git a/run_blockchain_tests.sh b/run_blockchain_tests.sh new file mode 100755 index 00000000..96497502 --- /dev/null +++ b/run_blockchain_tests.sh @@ -0,0 +1,54 @@ +#!/bin/sh +# +# Script for running the tests for a specific blockchain by starting up the backends in docker. +# simply run ./run_blockchain_tests [esplora|electrum] + +eprintln(){ + echo "$@" >&2 +} + +cleanup() { + if test "$id"; then + eprintln "cleaning up $blockchain docker container $id"; + docker rm -fv "$id"; + fi +} + +# Makes sure we clean up the container at the end or if ^C +trap 'rc=$?; cleanup; exit $rc' EXIT INT +blockchain=${1:-electrum} + +case "$blockchain" in + electrum) + eprintln "starting electrs docker container" + id="$(docker run -d -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp bitcoindevkit/electrs)" + ;; + esplora) + eprintln "starting esplora docker container" + id="$(docker run -d -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp -p 127.0.0.1:3002:3002/tcp bitcoindevkit/esplora)" + export BDK_ESPLORA_URL=http://127.0.0.1:3002 + ;; + *) + echo "'$blockchain' is not a blockchain we can test (electrum,esplora)"; + exit 1; + esac + +# taken from https://github.com/bitcoindevkit/bitcoin-regtest-box +export BDK_RPC_AUTH=USER_PASS +export BDK_RPC_USER=admin +export BDK_RPC_PASS=passw +export BDK_RPC_URL=127.0.0.1:18443 +export BDK_RPC_WALLET=bdk-test +export BDK_ELECTRUM_URL=tcp://127.0.0.1:60401 + +cli(){ + docker exec -it "$id" /root/bitcoin-cli -regtest -rpcuser=admin -rpcpassword=passw $@ +} + +eprintln "running getwalletinfo unitl bitcoind seems to be alive" +while ! cli getwalletinfo >/dev/null; do sleep 1; done + +# sleep again for good measure! +sleep 1; + +cargo test --features "test-blockchains,$blockchain" --no-default-features "$blockchain::bdk_blockchain_tests" From 4c92daf51779a6cf205a708ad81b225b973076f2 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Thu, 20 May 2021 14:33:02 +1000 Subject: [PATCH 08/10] Uppercase 'Test' so that github can see what's up It is expecting something named 'Test electrum' --- .github/workflows/cont_integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 9d6fe420..fe4a97e1 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -74,7 +74,7 @@ jobs: run: cargo test --features test-md-docs --no-default-features -- doctest::ReadmeDoctests test-blockchains: - name: test ${{ matrix.blockchain.name }} + name: Test ${{ matrix.blockchain.name }} runs-on: ubuntu-16.04 strategy: matrix: From d2a981efee612c9fb45c268680f1bc2d43d675cd Mon Sep 17 00:00:00 2001 From: LLFourn Date: Fri, 21 May 2021 13:21:41 +1000 Subject: [PATCH 09/10] run_blockchain_tests.sh improvements --- run_blockchain_tests.sh | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/run_blockchain_tests.sh b/run_blockchain_tests.sh index 96497502..ce87f26f 100755 --- a/run_blockchain_tests.sh +++ b/run_blockchain_tests.sh @@ -1,7 +1,13 @@ #!/bin/sh -# -# Script for running the tests for a specific blockchain by starting up the backends in docker. -# simply run ./run_blockchain_tests [esplora|electrum] + +usage() { + cat <<'EOF' +Script for running the bdk blockchain tests for a specific blockchain by starting up the backend in docker. + +Usage: ./run_blockchain_tests.sh [esplora|electrum] [test name]. + +EOF +} eprintln(){ echo "$@" >&2 @@ -10,13 +16,16 @@ eprintln(){ cleanup() { if test "$id"; then eprintln "cleaning up $blockchain docker container $id"; - docker rm -fv "$id"; + docker rm -fv "$id" > /dev/null; fi + trap - EXIT INT } # Makes sure we clean up the container at the end or if ^C trap 'rc=$?; cleanup; exit $rc' EXIT INT -blockchain=${1:-electrum} + +blockchain="$1" +test_name="$2" case "$blockchain" in electrum) @@ -29,8 +38,9 @@ case "$blockchain" in export BDK_ESPLORA_URL=http://127.0.0.1:3002 ;; *) - echo "'$blockchain' is not a blockchain we can test (electrum,esplora)"; + usage; exit 1; + ;; esac # taken from https://github.com/bitcoindevkit/bitcoin-regtest-box @@ -45,10 +55,10 @@ cli(){ docker exec -it "$id" /root/bitcoin-cli -regtest -rpcuser=admin -rpcpassword=passw $@ } -eprintln "running getwalletinfo unitl bitcoind seems to be alive" +eprintln "running getwalletinfo until bitcoind seems to be alive" while ! cli getwalletinfo >/dev/null; do sleep 1; done # sleep again for good measure! sleep 1; -cargo test --features "test-blockchains,$blockchain" --no-default-features "$blockchain::bdk_blockchain_tests" +cargo test --features "test-blockchains,$blockchain" --no-default-features "$blockchain::bdk_blockchain_tests::$test_name" From ea8488b2a78b07da4a7882b45bbbd4eda9bbb828 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Fri, 21 May 2021 13:21:59 +1000 Subject: [PATCH 10/10] Initialize env_logger at start of blockchain tests --- src/testutils/blockchain_tests.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/testutils/blockchain_tests.rs b/src/testutils/blockchain_tests.rs index 30127166..30999990 100644 --- a/src/testutils/blockchain_tests.rs +++ b/src/testutils/blockchain_tests.rs @@ -361,6 +361,8 @@ macro_rules! bdk_blockchain_tests { } fn init_single_sig() -> (Wallet<$blockchain, MemoryDatabase>, (String, Option), TestClient) { + let _ = env_logger::try_init(); + let descriptors = testutils! { @descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) ) };