1- use std:: path:: { Path , PathBuf } ;
1+ use std:: {
2+ path:: { Path , PathBuf } ,
3+ process:: Command ,
4+ sync:: Once ,
5+ } ;
26
37use bitcoin_capnp_types:: {
48 init_capnp:: init,
59 mining_capnp:: { block_template, mining} ,
610 proxy_capnp:: { thread, thread_map} ,
711} ;
12+ use bitcoin_primitives:: Transaction ;
813use capnp_rpc:: { RpcSystem , rpc_twoparty_capnp:: Side , twoparty:: VatNetwork } ;
14+ use encoding:: decode_from_slice;
915use futures:: io:: BufReader ;
1016use tokio:: net:: { UnixStream , unix:: OwnedReadHalf } ;
1117use tokio_util:: compat:: { Compat , TokioAsyncReadCompatExt , TokioAsyncWriteCompatExt } ;
1218
19+ static CHAIN_SETUP : Once = Once :: new ( ) ;
20+
1321pub fn unix_socket_path ( ) -> PathBuf {
1422 let home_dir_string = std:: env:: var ( "HOME" ) . unwrap ( ) ;
1523 let home_dir = home_dir_string. parse :: < PathBuf > ( ) . unwrap ( ) ;
@@ -25,6 +33,170 @@ pub fn unix_socket_path() -> PathBuf {
2533 regtest_dir. join ( "node.sock" )
2634}
2735
36+ fn bitcoin_bin ( ) -> String {
37+ std:: env:: var ( "BITCOIN_BIN" ) . unwrap_or_else ( |_| "bitcoin" . to_owned ( ) )
38+ }
39+
40+ pub fn bitcoin_test_wallet ( ) -> String {
41+ std:: env:: var ( "BITCOIN_TEST_WALLET" ) . unwrap_or_else ( |_| "ipc-test" . to_owned ( ) )
42+ }
43+
44+ fn bitcoin_rpc ( wallet : Option < & str > , args : & [ & str ] ) -> Result < String , String > {
45+ let owned_args: Vec < String > = args. iter ( ) . map ( |arg| ( * arg) . to_owned ( ) ) . collect ( ) ;
46+ bitcoin_rpc_owned ( wallet, & owned_args)
47+ }
48+
49+ fn bitcoin_rpc_owned ( wallet : Option < & str > , args : & [ String ] ) -> Result < String , String > {
50+ let mut command = Command :: new ( bitcoin_bin ( ) ) ;
51+ command. arg ( "rpc" ) . arg ( "-regtest" ) . arg ( "-rpcwait" ) ;
52+ if let Some ( wallet) = wallet {
53+ command. arg ( format ! ( "-rpcwallet={wallet}" ) ) ;
54+ }
55+ command. args ( args) ;
56+
57+ let output = command
58+ . output ( )
59+ . map_err ( |e| format ! ( "failed to execute bitcoin rpc command: {e}" ) ) ?;
60+ if output. status . success ( ) {
61+ Ok ( String :: from_utf8 ( output. stdout )
62+ . unwrap_or_else ( |_| String :: new ( ) )
63+ . trim ( )
64+ . to_owned ( ) )
65+ } else {
66+ Err ( format ! (
67+ "bitcoin rpc command failed: {}" ,
68+ String :: from_utf8_lossy( & output. stderr) . trim( )
69+ ) )
70+ }
71+ }
72+
73+ fn ensure_wallet_loaded ( wallet : & str ) {
74+ if bitcoin_rpc ( Some ( wallet) , & [ "getwalletinfo" ] ) . is_err ( ) {
75+ // First try loading an existing wallet from disk (common when regtest data
76+ // directory is re-used), then fall back to creating it.
77+ if bitcoin_rpc ( None , & [ "loadwallet" , wallet] ) . is_err ( ) {
78+ let _ = bitcoin_rpc ( None , & [ "createwallet" , wallet] ) ;
79+ }
80+
81+ bitcoin_rpc ( Some ( wallet) , & [ "getwalletinfo" ] ) . unwrap_or_else ( |e| {
82+ panic ! ( "wallet {wallet} is not available after load/create attempts: {e}" )
83+ } ) ;
84+ }
85+ }
86+
87+ fn ensure_bootstrap_chain_ready ( ) {
88+ // `call_once` serializes bootstrap initialization across all tests in this
89+ // process. Other callers block until this setup completes.
90+ CHAIN_SETUP . call_once ( || {
91+ let wallet = bitcoin_test_wallet ( ) ;
92+ ensure_chain_height_at_least ( 101 , & wallet) ;
93+ } ) ;
94+ }
95+
96+ fn ensure_chain_height_at_least ( min_height : u32 , wallet : & str ) {
97+ ensure_wallet_loaded ( wallet) ;
98+ let height: u32 = bitcoin_rpc ( None , & [ "getblockcount" ] )
99+ . unwrap_or_else ( |e| panic ! ( "failed to query block height: {e}" ) )
100+ . parse ( )
101+ . unwrap_or_else ( |e| panic ! ( "failed to parse block height: {e}" ) ) ;
102+ if height < min_height {
103+ let blocks_to_generate = ( min_height - height) . to_string ( ) ;
104+ let address = bitcoin_rpc ( Some ( wallet) , & [ "getnewaddress" ] )
105+ . unwrap_or_else ( |e| panic ! ( "failed to get new address from {wallet}: {e}" ) ) ;
106+ bitcoin_rpc (
107+ Some ( wallet) ,
108+ & [
109+ "generatetoaddress" ,
110+ blocks_to_generate. as_str ( ) ,
111+ address. as_str ( ) ,
112+ ] ,
113+ )
114+ . unwrap_or_else ( |e| {
115+ panic ! ( "failed to mine {blocks_to_generate} blocks to reach height {min_height}: {e}" )
116+ } ) ;
117+ }
118+ }
119+
120+ pub fn ensure_wallet_loaded_and_funded ( wallet : & str ) {
121+ ensure_wallet_loaded ( wallet) ;
122+
123+ // getbalance "*" 1 only counts confirmed spendable funds.
124+ let balance: f64 = bitcoin_rpc ( Some ( wallet) , & [ "getbalance" , "*" , "1" ] )
125+ . unwrap_or_else ( |e| panic ! ( "failed to query wallet balance for {wallet}: {e}" ) )
126+ . parse ( )
127+ . unwrap_or_else ( |e| panic ! ( "failed to parse wallet balance for {wallet}: {e}" ) ) ;
128+
129+ if balance < 1.0 {
130+ let address = bitcoin_rpc ( Some ( wallet) , & [ "getnewaddress" ] )
131+ . unwrap_or_else ( |e| panic ! ( "failed to get new address from {wallet}: {e}" ) ) ;
132+ // Mining a single block can mature older coinbase outputs when balance is low.
133+ bitcoin_rpc ( Some ( wallet) , & [ "generatetoaddress" , "1" , address. as_str ( ) ] )
134+ . unwrap_or_else ( |e| panic ! ( "failed to mine blocks to wallet {wallet}: {e}" ) ) ;
135+ }
136+ }
137+
138+ fn send_self_transfer ( wallet : & str ) -> Result < String , String > {
139+ let address = bitcoin_rpc ( Some ( wallet) , & [ "getnewaddress" ] ) ?;
140+ let send_args = vec ! [
141+ "-named" . to_owned( ) ,
142+ "sendtoaddress" . to_owned( ) ,
143+ format!( "address={address}" ) ,
144+ "amount=1" . to_owned( ) ,
145+ "fee_rate=25" . to_owned( ) ,
146+ ] ;
147+ bitcoin_rpc_owned ( Some ( wallet) , & send_args)
148+ }
149+
150+ fn decode_hex ( hex : & str ) -> Vec < u8 > {
151+ assert_eq ! (
152+ hex. len( ) % 2 ,
153+ 0 ,
154+ "hex string must have an even number of characters"
155+ ) ;
156+ ( 0 ..hex. len ( ) )
157+ . step_by ( 2 )
158+ . map ( |i| {
159+ u8:: from_str_radix ( & hex[ i..i + 2 ] , 16 )
160+ . unwrap_or_else ( |e| panic ! ( "invalid hex at byte {i}: {e}" ) )
161+ } )
162+ . collect ( )
163+ }
164+
165+ fn display_hash_to_internal_bytes ( hex : & str ) -> Vec < u8 > {
166+ let mut bytes = decode_hex ( hex) ;
167+ bytes. reverse ( ) ;
168+ bytes
169+ }
170+
171+ pub fn create_mempool_self_transfer ( wallet : & str ) -> ( Vec < u8 > , Vec < u8 > , Vec < u8 > ) {
172+ let txid = match send_self_transfer ( wallet) {
173+ Ok ( txid) => txid,
174+ Err ( _) => {
175+ // If the wallet exists but is unfunded or in an unexpected state,
176+ // try to recover by ensuring funding and retry once.
177+ ensure_wallet_loaded_and_funded ( wallet) ;
178+ send_self_transfer ( wallet)
179+ . unwrap_or_else ( |e| panic ! ( "failed to create self-transfer in {wallet}: {e}" ) )
180+ }
181+ } ;
182+ let raw_tx_hex = bitcoin_rpc ( None , & [ "getrawtransaction" , txid. as_str ( ) ] )
183+ . unwrap_or_else ( |e| panic ! ( "failed to fetch raw transaction {txid}: {e}" ) ) ;
184+ let raw_tx = decode_hex ( & raw_tx_hex) ;
185+ let tx: Transaction = decode_from_slice ( & raw_tx)
186+ . unwrap_or_else ( |e| panic ! ( "failed to deserialize raw transaction {txid}: {e}" ) ) ;
187+ let txid_display = format ! ( "{:x}" , tx. compute_txid( ) ) ;
188+ assert_eq ! (
189+ txid_display, txid,
190+ "transaction id from raw tx should match RPC txid"
191+ ) ;
192+ let wtxid_display = format ! ( "{:x}" , tx. compute_wtxid( ) ) ;
193+ (
194+ display_hash_to_internal_bytes ( & txid_display) ,
195+ display_hash_to_internal_bytes ( & wtxid_display) ,
196+ raw_tx,
197+ )
198+ }
199+
28200pub async fn connect_unix_stream (
29201 path : impl AsRef < Path > ,
30202) -> VatNetwork < BufReader < Compat < OwnedReadHalf > > > {
@@ -55,6 +227,8 @@ pub async fn connect_unix_stream(
55227pub async fn bootstrap (
56228 mut rpc_system : RpcSystem < capnp_rpc:: rpc_twoparty_capnp:: Side > ,
57229) -> ( init:: Client , thread:: Client ) {
230+ ensure_bootstrap_chain_ready ( ) ;
231+
58232 let client: init:: Client = rpc_system. bootstrap ( Side :: Server ) ;
59233 tokio:: task:: spawn_local ( rpc_system) ;
60234 let create_client_response = client
@@ -91,9 +265,8 @@ pub async fn make_mining(init: &init::Client, thread: &thread::Client) -> mining
91265/// The node must have height > 16. At height <= 16 the BIP34 height push
92266/// is only one byte, which is shorter than the two-byte minimum scriptSig
93267/// required by consensus (see `CheckTransaction`), causing `createNewBlock`
94- /// to fail with `bad-cb-length`. Either generate blocks via bitcoin rpc
95- /// (`generatetodescriptor`) before running these tests, or (in a real miner)
96- /// pad the coinbase scriptSig with an extra push like `OP_0`.
268+ /// to fail with `bad-cb-length`. `bootstrap()` ensures chain height is at
269+ /// least 101 before tests run, which satisfies this precondition.
97270pub async fn make_block_template (
98271 mining : & mining:: Client ,
99272 thread : & thread:: Client ,
0 commit comments