33pub mod utils;
44
55use 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+ } ;
610use 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
1017pub use electrsd;
1118pub 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+
4881impl 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) ) ]
320426mod 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