Skip to content

Commit 9b1111f

Browse files
committed
feat(testenv): add mine_block with custom timestamp and coinbase address
Refactor block mining in `TestEnv` to use `getblocktemplate` RPC properly: - Add `MineParams` struct to configure mining (empty blocks, custom timestamp, custom coinbase address) - Add `mine_block()` method that builds blocks from the template with proper BIP34 coinbase scriptSig, witness commitment, and merkle root - Add `min_time_for_next_block()` and `get_block_template()` helpers - Refactor `mine_empty_block()` to use the new `mine_block()` API - Include mempool transactions when `empty: false`
1 parent 01d0dd3 commit 9b1111f

1 file changed

Lines changed: 216 additions & 34 deletions

File tree

crates/testenv/src/lib.rs

Lines changed: 216 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@
33
pub mod utils;
44

55
use anyhow::Context;
6+
use bdk_chain::bitcoin::{
7+
block::Header, hash_types::TxMerkleNode, hex::FromHex, script::PushBytesBuf, transaction,
8+
Address, Amount, Block, BlockHash, ScriptBuf, Transaction, TxIn, TxOut, Txid,
9+
};
610
use bdk_chain::CheckPoint;
7-
use bitcoin::{address::NetworkChecked, Address, Amount, BlockHash, Txid};
8-
use std::time::Duration;
11+
use bitcoin::address::NetworkChecked;
12+
use bitcoin::hex::HexToBytesError;
13+
use core::time::Duration;
14+
use electrsd::corepc_node::mtype::GetBlockTemplate;
15+
use electrsd::corepc_node::{TemplateRequest, TemplateRules};
916

1017
pub use electrsd;
1118
pub use electrsd::corepc_client;
@@ -45,6 +52,32 @@ impl Default for Config<'_> {
4552
}
4653
}
4754

