-
Notifications
You must be signed in to change notification settings - Fork 427
Make mining more flexible in bdk_testenv
#2100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,9 +3,16 @@ | |
| pub mod utils; | ||
|
|
||
| use anyhow::Context; | ||
| use bdk_chain::bitcoin::{ | ||
| block::Header, hash_types::TxMerkleNode, hex::FromHex, script::PushBytesBuf, transaction, | ||
| Address, Amount, Block, BlockHash, ScriptBuf, Transaction, TxIn, TxOut, Txid, | ||
| }; | ||
| use bdk_chain::CheckPoint; | ||
| use bitcoin::{address::NetworkChecked, Address, Amount, BlockHash, Txid}; | ||
| use std::time::Duration; | ||
| use bitcoin::address::NetworkChecked; | ||
| use bitcoin::hex::HexToBytesError; | ||
| use core::time::Duration; | ||
| use electrsd::corepc_node::mtype::GetBlockTemplate; | ||
| use electrsd::corepc_node::{TemplateRequest, TemplateRules}; | ||
|
|
||
| pub use electrsd; | ||
| pub use electrsd::corepc_client; | ||
|
|
@@ -45,6 +52,32 @@ impl Default for Config<'_> { | |
| } | ||
| } | ||
|
|
||
| /// Parameters for [`TestEnv::mine_block`]. | ||
| #[non_exhaustive] | ||
| #[derive(Default)] | ||
| pub struct MineParams { | ||
| /// If `true`, the block will be empty (no mempool transactions). | ||
| pub empty: bool, | ||
| /// Set a custom block timestamp. Defaults to `max(min_time, now)`. | ||
| pub time: Option<u32>, | ||
| /// Set a custom coinbase output script. Defaults to `OP_TRUE`. | ||
| pub coinbase_address: Option<ScriptBuf>, | ||
| } | ||
|
|
||
| impl MineParams { | ||
| fn address_or_anyone_can_spend(&self) -> ScriptBuf { | ||
| use bdk_chain::bitcoin::opcodes::OP_TRUE; | ||
| self.coinbase_address | ||
| .clone() | ||
| // OP_TRUE (anyone can spend) | ||
| .unwrap_or_else(|| { | ||
| bdk_chain::bitcoin::script::Builder::new() | ||
| .push_opcode(OP_TRUE) | ||
| .into_script() | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| impl TestEnv { | ||
| /// Construct a new [`TestEnv`] instance with the default configuration used by BDK. | ||
| pub fn new() -> anyhow::Result<Self> { | ||
|
|
@@ -119,52 +152,135 @@ impl TestEnv { | |
| Ok(block_hashes) | ||
| } | ||
|
|
||
| /// Get a block template from the node. | ||
| pub fn get_block_template(&self) -> anyhow::Result<GetBlockTemplate> { | ||
| Ok(self | ||
| .bitcoind | ||
| .client | ||
| .get_block_template(&TemplateRequest { | ||
| rules: vec![ | ||
| TemplateRules::Segwit, | ||
| TemplateRules::Taproot, | ||
| TemplateRules::Csv, | ||
| ], | ||
| })? | ||
| .into_model()?) | ||
| } | ||
|
|
||
| /// Mine a block that is guaranteed to be empty even with transactions in the mempool. | ||
| #[cfg(feature = "std")] | ||
| pub fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> { | ||
| use bitcoin::secp256k1::rand::random; | ||
| use bitcoin::{ | ||
| block::Header, hashes::Hash, transaction, Block, ScriptBuf, ScriptHash, Transaction, | ||
| TxIn, TxMerkleNode, TxOut, | ||
| self.mine_block(MineParams { | ||
| empty: true, | ||
| ..Default::default() | ||
| }) | ||
| } | ||
|
|
||
| /// Get the minimum valid timestamp for the next block. | ||
| pub fn min_time_for_next_block(&self) -> anyhow::Result<u32> { | ||
| Ok(self.get_block_template()?.min_time) | ||
| } | ||
|
|
||
| /// Mine a single block with the given [`MineParams`]. | ||
| pub fn mine_block(&self, params: MineParams) -> anyhow::Result<(usize, BlockHash)> { | ||
| let bt = self.get_block_template()?; | ||
|
|
||
| // BIP34 requires the height to be the first item in coinbase scriptSig. | ||
| // Bitcoin Core validates by checking if scriptSig STARTS with the expected | ||
| // encoding (using minimal opcodes like OP_1 for height 1). | ||
| // The scriptSig must also be 2-100 bytes total. | ||
| fn build_coinbase_scriptsig(bt: &GetBlockTemplate, pad: bool) -> ScriptBuf { | ||
| let mut builder = bdk_chain::bitcoin::script::Builder::new().push_int(bt.height as i64); | ||
| if pad { | ||
| builder = builder.push_opcode(bdk_chain::bitcoin::opcodes::OP_0); | ||
| } | ||
| for v in bt.coinbase_aux.values() { | ||
| let bytes = Vec::<u8>::from_hex(v).expect("must be valid hex"); | ||
| let bytes_buf = PushBytesBuf::try_from(bytes).expect("must be valid bytes"); | ||
| builder = builder.push_slice(bytes_buf); | ||
| } | ||
| builder.into_script() | ||
| } | ||
| let coinbase_scriptsig = { | ||
| let mut script = build_coinbase_scriptsig(&bt, false); | ||
| // Ensure scriptSig is at least 2 bytes (pad with OP_0 if needed) | ||
| if script.len() < 2 { | ||
| script = build_coinbase_scriptsig(&bt, true); | ||
| }; | ||
| script | ||
|
Comment on lines
+204
to
+210
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: it's only being used here, so the padding could be handled directly, but it's not a blocker though. |
||
| }; | ||
| use corepc_node::{TemplateRequest, TemplateRules}; | ||
| let request = TemplateRequest { | ||
| rules: vec![TemplateRules::Segwit], | ||
|
|
||
| let coinbase_outputs = if params.empty { | ||
| let tx_fees: Amount = bt | ||
| .transactions | ||
| .iter() | ||
| .map(|tx| tx.fee.to_unsigned().expect("fee must be positive")) | ||
| .sum(); | ||
| let value = bt | ||
| .coinbase_value | ||
| .to_unsigned() | ||
| .expect("coinbase_value must be positive") | ||
| - tx_fees; | ||
| vec![TxOut { | ||
| value, | ||
| script_pubkey: params.address_or_anyone_can_spend(), | ||
| }] | ||
| } else { | ||
| core::iter::once(TxOut { | ||
| value: bt | ||
| .coinbase_value | ||
| .to_unsigned() | ||
| .expect("coinbase_value must be positive"), | ||
| script_pubkey: params.address_or_anyone_can_spend(), | ||
| }) | ||
| .chain( | ||
| bt.default_witness_commitment | ||
| .as_ref() | ||
| .map(|s| -> Result<_, HexToBytesError> { | ||
| Ok(TxOut { | ||
| value: Amount::ZERO, | ||
| script_pubkey: ScriptBuf::from_hex(s)?, | ||
| }) | ||
| }) | ||
| .transpose()?, | ||
| ) | ||
| .collect() | ||
|
Comment on lines
+213
to
+247
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: was this created by claude, or there's an specific reason you need this ? |
||
| }; | ||
| let bt = self | ||
| .bitcoind | ||
| .client | ||
| .get_block_template(&request)? | ||
| .into_model()?; | ||
|
|
||
| let txdata = vec![Transaction { | ||
| let coinbase_tx = Transaction { | ||
| version: transaction::Version::ONE, | ||
| lock_time: bdk_chain::bitcoin::absolute::LockTime::from_height(0)?, | ||
| input: vec![TxIn { | ||
| previous_output: bdk_chain::bitcoin::OutPoint::default(), | ||
| script_sig: ScriptBuf::builder() | ||
| .push_int(bt.height as _) | ||
| // random number so that re-mining creates unique block | ||
| .push_int(random()) | ||
| .into_script(), | ||
| script_sig: coinbase_scriptsig, | ||
| sequence: bdk_chain::bitcoin::Sequence::default(), | ||
| witness: bdk_chain::bitcoin::Witness::new(), | ||
| }], | ||
| output: vec![TxOut { | ||
| value: Amount::ZERO, | ||
| script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()), | ||
| }], | ||
| }]; | ||
| output: coinbase_outputs, | ||
| }; | ||
|
|
||
| let txdata = if params.empty { | ||
| vec![coinbase_tx] | ||
| } else { | ||
| core::iter::once(coinbase_tx) | ||
| .chain(bt.transactions.iter().map(|tx| tx.data.clone())) | ||
| .collect() | ||
| }; | ||
|
|
||
| let mut block = Block { | ||
| header: Header { | ||
| version: bt.version, | ||
| prev_blockhash: bt.previous_block_hash, | ||
| merkle_root: TxMerkleNode::all_zeros(), | ||
| time: Ord::max( | ||
| merkle_root: TxMerkleNode::from_raw_hash( | ||
| bdk_chain::bitcoin::merkle_tree::calculate_root( | ||
| txdata.iter().map(|tx| tx.compute_txid().to_raw_hash()), | ||
| ) | ||
| .expect("must have atleast one tx"), | ||
| ), | ||
| time: params.time.unwrap_or(Ord::max( | ||
| bt.min_time, | ||
| std::time::UNIX_EPOCH.elapsed()?.as_secs() as u32, | ||
| ), | ||
| )), | ||
| bits: bt.bits, | ||
| nonce: 0, | ||
| }, | ||
|
|
@@ -173,16 +289,18 @@ impl TestEnv { | |
|
|
||
| block.header.merkle_root = block.compute_merkle_root().expect("must compute"); | ||
|
|
||
| // Mine! | ||
| let target = block.header.target(); | ||
| for nonce in 0..=u32::MAX { | ||
| block.header.nonce = nonce; | ||
| if block.header.target().is_met_by(block.block_hash()) { | ||
| break; | ||
| let blockhash = block.block_hash(); | ||
| if target.is_met_by(blockhash) { | ||
| self.rpc_client().submit_block(&block)?; | ||
| return Ok((bt.height as usize, blockhash)); | ||
| } | ||
| } | ||
|
|
||
| self.bitcoind.client.submit_block(&block)?; | ||
|
|
||
| Ok((bt.height as usize, block.block_hash())) | ||
| Err(anyhow::anyhow!("Cannot find nonce that meets the target")) | ||
| } | ||
|
|
||
| /// This method waits for the Electrum notification indicating that a new block has been mined. | ||
|
|
@@ -318,9 +436,12 @@ impl TestEnv { | |
| #[cfg(test)] | ||
| #[cfg_attr(coverage_nightly, coverage(off))] | ||
| mod test { | ||
| use crate::TestEnv; | ||
| use crate::{MineParams, TestEnv}; | ||
| use bdk_chain::bitcoin::opcodes::OP_TRUE; | ||
| use bdk_chain::bitcoin::Amount; | ||
| use core::time::Duration; | ||
| use electrsd::corepc_node::anyhow::Result; | ||
| use std::collections::BTreeSet; | ||
|
|
||
| /// This checks that reorgs initiated by `bitcoind` is detected by our `electrsd` instance. | ||
| #[test] | ||
|
|
@@ -355,4 +476,77 @@ mod test { | |
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_mine_block() -> Result<()> { | ||
| let anyone_can_spend = bdk_chain::bitcoin::script::Builder::new() | ||
| .push_opcode(OP_TRUE) | ||
| .into_script(); | ||
|
|
||
| let env = TestEnv::new()?; | ||
|
|
||
| // So we can spend. | ||
| let addr = env | ||
| .rpc_client() | ||
| .get_new_address(None, None)? | ||
| .address()? | ||
| .assume_checked(); | ||
| env.mine_blocks(100, Some(addr.clone()))?; | ||
|
|
||
| // Try mining a block with custom time. | ||
| let custom_time = env.min_time_for_next_block()? + 100; | ||
| let (_a_height, a_hash) = env.mine_block(MineParams { | ||
| empty: false, | ||
| time: Some(custom_time), | ||
| coinbase_address: None, | ||
| })?; | ||
| let a_block = env.rpc_client().get_block(a_hash)?; | ||
| assert_eq!(a_block.header.time, custom_time); | ||
| assert_eq!( | ||
| a_block.txdata[0].output[0].script_pubkey, anyone_can_spend, | ||
| "Subsidy address must be anyone_can_spend" | ||
| ); | ||
|
|
||
| // Now try mining with min time & some txs. | ||
| let txid1 = env.send(&addr, Amount::from_sat(100_000))?; | ||
| let txid2 = env.send(&addr, Amount::from_sat(200_000))?; | ||
| let txid3 = env.send(&addr, Amount::from_sat(300_000))?; | ||
| let min_time = env.min_time_for_next_block()?; | ||
| let (_b_height, b_hash) = env.mine_block(MineParams { | ||
| empty: false, | ||
| time: Some(min_time), | ||
| coinbase_address: None, | ||
| })?; | ||
| let b_block = env.rpc_client().get_block(b_hash)?; | ||
| assert_eq!(b_block.header.time, min_time); | ||
| assert_eq!( | ||
| a_block.txdata[0].output[0].script_pubkey, anyone_can_spend, | ||
| "Subsidy address must be anyone_can_spend" | ||
| ); | ||
| assert_eq!( | ||
| b_block | ||
| .txdata | ||
| .iter() | ||
| .skip(1) // ignore coinbase | ||
| .map(|tx| tx.compute_txid()) | ||
| .collect::<BTreeSet<_>>(), | ||
| [txid1, txid2, txid3].into_iter().collect(), | ||
| "Must have all txs" | ||
| ); | ||
|
|
||
| // Custom subsidy address. | ||
| let (_c_height, c_hash) = env.mine_block(MineParams { | ||
| empty: false, | ||
| time: None, | ||
| coinbase_address: Some(addr.script_pubkey()), | ||
| })?; | ||
| let c_block = env.rpc_client().get_block(c_hash)?; | ||
| assert_eq!( | ||
| c_block.txdata[0].output[0].script_pubkey, | ||
| addr.script_pubkey(), | ||
| "Custom address works" | ||
| ); | ||
|
|
||
| Ok(()) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: currently it's only being used on the tests, is it really needed ? I mean, the same can be achieved directly calling the getblocktemplate.