Skip to content

Commit 9d7f859

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 09ddfb2 commit 9d7f859

File tree

1 file changed

+227
-37
lines changed

1 file changed

+227
-37
lines changed

crates/testenv/src/lib.rs

Lines changed: 227 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,18 @@
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, CompactTarget, 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::consensus::Decodable;
13+
use bitcoin::hex::HexToBytesError;
14+
use core::str::FromStr;
15+
use core::time::Duration;
16+
use electrsd::corepc_node::vtype::GetBlockTemplate;
17+
use electrsd::corepc_node::{TemplateRequest, TemplateRules};
918

1019
pub use electrsd;
1120
pub use electrsd::corepc_client;
@@ -45,6 +54,27 @@ impl Default for Config<'_> {
4554
}
4655
}
4756

57+
/// Parameters for [`TestEnv::mine_block`].
58+
#[non_exhaustive]
59+
#[derive(Default)]
60+
pub struct MineParams {
61+
/// If `true`, the block will be empty (no mempool transactions).
62+
pub empty: bool,
63+
/// Set a custom block timestamp. Defaults to `max(min_time, now)`.
64+
pub time: Option<u32>,
65+
/// Set a custom coinbase output script. Defaults to `OP_TRUE`.
66+
pub coinbase_address: Option<ScriptBuf>,
67+
}
68+
69+
impl MineParams {
70+
fn address_or_anyone_can_spend(&self) -> ScriptBuf {
71+
self.coinbase_address
72+
.clone()
73+
// OP_TRUE (anyone can spend)
74+
.unwrap_or(ScriptBuf::from_bytes(vec![0x51]))
75+
}
76+
}
77+
4878
impl TestEnv {
4979
/// Construct a new [`TestEnv`] instance with the default configuration used by BDK.
5080
pub fn new() -> anyhow::Result<Self> {
@@ -119,70 +149,157 @@ impl TestEnv {
119149
Ok(block_hashes)
120150
}
121151

152+
/// Get a block template from the node.
153+
pub fn get_block_template(&self) -> anyhow::Result<GetBlockTemplate> {
154+
use corepc_node::TemplateRequest;
155+
Ok(self.bitcoind.client.get_block_template(&TemplateRequest {
156+
rules: vec![
157+
TemplateRules::Segwit,
158+
TemplateRules::Taproot,
159+
TemplateRules::Csv,
160+
],
161+
})?)
162+
}
163+
122164
/// Mine a block that is guaranteed to be empty even with transactions in the mempool.
123165
#[cfg(feature = "std")]
124166
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,
129-
};
130-
use corepc_node::{TemplateRequest, TemplateRules};
131-
let request = TemplateRequest {
132-
rules: vec![TemplateRules::Segwit],
133-
};
134-
let bt = self
167+
self.mine_block(MineParams {
168+
empty: true,
169+
..Default::default()
170+
})
171+
}
172+
173+
/// Get the minimum valid timestamp for the next block.
174+
pub fn min_time_for_next_block(&self) -> anyhow::Result<u32> {
175+
Ok(self
135176
.bitcoind
136177
.client
137-
.get_block_template(&request)?
138-
.into_model()?;
178+
.get_block_template(&TemplateRequest {
179+
rules: vec![
180+
TemplateRules::Segwit,
181+
TemplateRules::Taproot,
182+
TemplateRules::Csv,
183+
],
184+
})?
185+
.min_time)
186+
}
187+
188+
/// Mine a single block with the given [`MineParams`].
189+
pub fn mine_block(&self, params: MineParams) -> anyhow::Result<(usize, BlockHash)> {
190+
let bt = self.bitcoind.client.get_block_template(&TemplateRequest {
191+
rules: vec![
192+
TemplateRules::Segwit,
193+
TemplateRules::Taproot,
194+
TemplateRules::Csv,
195+
],
196+
})?;
197+
let coinbase_scriptsig = {
198+
// BIP34 requires the height to be the first item in coinbase scriptSig.
199+
// Bitcoin Core validates by checking if scriptSig STARTS with the expected
200+
// encoding (using minimal opcodes like OP_1 for height 1).
201+
// The scriptSig must also be 2-100 bytes total.
202+
let mut builder = bdk_chain::bitcoin::script::Builder::new().push_int(bt.height);
203+
for v in bt.coinbase_aux.values() {
204+
let bytes = Vec::<u8>::from_hex(v).expect("must be valid hex");
205+
let bytes_buf = PushBytesBuf::try_from(bytes).expect("must be valid bytes");
206+
builder = builder.push_slice(bytes_buf);
207+
}
208+
let script = builder.into_script();
209+
// Ensure scriptSig is at least 2 bytes (pad with OP_0 if needed)
210+
if script.len() < 2 {
211+
bdk_chain::bitcoin::script::Builder::new()
212+
.push_int(bt.height)
213+
.push_opcode(bdk_chain::bitcoin::opcodes::OP_0)
214+
.into_script()
215+
} else {
216+
script
217+
}
218+
};
139219

140-
let txdata = vec![Transaction {
220+
let coinbase_outputs = if params.empty {
221+
let value: Amount = Amount::from_sat(
222+
(bt.coinbase_value - bt.transactions.iter().map(|tx| tx.fee).sum::<i64>()) as u64,
223+
);
224+
vec![TxOut {
225+
value,
226+
script_pubkey: params.address_or_anyone_can_spend(),
227+
}]
228+
} else {
229+
core::iter::once(TxOut {
230+
value: Amount::from_sat(bt.coinbase_value.try_into().expect("must fit into u64")),
231+
script_pubkey: params.address_or_anyone_can_spend(),
232+
})
233+
.chain(
234+
bt.default_witness_commitment
235+
.map(|s| -> Result<_, HexToBytesError> {
236+
Ok(TxOut {
237+
value: Amount::ZERO,
238+
script_pubkey: ScriptBuf::from_hex(&s)?,
239+
})
240+
})
241+
.transpose()?,
242+
)
243+
.collect()
244+
};
245+
246+
let coinbase_tx = Transaction {
141247
version: transaction::Version::ONE,
142248
lock_time: bdk_chain::bitcoin::absolute::LockTime::from_height(0)?,
143249
input: vec![TxIn {
144250
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(),
251+
script_sig: coinbase_scriptsig,
150252
sequence: bdk_chain::bitcoin::Sequence::default(),
151253
witness: bdk_chain::bitcoin::Witness::new(),
152254
}],
153-
output: vec![TxOut {
154-
value: Amount::ZERO,
155-
script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
156-
}],
157-
}];
255+
output: coinbase_outputs,
256+
};
257+
258+
let txdata = if params.empty {
259+
vec![coinbase_tx]
260+
} else {
261+
core::iter::once(coinbase_tx)
262+
.chain(bt.transactions.iter().map(|tx| {
263+
let raw = Vec::<u8>::from_hex(&tx.data).expect("must be valid hex");
264+
Transaction::consensus_decode(&mut raw.as_slice()).expect("must decode tx")
265+
}))
266+
.collect()
267+
};
158268

159269
let mut block = Block {
160270
header: Header {
161-
version: bt.version,
162-
prev_blockhash: bt.previous_block_hash,
163-
merkle_root: TxMerkleNode::all_zeros(),
164-
time: Ord::max(
271+
version: bdk_chain::bitcoin::blockdata::block::Version::from_consensus(bt.version),
272+
prev_blockhash: BlockHash::from_str(&bt.previous_block_hash)?,
273+
merkle_root: TxMerkleNode::from_raw_hash(
274+
bdk_chain::bitcoin::merkle_tree::calculate_root(
275+
txdata.iter().map(|tx| tx.compute_txid().to_raw_hash()),
276+
)
277+
.expect("must have atleast one tx"),
278+
),
279+
time: params.time.unwrap_or(Ord::max(
165280
bt.min_time,
166281
std::time::UNIX_EPOCH.elapsed()?.as_secs() as u32,
167-
),
168-
bits: bt.bits,
282+
)),
283+
bits: CompactTarget::from_unprefixed_hex(&bt.bits)?,
169284
nonce: 0,
170285
},
171286
txdata,
172287
};
173288

174289
block.header.merkle_root = block.compute_merkle_root().expect("must compute");
175290

291+
// Mine!
292+
let target = block.header.target();
176293
for nonce in 0..=u32::MAX {
177294
block.header.nonce = nonce;
178-
if block.header.target().is_met_by(block.block_hash()) {
179-
break;
295+
let blockhash = block.block_hash();
296+
if target.is_met_by(blockhash) {
297+
self.rpc_client().submit_block(&block)?;
298+
return Ok((bt.height as usize, blockhash));
180299
}
181300
}
182301

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

188305
/// This method waits for the Electrum notification indicating that a new block has been mined.
@@ -318,9 +435,11 @@ impl TestEnv {
318435
#[cfg(test)]
319436
#[cfg_attr(coverage_nightly, coverage(off))]
320437
mod test {
321-
use crate::TestEnv;
438+
use crate::{MineParams, TestEnv};
439+
use bdk_chain::bitcoin::{Amount, ScriptBuf};
322440
use core::time::Duration;
323441
use electrsd::corepc_node::anyhow::Result;
442+
use std::collections::BTreeSet;
324443

325444
/// This checks that reorgs initiated by `bitcoind` is detected by our `electrsd` instance.
326445
#[test]
@@ -355,4 +474,75 @@ mod test {
355474

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

0 commit comments

Comments
 (0)