55+
/// Parameters for [`TestEnv::mine_block`].
56+
#[non_exhaustive]
57+
#[derive(Default)]
58+
pub struct MineParams {
59+
/// If `true`, the block will be empty (no mempool transactions).
60+
pub empty: bool,
61+
/// Set a custom block timestamp. Defaults to `max(min_time, now)`.
62+
pub time: Option<u32>,
63+
/// Set a custom coinbase output script. Defaults to `OP_TRUE`.
64+
pub coinbase_address: Option<ScriptBuf>,
65+
}
66+
67+
impl MineParams {
68+
fn address_or_anyone_can_spend(&self) -> ScriptBuf {
69+
use bdk_chain::bitcoin::opcodes::OP_TRUE;
70+
self.coinbase_address
71+
.clone()
72+
// OP_TRUE (anyone can spend)
73+
.unwrap_or_else(|| {
74+
bdk_chain::bitcoin::script::Builder::new()
75+
.push_opcode(OP_TRUE)
76+
.into_script()
77+
})
78+
}
79+
}
80+
4881
impl TestEnv {
4982
/// Construct a new [`TestEnv`] instance with the default configuration used by BDK.
5083
pub fn new() -> anyhow::Result<Self> {
@@ -119,52 +152,123 @@ impl TestEnv {
119152
Ok(block_hashes)
120153
}
121154

155+
/// Get a block template from the node.
156+
pub fn get_block_template(&self) -> anyhow::Result<GetBlockTemplate> {
157+
Ok(self
158+
.bitcoind
159+
.client
160+
.get_block_template(&TemplateRequest {
161+
rules: vec![
162+
TemplateRules::Segwit,
163+
TemplateRules::Taproot,
164+
TemplateRules::Csv,
165+
],
166+
})?
167+
.into_model()?)
168+
}
169+
122170
/// Mine a block that is guaranteed to be empty even with transactions in the mempool.
123171
#[cfg(feature = "std")]
124172
pub fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
125-
use bitcoin::secp256k1::rand::random;
126-
use bitcoin::{
127-
block::Header, hashes::Hash, transaction, Block, ScriptBuf, ScriptHash, Transaction,
128-
TxIn, TxMerkleNode, TxOut,
173+
self.mine_block(MineParams {
174+
empty: true,
175+
..Default::default()
176+
})
177+
}
178+
179+
/// Mine a single block with the given [`MineParams`].
180+
pub fn mine_block(&self, params: MineParams) -> anyhow::Result<(usize, BlockHash)> {
181+
let bt = self.get_block_template()?;
182+
183+
// BIP34 requires the height to be the first item in coinbase scriptSig.
184+
// Bitcoin Core validates by checking if scriptSig STARTS with the expected
185+
// encoding (using minimal opcodes like OP_1 for height 1).
186+
// The scriptSig must also be 2-100 bytes total.
187+
let coinbase_scriptsig = {
188+
let mut builder = bdk_chain::bitcoin::script::Builder::new().push_int(bt.height as i64);
189+
for v in bt.coinbase_aux.values() {
190+
let bytes = Vec::<u8>::from_hex(v).expect("must be valid hex");
191+
let bytes_buf = PushBytesBuf::try_from(bytes).expect("must be valid bytes");
192+
builder = builder.push_slice(bytes_buf);
193+
}
194+
// Ensure scriptSig is at least 2 bytes (pad with OP_0 if needed)
195+
if builder.as_bytes().len() < 2 {
196+
builder = builder.push_opcode(bdk_chain::bitcoin::opcodes::OP_0);
197+
}
198+
builder.into_script()
129199
};
130-
use corepc_node::{TemplateRequest, TemplateRules};
131-
let request = TemplateRequest {
132-
rules: vec![TemplateRules::Segwit],
200+
201+
let coinbase_outputs = if params.empty {
202+
let tx_fees: Amount = bt
203+
.transactions
204+
.iter()
205+
.map(|tx| tx.fee.to_unsigned().expect("fee must be positive"))
206+
.sum();
207+
let value = bt
208+
.coinbase_value
209+
.to_unsigned()
210+
.expect("coinbase_value must be positive")
211+
- tx_fees;
212+
vec![TxOut {
213+
value,
214+
script_pubkey: params.address_or_anyone_can_spend(),
215+
}]
216+
} else {
217+
core::iter::once(TxOut {
218+
value: bt
219+
.coinbase_value
220+
.to_unsigned()
221+
.expect("coinbase_value must be positive"),
222+
script_pubkey: params.address_or_anyone_can_spend(),
223+
})
224+
.chain(
225+
bt.default_witness_commitment
226+
.as_ref()
227+
.map(|s| -> Result<_, HexToBytesError> {
228+
Ok(TxOut {
229+
value: Amount::ZERO,
230+
script_pubkey: ScriptBuf::from_hex(s)?,
231+
})
232+
})
233+
.transpose()?,
234+
)
235+
.collect()
133236
};
134-
let bt = self
135-
.bitcoind
136-
.client
137-
.get_block_template(&request)?
138-
.into_model()?;
139237

140-
let txdata = vec![Transaction {
238+
let coinbase_tx = Transaction {
141239
version: transaction::Version::ONE,
142240
lock_time: bdk_chain::bitcoin::absolute::LockTime::from_height(0)?,
143241
input: vec![TxIn {
144242
previous_output: bdk_chain::bitcoin::OutPoint::default(),
145-
script_sig: ScriptBuf::builder()
146-
.push_int(bt.height as _)
147-
// random number so that re-mining creates unique block
148-
.push_int(random())
149-
.into_script(),
243+
script_sig: coinbase_scriptsig,
150244
sequence: bdk_chain::bitcoin::Sequence::default(),
151245
witness: bdk_chain::bitcoin::Witness::new(),
152246
}],
153-
output: vec![TxOut {
154-
value: Amount::ZERO,
155-
script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
156-
}],
157-
}];
247+
output: coinbase_outputs,
248+
};
249+
250+
let txdata = if params.empty {
251+
vec![coinbase_tx]
252+
} else {
253+
core::iter::once(coinbase_tx)
254+
.chain(bt.transactions.iter().map(|tx| tx.data.clone()))
255+
.collect()
256+
};
158257

159258
let mut block = Block {
160259
header: Header {
161260
version: bt.version,
162261
prev_blockhash: bt.previous_block_hash,
163-
merkle_root: TxMerkleNode::all_zeros(),
164-
time: Ord::max(
262+
merkle_root: TxMerkleNode::from_raw_hash(
263+
bdk_chain::bitcoin::merkle_tree::calculate_root(
264+
txdata.iter().map(|tx| tx.compute_txid().to_raw_hash()),
265+
)
266+
.expect("must have atleast one tx"),
267+
),
268+
time: params.time.unwrap_or(Ord::max(
165269
bt.min_time,
166270
std::time::UNIX_EPOCH.elapsed()?.as_secs() as u32,
167-
),
271+
)),
168272
bits: bt.bits,
169273
nonce: 0,
170274
},
@@ -173,16 +277,18 @@ impl TestEnv {
173277

174278
block.header.merkle_root = block.compute_merkle_root().expect("must compute");
175279

280+
// Mine!
281+
let target = block.header.target();
176282
for nonce in 0..=u32::MAX {
177283
block.header.nonce = nonce;
178-
if block.header.target().is_met_by(block.block_hash()) {
179-
break;
284+
let blockhash = block.block_hash();
285+
if target.is_met_by(blockhash) {
286+
self.rpc_client().submit_block(&block)?;
287+
return Ok((bt.height as usize, blockhash));
180288
}
181289
}
182290

183-
self.bitcoind.client.submit_block(&block)?;
184-
185-
Ok((bt.height as usize, block.block_hash()))
291+
Err(anyhow::anyhow!("Cannot find nonce that meets the target"))
186292
}
187293

188294
/// This method waits for the Electrum notification indicating that a new block has been mined.
@@ -318,9 +424,12 @@ impl TestEnv {
318424
#[cfg(test)]
319425
#[cfg_attr(coverage_nightly, coverage(off))]
320426
mod test {
321-
use crate::TestEnv;
427+
use crate::{MineParams, TestEnv};
428+
use bdk_chain::bitcoin::opcodes::OP_TRUE;
429+
use bdk_chain::bitcoin::Amount;
322430
use core::time::Duration;
323431
use electrsd::corepc_node::anyhow::Result;
432+
use std::collections::BTreeSet;
324433

325434
/// This checks that reorgs initiated by `bitcoind` is detected by our `electrsd` instance.
326435
#[test]
@@ -355,4 +464,77 @@ mod test {
355464

356465
Ok(())
357466
}
467+
468+
#[test]
469+
fn test_mine_block() -> Result<()> {
470+
let anyone_can_spend = bdk_chain::bitcoin::script::Builder::new()
471+
.push_opcode(OP_TRUE)
472+
.into_script();
473+
474+
let env = TestEnv::new()?;
475+
476+
// So we can spend.
477+
let addr = env
478+
.rpc_client()
479+
.get_new_address(None, None)?
480+
.address()?
481+
.assume_checked();
482+
env.mine_blocks(100, Some(addr.clone()))?;
483+
484+
// Try mining a block with custom time.
485+
let custom_time = env.get_block_template()?.min_time + 100;
486+
let (_a_height, a_hash) = env.mine_block(MineParams {
487+
empty: false,
488+
time: Some(custom_time),
489+
coinbase_address: None,
490+
})?;
491+
let a_block = env.rpc_client().get_block(a_hash)?;
492+
assert_eq!(a_block.header.time, custom_time);
493+
assert_eq!(
494+
a_block.txdata[0].output[0].script_pubkey, anyone_can_spend,
495+
"Subsidy address must be anyone_can_spend"
496+
);
497+
498+
// Now try mining with min time & some txs.
499+
let txid1 = env.send(&addr, Amount::from_sat(100_000))?;
500+
let txid2 = env.send(&addr, Amount::from_sat(200_000))?;
501+
let txid3 = env.send(&addr, Amount::from_sat(300_000))?;
502+
let min_time = env.get_block_template()?.min_time;
503+
let (_b_height, b_hash) = env.mine_block(MineParams {
504+
empty: false,
505+
time: Some(min_time),
506+
coinbase_address: None,
507+
})?;
508+
let b_block = env.rpc_client().get_block(b_hash)?;
509+
assert_eq!(b_block.header.time, min_time);
510+
assert_eq!(
511+
a_block.txdata[0].output[0].script_pubkey, anyone_can_spend,
512+
"Subsidy address must be anyone_can_spend"
513+
);
514+
assert_eq!(
515+
b_block
516+
.txdata
517+
.iter()
518+
.skip(1) // ignore coinbase
519+
.map(|tx| tx.compute_txid())
520+
.collect::<BTreeSet<_>>(),
521+
[txid1, txid2, txid3].into_iter().collect(),
522+
"Must have all txs"
523+
);
524+
525+
// Custom subsidy address.
526+
let (_c_height, c_hash) = env.mine_block(MineParams {
527+
empty: false,
528+
time: None,
529+
coinbase_address: Some(addr.script_pubkey()),
530+
})?;
531+
let c_block = env.rpc_client().get_block(c_hash)?;
532+
assert_eq!(
533+
c_block.txdata[0].output[0].script_pubkey,
534+
addr.script_pubkey(),
535+
"Custom address works"
536+
);
537+
538+
Ok(())
539+
}
358540
}

0 commit comments

Comments
 (0)