• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacks-network / stacks-core / 30651892710-1

31 Jul 2026 05:37PM UTC coverage: 86.546% (-0.005%) from 86.551%
30651892710-1

push

github

web-flow
fix: bitcoind wallet setup for version  >= 31 (miner wallet resolution & startup hardening) (#7433)

Co-authored-by: francesco <francesco-stacks@users.noreply.github.com>

221 of 240 new or added lines in 6 files covered. (92.08%)

122 existing lines in 30 files now uncovered.

200557 of 231735 relevant lines covered (86.55%)

19257072.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.06
/stacks-node/src/burnchains/bitcoin_regtest_controller.rs
1
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2
// Copyright (C) 2020-2024 Stacks Open Internet Foundation
3
//
4
// This program is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// This program is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

17
use std::cmp;
18
use std::collections::HashSet;
19
use std::sync::atomic::{AtomicBool, Ordering};
20
use std::sync::Arc;
21
use std::time::Instant;
22

23
use stacks::burnchains::bitcoin::address::{
24
    BitcoinAddress, LegacyBitcoinAddress, LegacyBitcoinAddressType, SegwitBitcoinAddress,
25
};
26
use stacks::burnchains::bitcoin::indexer::{
27
    BitcoinIndexer, BitcoinIndexerConfig, BitcoinIndexerRuntime,
28
};
29
use stacks::burnchains::bitcoin::spv::SpvClient;
30
use stacks::burnchains::bitcoin::{BitcoinNetworkType, Error as btc_error};
31
use stacks::burnchains::db::BurnchainDB;
32
use stacks::burnchains::indexer::BurnchainIndexer;
33
use stacks::burnchains::{
34
    Burnchain, BurnchainParameters, BurnchainStateTransitionOps, Error as burnchain_error,
35
    PoxConstants, PublicKey, Txid,
36
};
37
use stacks::chainstate::burn::db::sortdb::SortitionDB;
38
use stacks::chainstate::burn::operations::{
39
    BlockstackOperationType, DelegateStxOp, LeaderBlockCommitOp, LeaderKeyRegisterOp, PreStxOp,
40
    StackStxOp, TransferStxOp, VoteForAggregateKeyOp,
41
};
42
#[cfg(test)]
43
use stacks::chainstate::burn::Opcodes;
44
use stacks::chainstate::coordinator::comm::CoordinatorChannels;
45
#[cfg(test)]
46
use stacks::chainstate::stacks::address::PoxAddress;
47
use stacks::config::BurnchainConfig;
48
#[cfg(test)]
49
use stacks::config::{
50
    OP_TX_ANY_ESTIM_SIZE, OP_TX_DELEGATE_STACKS_ESTIM_SIZE, OP_TX_PRE_STACKS_ESTIM_SIZE,
51
    OP_TX_STACK_STX_ESTIM_SIZE, OP_TX_TRANSFER_STACKS_ESTIM_SIZE, OP_TX_VOTE_AGG_ESTIM_SIZE,
52
};
53
use stacks::core::{EpochList, StacksEpochId};
54
use stacks::monitoring::{increment_btc_blocks_received_counter, increment_btc_ops_sent_counter};
55
use stacks_common::codec::StacksMessageCodec;
56
use stacks_common::deps_common::bitcoin::blockdata::opcodes;
57
use stacks_common::deps_common::bitcoin::blockdata::script::{Builder, Script};
58
use stacks_common::deps_common::bitcoin::blockdata::transaction::{
59
    OutPoint, Transaction, TxIn, TxOut,
60
};
61
use stacks_common::deps_common::bitcoin::network::serialize::{serialize, serialize_hex};
62
use stacks_common::deps_common::bitcoin::util::hash::Sha256dHash;
63
use stacks_common::types::chainstate::BurnchainHeaderHash;
64
use stacks_common::util::hash::{hex_bytes, Hash160};
65
use stacks_common::util::secp256k1::Secp256k1PublicKey;
66
use stacks_common::util::sleep_ms;
67

68
use super::super::operations::BurnchainOpSigner;
69
use super::super::Config;
70
use super::{BurnchainController, BurnchainTip, Error as BurnchainControllerError};
71
use crate::burnchains::rpc::bitcoin_rpc_client::{
72
    BitcoinRpcClient, BitcoinRpcClientError, BitcoinRpcClientResult, ImportDescriptorsRequest,
73
    Timestamp,
74
};
75
use crate::burnchains::rpc::rpc_transport::RpcError;
76

77
/// The number of bitcoin blocks that can have
78
///  passed since the UTXO cache was last refreshed before
79
///  the cache is force-reset.
80
const UTXO_CACHE_STALENESS_LIMIT: u64 = 6;
81
const DUST_UTXO_LIMIT: u64 = 5500;
82

83
#[cfg(test)]
84
// Used to inject invalid block commits during testing.
85
pub static TEST_MAGIC_BYTES: std::sync::Mutex<Option<[u8; 2]>> = std::sync::Mutex::new(None);
86

87
pub struct BitcoinRegtestController {
88
    config: Config,
89
    indexer: BitcoinIndexer,
90
    db: Option<SortitionDB>,
91
    burnchain_db: Option<BurnchainDB>,
92
    chain_tip: Option<BurnchainTip>,
93
    use_coordinator: Option<CoordinatorChannels>,
94
    burnchain_config: Option<Burnchain>,
95
    ongoing_block_commit: Option<OngoingBlockCommit>,
96
    should_keep_running: Option<Arc<AtomicBool>>,
97
    /// Optional Bitcoin RPC client used to interact with a `bitcoind` node.
98
    /// - For **miner** node this field must be always `Some`.
99
    /// - For **other** node (e.g. follower node), this field is `None`.
100
    rpc_client: Option<BitcoinRpcClient>,
101
}
102

103
#[derive(Clone)]
104
pub struct OngoingBlockCommit {
105
    pub payload: LeaderBlockCommitOp,
106
    utxos: UTXOSet,
107
    fees: LeaderBlockCommitFees,
108
    txids: Vec<Txid>,
109
}
110

111
#[derive(Clone)]
112
struct LeaderBlockCommitFees {
113
    sunset_fee: u64,
114
    fee_rate: u64,
115
    sortition_fee: u64,
116
    outputs_len: u64,
117
    default_tx_size: u64,
118
    spent_in_attempts: u64,
119
    is_rbf_enabled: bool,
120
    final_size: u64,
121
}
122

123
// TODO: add tests from mutation testing results #4862
124
#[cfg_attr(test, mutants::skip)]
125
pub fn burnchain_params_from_config(config: &BurnchainConfig) -> BurnchainParameters {
241,671✔
126
    let (network, _) = config.get_bitcoin_network();
241,671✔
127
    let mut params = BurnchainParameters::from_params(&config.chain, &network)
241,671✔
128
        .expect("Bitcoin network unsupported");
241,671✔
129
    if let Some(first_burn_block_height) = config.first_burn_block_height {
241,671✔
130
        params.first_block_height = first_burn_block_height;
×
131
    }
241,671✔
132
    params
241,671✔
133
}
241,671✔
134

135
// TODO: add tests from mutation testing results #4863
136
#[cfg_attr(test, mutants::skip)]
137
/// Helper method to create a BitcoinIndexer
138
pub fn make_bitcoin_indexer(
1,373✔
139
    config: &Config,
1,373✔
140
    should_keep_running: Option<Arc<AtomicBool>>,
1,373✔
141
) -> BitcoinIndexer {
1,373✔
142
    let burnchain_params = burnchain_params_from_config(&config.burnchain);
1,373✔
143
    let indexer_config = {
1,373✔
144
        let burnchain_config = config.burnchain.clone();
1,373✔
145
        BitcoinIndexerConfig {
1,373✔
146
            peer_host: burnchain_config.peer_host,
1,373✔
147
            peer_port: burnchain_config.peer_port,
1,373✔
148
            rpc_port: burnchain_config.rpc_port,
1,373✔
149
            rpc_ssl: burnchain_config.rpc_ssl,
1,373✔
150
            username: burnchain_config.username,
1,373✔
151
            password: burnchain_config.password,
1,373✔
152
            timeout: burnchain_config.timeout,
1,373✔
153
            socket_timeout: burnchain_config.socket_timeout,
1,373✔
154
            spv_headers_path: config.get_spv_headers_file_path(),
1,373✔
155
            first_block: burnchain_params.first_block_height,
1,373✔
156
            magic_bytes: burnchain_config.magic_bytes,
1,373✔
157
            epochs: burnchain_config.epochs,
1,373✔
158
        }
1,373✔
159
    };
160

161
    let (_, network_type) = config.burnchain.get_bitcoin_network();
1,373✔
162
    let indexer_runtime = BitcoinIndexerRuntime::new(network_type, indexer_config.timeout);
1,373✔
163
    BitcoinIndexer {
1,373✔
164
        config: indexer_config,
1,373✔
165
        runtime: indexer_runtime,
1,373✔
166
        should_keep_running,
1,373✔
167
    }
1,373✔
168
}
1,373✔
169

170
pub fn get_satoshis_per_byte(config: &Config) -> u64 {
25,369✔
171
    config.get_burnchain_config().satoshis_per_byte
25,369✔
172
}
25,369✔
173

174
pub fn get_rbf_fee_increment(config: &Config) -> u64 {
603✔
175
    config.get_burnchain_config().rbf_fee_increment
603✔
176
}
603✔
177

178
pub fn get_max_rbf(config: &Config) -> u64 {
14,144✔
179
    config.get_burnchain_config().max_rbf
14,144✔
180
}
14,144✔
181

182
impl LeaderBlockCommitFees {
183
    pub fn fees_from_previous_tx(
603✔
184
        &self,
603✔
185
        payload: &LeaderBlockCommitOp,
603✔
186
        config: &Config,
603✔
187
    ) -> LeaderBlockCommitFees {
603✔
188
        let mut fees = LeaderBlockCommitFees::estimated_fees_from_payload(payload, config);
603✔
189
        fees.spent_in_attempts = cmp::max(1, self.spent_in_attempts);
603✔
190
        fees.final_size = self.final_size;
603✔
191
        fees.fee_rate = self.fee_rate + get_rbf_fee_increment(config);
603✔
192
        fees.is_rbf_enabled = true;
603✔
193
        fees
603✔
194
    }
603✔
195

196
    pub fn estimated_fees_from_payload(
10,588✔
197
        payload: &LeaderBlockCommitOp,
10,588✔
198
        config: &Config,
10,588✔
199
    ) -> LeaderBlockCommitFees {
10,588✔
200
        let sunset_fee = if payload.sunset_burn > 0 {
10,588✔
201
            cmp::max(payload.sunset_burn, DUST_UTXO_LIMIT)
21✔
202
        } else {
203
            0
10,567✔
204
        };
205

206
        let number_of_transfers = payload.commit_outs.len() as u64;
10,588✔
207
        let value_per_transfer = payload.burn_fee / number_of_transfers;
10,588✔
208
        let sortition_fee = value_per_transfer * number_of_transfers;
10,588✔
209
        let spent_in_attempts = 0;
10,588✔
210
        let fee_rate = get_satoshis_per_byte(config);
10,588✔
211
        let default_tx_size = config.burnchain.block_commit_tx_estimated_size;
10,588✔
212

213
        LeaderBlockCommitFees {
10,588✔
214
            sunset_fee,
10,588✔
215
            fee_rate,
10,588✔
216
            sortition_fee,
10,588✔
217
            outputs_len: number_of_transfers,
10,588✔
218
            default_tx_size,
10,588✔
219
            spent_in_attempts,
10,588✔
220
            is_rbf_enabled: false,
10,588✔
221
            final_size: 0,
10,588✔
222
        }
10,588✔
223
    }
10,588✔
224

225
    pub fn estimated_miner_fee(&self) -> u64 {
10,589✔
226
        self.fee_rate * self.default_tx_size
10,589✔
227
    }
10,589✔
228

229
    pub fn rbf_fee(&self) -> u64 {
10,589✔
230
        if self.is_rbf_enabled {
10,589✔
231
            self.spent_in_attempts + self.default_tx_size
603✔
232
        } else {
233
            0
9,986✔
234
        }
235
    }
10,589✔
236

237
    pub fn estimated_amount_required(&self) -> u64 {
10,589✔
238
        self.estimated_miner_fee() + self.rbf_fee() + self.sunset_fee + self.sortition_fee
10,589✔
239
    }
10,589✔
240

241
    pub fn total_spent(&self) -> u64 {
9,609✔
242
        self.fee_rate * self.final_size
9,609✔
243
            + self.spent_in_attempts
9,609✔
244
            + self.sunset_fee
9,609✔
245
            + self.sortition_fee
9,609✔
246
    }
9,609✔
247

248
    pub fn amount_per_output(&self) -> u64 {
16,878✔
249
        self.sortition_fee / self.outputs_len
16,878✔
250
    }
16,878✔
251

252
    pub fn total_spent_in_outputs(&self) -> u64 {
9,608✔
253
        self.sunset_fee + self.sortition_fee
9,608✔
254
    }
9,608✔
255

256
    pub fn min_tx_size(&self) -> u64 {
9,608✔
257
        cmp::max(self.final_size, self.default_tx_size)
9,608✔
258
    }
9,608✔
259

260
    pub fn register_replacement(&mut self, tx_size: u64) {
9,608✔
261
        let new_size = cmp::max(tx_size, self.final_size);
9,608✔
262
        if self.is_rbf_enabled {
9,608✔
263
            self.spent_in_attempts += new_size;
603✔
264
        }
9,005✔
265
        self.final_size = new_size;
9,608✔
266
    }
9,608✔
267
}
268

269
/// Extension methods for working with [`BitcoinRpcClient`] result
270
/// that log failures and panic.
271
#[cfg(test)]
272
trait BitcoinRpcClientResultExt<T> {
273
    /// Unwraps the result, returning the value if `Ok`.
274
    ///
275
    /// If the result is an `Err`, it logs the error with the given context
276
    /// using the [`error!`] macro and then panics.
277
    fn unwrap_or_log_panic(self, context: &str) -> T;
278
    /// Ensure the result is `Ok`, ignoring its value.
279
    ///
280
    /// If the result is an `Err`, it logs the error with the given context
281
    /// using the [`error!`] macro and then panics.
282
    fn ok_or_log_panic(self, context: &str);
283
}
284

285
#[cfg(test)]
286
impl<T> BitcoinRpcClientResultExt<T> for Result<T, BitcoinRpcClientError> {
287
    fn unwrap_or_log_panic(self, context: &str) -> T {
8,310✔
288
        match self {
8,310✔
289
            Ok(val) => val,
8,310✔
290
            Err(e) => {
×
291
                error!("Bitcoin RPC failure: {context} {e:?}");
×
292
                panic!();
×
293
            }
294
        }
295
    }
8,310✔
296

297
    fn ok_or_log_panic(self, context: &str) {
8,137✔
298
        _ = self.unwrap_or_log_panic(context);
8,137✔
299
    }
8,137✔
300
}
301

302
/// Represents errors that can occur when using [`BitcoinRegtestController`].
303
#[derive(Debug, thiserror::Error)]
304
pub enum BitcoinRegtestControllerError {
305
    /// Error related to Bitcoin RPC failures.
306
    #[error("Bitcoin RPC error: {0}")]
307
    Rpc(#[from] BitcoinRpcClientError),
308
    /// Error related to invalid or malformed [`Secp256k1PublicKey`].
309
    #[error("Invalid public key: {0}")]
310
    InvalidPublicKey(btc_error),
311
    /// A descriptor import was rejected by the bitcoin node.
312
    #[error("Importing descriptor failed: {0}")]
313
    ImportDescriptors(String),
314
    /// The configured mining wallet does not exist in the bitcoin node's wallet directory.
315
    #[error("Configured bitcoin wallet `{0}` was not found; create or restore it before starting the miner")]
316
    WalletNotFound(String),
317
}
318

319
/// Alias for results returned from [`BitcoinRegtestController`] operations.
320
pub type BitcoinRegtestControllerResult<T> = Result<T, BitcoinRegtestControllerError>;
321

322
impl BitcoinRegtestControllerError {
323
    /// Whether retrying could plausibly succeed, i.e. bitcoind is not reachable
324
    /// yet. Only connection-level failures clear on their own: a rejection from
325
    /// bitcoind, or a serialization failure, will fail again identically.
NEW
326
    fn is_transient(&self) -> bool {
×
NEW
327
        matches!(
×
NEW
328
            self,
×
329
            Self::Rpc(BitcoinRpcClientError::Rpc(
330
                RpcError::NetworkIO(_)
331
                    | RpcError::NetworkStacksLib(_)
332
                    | RpcError::NetworkStacksCommon(_)
333
            ))
334
        )
NEW
335
    }
×
336
}
337

338
impl BitcoinRegtestController {
339
    pub fn new(config: Config, coordinator_channel: Option<CoordinatorChannels>) -> Self {
289✔
340
        BitcoinRegtestController::with_burnchain(config, coordinator_channel, None, None)
289✔
341
    }
289✔
342

343
    // TODO: add tests from mutation testing results #4864
344
    #[cfg_attr(test, mutants::skip)]
345
    pub fn with_burnchain(
864✔
346
        config: Config,
864✔
347
        coordinator_channel: Option<CoordinatorChannels>,
864✔
348
        burnchain: Option<Burnchain>,
864✔
349
        should_keep_running: Option<Arc<AtomicBool>>,
864✔
350
    ) -> Self {
864✔
351
        std::fs::create_dir_all(config.get_burnchain_path_str()).expect("Unable to create workdir");
864✔
352
        let (_, network_id) = config.burnchain.get_bitcoin_network();
864✔
353

354
        let res = SpvClient::new(
864✔
355
            &config.get_spv_headers_file_path(),
864✔
356
            0,
357
            None,
864✔
358
            network_id,
864✔
359
            true,
360
            false,
361
        );
362
        if let Err(err) = res {
864✔
363
            error!("Unable to init block headers: {err}");
×
364
            panic!()
×
365
        }
864✔
366

367
        let burnchain_params = burnchain_params_from_config(&config.burnchain);
864✔
368

369
        if network_id == BitcoinNetworkType::Mainnet && config.burnchain.epochs.is_some() {
864✔
370
            panic!("It is an error to set custom epochs while running on Mainnet: network_id {network_id:?} config.burnchain {:#?}",
×
371
                   &config.burnchain);
×
372
        }
864✔
373

374
        let indexer_config = {
864✔
375
            let burnchain_config = config.burnchain.clone();
864✔
376
            BitcoinIndexerConfig {
864✔
377
                peer_host: burnchain_config.peer_host,
864✔
378
                peer_port: burnchain_config.peer_port,
864✔
379
                rpc_port: burnchain_config.rpc_port,
864✔
380
                rpc_ssl: burnchain_config.rpc_ssl,
864✔
381
                username: burnchain_config.username,
864✔
382
                password: burnchain_config.password,
864✔
383
                timeout: burnchain_config.timeout,
864✔
384
                socket_timeout: burnchain_config.socket_timeout,
864✔
385
                spv_headers_path: config.get_spv_headers_file_path(),
864✔
386
                first_block: burnchain_params.first_block_height,
864✔
387
                magic_bytes: burnchain_config.magic_bytes,
864✔
388
                epochs: burnchain_config.epochs,
864✔
389
            }
864✔
390
        };
391

392
        let (_, network_type) = config.burnchain.get_bitcoin_network();
864✔
393
        let indexer_runtime = BitcoinIndexerRuntime::new(network_type, config.burnchain.timeout);
864✔
394
        let burnchain_indexer = BitcoinIndexer {
864✔
395
            config: indexer_config,
864✔
396
            runtime: indexer_runtime,
864✔
397
            should_keep_running: should_keep_running.clone(),
864✔
398
        };
864✔
399

400
        let rpc_client = Self::create_rpc_client_unchecked(&config);
864✔
401

402
        Self {
864✔
403
            use_coordinator: coordinator_channel,
864✔
404
            config,
864✔
405
            indexer: burnchain_indexer,
864✔
406
            db: None,
864✔
407
            burnchain_db: None,
864✔
408
            chain_tip: None,
864✔
409
            burnchain_config: burnchain,
864✔
410
            ongoing_block_commit: None,
864✔
411
            should_keep_running,
864✔
412
            rpc_client,
864✔
413
        }
864✔
414
    }
864✔
415

416
    // TODO: add tests from mutation testing results #4864
417
    #[cfg_attr(test, mutants::skip)]
418
    /// create a dummy bitcoin regtest controller.
419
    ///   used just for submitting bitcoin ops.
420
    pub fn new_dummy(config: Config) -> Self {
238,977✔
421
        let burnchain_params = burnchain_params_from_config(&config.burnchain);
238,977✔
422

423
        let indexer_config = {
238,977✔
424
            let burnchain_config = config.burnchain.clone();
238,977✔
425
            BitcoinIndexerConfig {
238,977✔
426
                peer_host: burnchain_config.peer_host,
238,977✔
427
                peer_port: burnchain_config.peer_port,
238,977✔
428
                rpc_port: burnchain_config.rpc_port,
238,977✔
429
                rpc_ssl: burnchain_config.rpc_ssl,
238,977✔
430
                username: burnchain_config.username,
238,977✔
431
                password: burnchain_config.password,
238,977✔
432
                timeout: burnchain_config.timeout,
238,977✔
433
                socket_timeout: burnchain_config.socket_timeout,
238,977✔
434
                spv_headers_path: config.get_spv_headers_file_path(),
238,977✔
435
                first_block: burnchain_params.first_block_height,
238,977✔
436
                magic_bytes: burnchain_config.magic_bytes,
238,977✔
437
                epochs: burnchain_config.epochs,
238,977✔
438
            }
238,977✔
439
        };
440

441
        let (_, network_type) = config.burnchain.get_bitcoin_network();
238,977✔
442
        let indexer_runtime = BitcoinIndexerRuntime::new(network_type, config.burnchain.timeout);
238,977✔
443
        let burnchain_indexer = BitcoinIndexer {
238,977✔
444
            config: indexer_config,
238,977✔
445
            runtime: indexer_runtime,
238,977✔
446
            should_keep_running: None,
238,977✔
447
        };
238,977✔
448

449
        let rpc_client = Self::create_rpc_client_unchecked(&config);
238,977✔
450

451
        Self {
238,977✔
452
            use_coordinator: None,
238,977✔
453
            config,
238,977✔
454
            indexer: burnchain_indexer,
238,977✔
455
            db: None,
238,977✔
456
            burnchain_db: None,
238,977✔
457
            chain_tip: None,
238,977✔
458
            burnchain_config: None,
238,977✔
459
            ongoing_block_commit: None,
238,977✔
460
            should_keep_running: None,
238,977✔
461
            rpc_client,
238,977✔
462
        }
238,977✔
463
    }
238,977✔
464

465
    /// Creates a dummy bitcoin regtest controller, with the given ongoing block-commits
466
    pub fn new_ongoing_dummy(config: Config, ongoing: Option<OngoingBlockCommit>) -> Self {
238,414✔
467
        let mut ret = Self::new_dummy(config);
238,414✔
468
        ret.ongoing_block_commit = ongoing;
238,414✔
469
        ret
238,414✔
470
    }
238,414✔
471

472
    /// Get an owned copy of the ongoing block commit state
473
    pub fn get_ongoing_commit(&self) -> Option<OngoingBlockCommit> {
245,683✔
474
        self.ongoing_block_commit.clone()
245,683✔
475
    }
245,683✔
476

477
    /// Set the ongoing block commit state
478
    pub fn set_ongoing_commit(&mut self, ongoing: Option<OngoingBlockCommit>) {
7,263✔
479
        self.ongoing_block_commit = ongoing;
7,263✔
480
    }
7,263✔
481

482
    /// Get the default Burnchain instance from our config
483
    fn default_burnchain(&self) -> Burnchain {
1,026,129✔
484
        match &self.burnchain_config {
1,026,129✔
485
            Some(burnchain) => burnchain.clone(),
×
486
            None => self.config.get_burnchain(),
1,026,129✔
487
        }
488
    }
1,026,129✔
489

490
    /// Get the PoX constants in use
491
    pub fn get_pox_constants(&self) -> PoxConstants {
×
492
        let burnchain = self.get_burnchain();
×
493
        burnchain.pox_constants
×
494
    }
×
495

496
    /// Get the Burnchain in use
497
    pub fn get_burnchain(&self) -> Burnchain {
1,083,223✔
498
        match self.burnchain_config {
1,083,223✔
499
            Some(ref burnchain) => burnchain.clone(),
57,094✔
500
            None => self.default_burnchain(),
1,026,129✔
501
        }
502
    }
1,083,223✔
503

504
    /// Attempt to create a new [`BitcoinRpcClient`] from the given [`Config`].
505
    ///
506
    /// If the provided config indicates that the node is a **miner**,
507
    /// tries to instantiate it or **panics** otherwise.
508
    /// If the node is **not** a miner, returns None (e.g. follower node).
509
    fn create_rpc_client_unchecked(config: &Config) -> Option<BitcoinRpcClient> {
239,841✔
510
        config.node.miner.then(|| {
239,841✔
511
            BitcoinRpcClient::from_stx_config(&config)
239,820✔
512
                .expect("unable to instantiate the RPC client for miner node!")
239,820✔
513
        })
239,820✔
514
    }
239,841✔
515

516
    /// Attempt to get a reference to the underlying [`BitcoinRpcClient`].
517
    ///
518
    /// This function will panic if the RPC client has not been configured
519
    /// (i.e. [`Self::create_rpc_client_unchecked`] returned `None` during initialization),
520
    /// but an attempt is made to use it anyway.
521
    ///
522
    /// In practice, this means the node is expected to act as a miner,
523
    /// yet no [`BitcoinRpcClient`] was created or properly configured.
524
    fn get_rpc_client(&self) -> &BitcoinRpcClient {
65,876✔
525
        self.rpc_client
65,876✔
526
            .as_ref()
65,876✔
527
            .expect("BUG: BitcoinRpcClient is required, but it has not been configured properly!")
65,876✔
528
    }
65,876✔
529

530
    /// Helium (devnet) blocks receiver.  Returns the new burnchain tip.
531
    fn receive_blocks_helium(&mut self) -> BurnchainTip {
×
532
        let mut burnchain = self.get_burnchain();
×
533
        let (block_snapshot, state_transition) = loop {
×
534
            match burnchain.sync_with_indexer_deprecated(&mut self.indexer) {
×
535
                Ok(x) => {
×
536
                    break x;
×
537
                }
538
                Err(e) => {
×
539
                    // keep trying
540
                    error!("Unable to sync with burnchain: {e}");
×
541
                    match e {
×
542
                        burnchain_error::TrySyncAgain => {
543
                            // try again immediately
544
                            continue;
×
545
                        }
546
                        burnchain_error::BurnchainPeerBroken => {
547
                            // remote burnchain peer broke, and produced a shorter blockchain fork.
548
                            // just keep trying
549
                            sleep_ms(5000);
×
550
                            continue;
×
551
                        }
552
                        _ => {
553
                            // delay and try again
554
                            sleep_ms(5000);
×
555
                            continue;
×
556
                        }
557
                    }
558
                }
559
            }
560
        };
561

562
        let rest = match (state_transition, &self.chain_tip) {
×
563
            (None, Some(chain_tip)) => chain_tip.clone(),
×
564
            (Some(state_transition), _) => {
×
565
                let burnchain_tip = BurnchainTip {
×
566
                    block_snapshot,
×
567
                    state_transition: BurnchainStateTransitionOps::from(state_transition),
×
568
                    received_at: Instant::now(),
×
569
                };
×
570
                self.chain_tip = Some(burnchain_tip.clone());
×
571
                burnchain_tip
×
572
            }
573
            (None, None) => {
574
                // can happen at genesis
575
                let burnchain_tip = BurnchainTip {
×
576
                    block_snapshot,
×
577
                    state_transition: BurnchainStateTransitionOps::noop(),
×
578
                    received_at: Instant::now(),
×
579
                };
×
580
                self.chain_tip = Some(burnchain_tip.clone());
×
581
                burnchain_tip
×
582
            }
583
        };
584

585
        debug!("Done receiving blocks");
×
586
        rest
×
587
    }
×
588

589
    fn receive_blocks(
485,898✔
590
        &mut self,
485,898✔
591
        block_for_sortitions: bool,
485,898✔
592
        target_block_height_opt: Option<u64>,
485,898✔
593
    ) -> Result<(BurnchainTip, u64), BurnchainControllerError> {
485,898✔
594
        let coordinator_comms = match self.use_coordinator.as_ref() {
485,898✔
595
            Some(x) => x.clone(),
485,898✔
596
            None => {
597
                // pre-PoX helium node
598
                let tip = self.receive_blocks_helium();
×
599
                let height = tip.block_snapshot.block_height;
×
600
                return Ok((tip, height));
×
601
            }
602
        };
603

604
        let mut burnchain = self.get_burnchain();
485,898✔
605
        let (block_snapshot, burnchain_height, state_transition) = loop {
485,835✔
606
            if !self.should_keep_running() {
485,973✔
607
                return Err(BurnchainControllerError::CoordinatorClosed);
59✔
608
            }
485,914✔
609

610
            match burnchain.sync_with_indexer(
485,914✔
611
                &mut self.indexer,
485,914✔
612
                coordinator_comms.clone(),
485,914✔
613
                target_block_height_opt,
485,914✔
614
                Some(burnchain.pox_constants.reward_cycle_length as u64),
485,914✔
615
                self.should_keep_running.clone(),
485,914✔
616
            ) {
485,914✔
617
                Ok(x) => {
485,836✔
618
                    increment_btc_blocks_received_counter();
485,836✔
619

620
                    // initialize the dbs...
621
                    self.sortdb_mut();
485,836✔
622

623
                    // wait for the chains coordinator to catch up with us.
624
                    // don't wait for heights beyond the burnchain tip.
625
                    if block_for_sortitions {
485,836✔
626
                        self.wait_for_sortitions(
485,239✔
627
                            coordinator_comms,
485,239✔
628
                            target_block_height_opt.unwrap_or(x.block_height),
485,239✔
629
                        )?;
1✔
630
                    }
597✔
631

632
                    // NOTE: This is the latest _sortition_ on the canonical sortition history, not the latest burnchain block!
633
                    let sort_tip =
485,835✔
634
                        SortitionDB::get_canonical_burn_chain_tip(self.sortdb_ref().conn())
485,835✔
635
                            .expect("Sortition DB error.");
485,835✔
636

637
                    let (snapshot, state_transition) = self
485,835✔
638
                        .sortdb_ref()
485,835✔
639
                        .get_sortition_result(&sort_tip.sortition_id)
485,835✔
640
                        .expect("Sortition DB error.")
485,835✔
641
                        .expect("BUG: no data for the canonical chain tip");
485,835✔
642

643
                    let burnchain_height = self
485,835✔
644
                        .indexer
485,835✔
645
                        .get_highest_header_height()
485,835✔
646
                        .map_err(BurnchainControllerError::IndexerError)?;
485,835✔
647
                    break (snapshot, burnchain_height, state_transition);
485,835✔
648
                }
649
                Err(e) => {
78✔
650
                    // keep trying
651
                    error!("Unable to sync with burnchain: {e}");
78✔
652
                    match e {
78✔
653
                        burnchain_error::CoordinatorClosed => {
654
                            return Err(BurnchainControllerError::CoordinatorClosed)
3✔
655
                        }
656
                        burnchain_error::TrySyncAgain => {
657
                            // try again immediately
658
                            continue;
60✔
659
                        }
660
                        burnchain_error::BurnchainPeerBroken => {
661
                            // remote burnchain peer broke, and produced a shorter blockchain fork.
662
                            // just keep trying
663
                            sleep_ms(5000);
3✔
664
                            continue;
3✔
665
                        }
666
                        _ => {
667
                            // delay and try again
668
                            sleep_ms(5000);
17✔
669
                            continue;
17✔
670
                        }
671
                    }
672
                }
673
            }
674
        };
675

676
        let burnchain_tip = BurnchainTip {
485,835✔
677
            block_snapshot,
485,835✔
678
            state_transition,
485,835✔
679
            received_at: Instant::now(),
485,835✔
680
        };
485,835✔
681

682
        let received = self
485,835✔
683
            .chain_tip
485,835✔
684
            .as_ref()
485,835✔
685
            .map(|tip| tip.block_snapshot.block_height)
485,835✔
686
            .unwrap_or(0)
485,835✔
687
            == burnchain_tip.block_snapshot.block_height;
485,835✔
688
        self.chain_tip = Some(burnchain_tip.clone());
485,835✔
689
        debug!("Done receiving blocks");
485,835✔
690

691
        if self.config.burnchain.fault_injection_burnchain_block_delay > 0 && received {
485,835✔
692
            info!(
×
693
                "Fault injection: delaying burnchain blocks by {} milliseconds",
694
                self.config.burnchain.fault_injection_burnchain_block_delay
695
            );
696
            sleep_ms(self.config.burnchain.fault_injection_burnchain_block_delay);
×
697
        }
485,835✔
698

699
        Ok((burnchain_tip, burnchain_height))
485,835✔
700
    }
485,898✔
701

702
    fn should_keep_running(&self) -> bool {
493,552✔
703
        match self.should_keep_running {
493,552✔
704
            Some(ref should_keep_running) => should_keep_running.load(Ordering::SeqCst),
493,552✔
705
            _ => true,
×
706
        }
707
    }
493,552✔
708

709
    /// Retrieves all UTXOs associated with the given public key.
710
    ///
711
    /// The address to query is computed from the public key,
712
    /// disregard the epoch we're in and currently set to [`StacksEpochId::Epoch21`].
713
    ///
714
    /// Automatically imports descriptors into the wallet for the public_key
715
    #[cfg(test)]
716
    pub fn get_all_utxos(&self, public_key: &Secp256k1PublicKey) -> Vec<UTXO> {
77✔
717
        const EPOCH: StacksEpochId = StacksEpochId::Epoch21;
718
        let address = self.get_miner_address(EPOCH, public_key);
77✔
719
        let pub_key_rev = self.to_epoch_aware_pubkey(EPOCH, public_key);
77✔
720

721
        test_debug!("Import public key '{}'", &pub_key_rev.to_hex());
77✔
722
        self.import_public_key(&pub_key_rev)
77✔
723
            .unwrap_or_else(|error| {
77✔
724
                panic!(
×
725
                    "Import public key '{}' failed: {error:?}",
726
                    pub_key_rev.to_hex()
×
727
                )
728
            });
729

730
        sleep_ms(1000);
77✔
731

732
        self.retrieve_utxo_set(&address, true, 1, &None, 0)
77✔
733
            .unwrap_or_log_panic("retrieve all utxos")
77✔
734
            .utxos
77✔
735
    }
77✔
736

737
    /// Retrieve all loaded wallets.
738
    pub fn list_wallets(&self) -> BitcoinRegtestControllerResult<Vec<String>> {
834✔
739
        Ok(self.get_rpc_client().list_wallets()?)
834✔
740
    }
834✔
741

742
    /// Ensures the configured wallet exists and is loaded in the connected
743
    /// bitcoin node, loading it from disk when needed.
744
    ///
745
    /// Operators who want the wallet to survive a bitcoind restart set
746
    /// `wallet=<name>` in `bitcoin.conf`.
747
    pub fn ensure_wallet_loaded(&self) -> BitcoinRegtestControllerResult<()> {
832✔
748
        let wallet_name = self.get_wallet_name();
832✔
749

750
        if self.list_wallets()?.iter().any(|name| name == wallet_name) {
832✔
751
            return Ok(());
547✔
752
        }
285✔
753

754
        let on_disk_wallets = self.get_rpc_client().list_wallet_dir()?;
285✔
755
        if on_disk_wallets.iter().any(|name| name == wallet_name) {
285✔
756
            self.get_rpc_client().load_wallet(wallet_name)?;
1✔
757
        } else {
758
            return Err(BitcoinRegtestControllerError::WalletNotFound(
284✔
759
                wallet_name.to_string(),
284✔
760
            ));
284✔
761
        }
762
        Ok(())
1✔
763
    }
832✔
764

765
    /// Block until the miner's bitcoin wallet is loaded, retrying while
766
    /// bitcoind may still be starting up. Fatal on misconfiguration or timeout.
767
    pub fn ensure_miner_wallet_loaded(&self) {
542✔
768
        /// Milliseconds to wait between wallet load attempts during startup
769
        const WALLET_LOAD_INTERVAL_MS: u64 = 10_000;
770
        /// Total wallet load attempts before giving up on bitcoind
771
        const WALLET_LOAD_ATTEMPTS: u64 = 6;
772

773
        let mut last_error = String::from("none recorded");
542✔
774
        for attempt in 1..=WALLET_LOAD_ATTEMPTS {
542✔
775
            match self.ensure_wallet_loaded() {
542✔
776
                Ok(()) => return,
542✔
NEW
777
                Err(e) if e.is_transient() => {
×
NEW
778
                    warn!("Error ensuring bitcoin wallet is loaded, will retry: {e:?}");
×
NEW
779
                    last_error = e.to_string();
×
780
                }
781
                // misconfiguration or a bug: retrying cannot fix it
NEW
782
                Err(e) => panic!("FATAL: {e}"),
×
783
            }
NEW
784
            if attempt < WALLET_LOAD_ATTEMPTS {
×
NEW
785
                sleep_ms(WALLET_LOAD_INTERVAL_MS);
×
NEW
786
            }
×
787
        }
NEW
788
        panic!(
×
789
            "FATAL: unable to load a bitcoin wallet after {WALLET_LOAD_ATTEMPTS} attempts, \
790
             exiting. Last error: {last_error}"
791
        );
792
    }
542✔
793

794
    /// Creates the configured test wallet when absent, then ensures it is
795
    /// loaded. Production miners must provision their wallet before startup.
796
    #[cfg(test)]
797
    fn ensure_test_wallet_loaded(&self) -> BitcoinRegtestControllerResult<()> {
286✔
798
        match self.ensure_wallet_loaded() {
286✔
799
            Err(BitcoinRegtestControllerError::WalletNotFound(_)) => {
800
                self.get_rpc_client()
282✔
801
                    .create_wallet(self.get_wallet_name(), Some(true))?;
282✔
802
                Ok(())
282✔
803
            }
804
            result => result,
4✔
805
        }
806
    }
286✔
807

808
    pub fn get_utxos(
9,885✔
809
        &self,
9,885✔
810
        epoch_id: StacksEpochId,
9,885✔
811
        public_key: &Secp256k1PublicKey,
9,885✔
812
        total_required: u64,
9,885✔
813
        utxos_to_exclude: Option<UTXOSet>,
9,885✔
814
        block_height: u64,
9,885✔
815
    ) -> Option<UTXOSet> {
9,885✔
816
        let pub_key_rev = self.to_epoch_aware_pubkey(epoch_id, public_key);
9,885✔
817

818
        // Configure UTXO filter
819
        let address = self.get_miner_address(epoch_id, &pub_key_rev);
9,885✔
820
        test_debug!("Get UTXOs for {} ({address})", pub_key_rev.to_hex());
9,885✔
821

822
        let mut utxos = loop {
9,885✔
823
            let result = self.retrieve_utxo_set(
9,914✔
824
                &address,
9,914✔
825
                false,
826
                total_required,
9,914✔
827
                &utxos_to_exclude,
9,914✔
828
                block_height,
9,914✔
829
            );
830

831
            // Perform request
832
            match result {
9,914✔
833
                Ok(utxos) => {
9,885✔
834
                    break utxos;
9,885✔
835
                }
836
                Err(e) => {
29✔
837
                    error!("Bitcoin RPC failure: error listing utxos {e:?}");
29✔
838
                    sleep_ms(5000);
29✔
839
                    continue;
29✔
840
                }
841
            };
842
        };
843

844
        let utxos = if utxos.is_empty() {
9,885✔
845
            let (_, network) = self.config.burnchain.get_bitcoin_network();
25✔
846
            loop {
847
                if let BitcoinNetworkType::Regtest = network {
25✔
848
                    // Performing this operation on Mainnet / Testnet is very expensive, and can be longer than bitcoin block time.
849
                    // Assuming that miners are in charge of correctly operating their bitcoind nodes sounds
850
                    // reasonable to me.
851
                    // $ bitcoin-cli importaddress mxVFsFW5N4mu1HPkxPttorvocvzeZ7KZyk
852
                    let result = self.import_public_key(&pub_key_rev);
25✔
853
                    if let Err(error) = result {
25✔
854
                        warn!(
×
855
                            "Import public key '{}' failed: {error:?}",
856
                            &pub_key_rev.to_hex()
×
857
                        );
858
                    }
25✔
859
                    sleep_ms(1000);
25✔
860
                }
×
861

862
                let result = self.retrieve_utxo_set(
25✔
863
                    &address,
25✔
864
                    false,
865
                    total_required,
25✔
866
                    &utxos_to_exclude,
25✔
867
                    block_height,
25✔
868
                );
869

870
                utxos = match result {
25✔
871
                    Ok(utxos) => utxos,
25✔
872
                    Err(e) => {
×
873
                        error!("Bitcoin RPC failure: error listing utxos {e:?}");
×
874
                        sleep_ms(5000);
×
875
                        continue;
×
876
                    }
877
                };
878

879
                test_debug!("Unspent for {address:?}: {utxos:?}");
25✔
880

881
                if utxos.is_empty() {
25✔
882
                    return None;
14✔
883
                } else {
884
                    break utxos;
11✔
885
                }
886
            }
887
        } else {
888
            debug!("Got {} UTXOs for {address:?}", utxos.utxos.len(),);
9,860✔
889
            utxos
9,860✔
890
        };
891

892
        let total_unspent = utxos.total_available();
9,871✔
893
        if total_unspent < total_required {
9,871✔
894
            warn!(
×
895
                "Total unspent {total_unspent} < {total_required} for {:?}",
896
                &pub_key_rev.to_hex()
×
897
            );
898
            return None;
×
899
        }
9,871✔
900

901
        Some(utxos)
9,871✔
902
    }
9,885✔
903

904
    fn build_leader_key_register_tx(
290✔
905
        &mut self,
290✔
906
        epoch_id: StacksEpochId,
290✔
907
        payload: LeaderKeyRegisterOp,
290✔
908
        signer: &mut BurnchainOpSigner,
290✔
909
    ) -> Result<Transaction, BurnchainControllerError> {
290✔
910
        let public_key = signer.get_public_key();
290✔
911

912
        // reload the config to find satoshis_per_byte changes
913
        let btc_miner_fee = self.config.burnchain.leader_key_tx_estimated_size
290✔
914
            * get_satoshis_per_byte(&self.config);
290✔
915
        let budget_for_outputs = DUST_UTXO_LIMIT;
290✔
916
        let total_required = btc_miner_fee + budget_for_outputs;
290✔
917

918
        let (mut tx, mut utxos) =
289✔
919
            self.prepare_tx(epoch_id, &public_key, total_required, None, None, 0)?;
290✔
920

921
        // Serialize the payload
922
        let op_bytes = {
289✔
923
            let mut buffer = vec![];
289✔
924
            let mut magic_bytes = self.config.burnchain.magic_bytes.as_bytes().to_vec();
289✔
925
            buffer.append(&mut magic_bytes);
289✔
926
            payload
289✔
927
                .consensus_serialize(&mut buffer)
289✔
928
                .expect("FATAL: invalid operation");
289✔
929
            buffer
289✔
930
        };
931

932
        let consensus_output = TxOut {
289✔
933
            value: 0,
289✔
934
            script_pubkey: Builder::new()
289✔
935
                .push_opcode(opcodes::All::OP_RETURN)
289✔
936
                .push_slice(&op_bytes)
289✔
937
                .into_script(),
289✔
938
        };
289✔
939

940
        tx.output = vec![consensus_output];
289✔
941

942
        let fee_rate = get_satoshis_per_byte(&self.config);
289✔
943

944
        self.finalize_tx(
289✔
945
            epoch_id,
289✔
946
            &mut tx,
289✔
947
            budget_for_outputs,
289✔
948
            0,
949
            self.config.burnchain.leader_key_tx_estimated_size,
289✔
950
            fee_rate,
289✔
951
            &mut utxos,
289✔
952
            signer,
289✔
953
            true, // key register op requires change output to exist
954
        );
955

956
        increment_btc_ops_sent_counter();
289✔
957

958
        info!(
289✔
959
            "Miner node: submitting leader_key_register op - {}, waiting for its inclusion in the next Bitcoin block",
960
            public_key.to_hex()
289✔
961
        );
962

963
        Ok(tx)
289✔
964
    }
290✔
965

966
    #[cfg(not(test))]
967
    fn build_transfer_stacks_tx(
968
        &mut self,
969
        _epoch_id: StacksEpochId,
970
        _payload: TransferStxOp,
971
        _signer: &mut BurnchainOpSigner,
972
        _utxo: Option<UTXO>,
973
    ) -> Result<Transaction, BurnchainControllerError> {
974
        unimplemented!()
975
    }
976

977
    #[cfg(not(test))]
978
    fn build_delegate_stacks_tx(
979
        &mut self,
980
        _epoch_id: StacksEpochId,
981
        _payload: DelegateStxOp,
982
        _signer: &mut BurnchainOpSigner,
983
        _utxo: Option<UTXO>,
984
    ) -> Result<Transaction, BurnchainControllerError> {
985
        unimplemented!()
986
    }
987

988
    #[cfg(test)]
989
    pub fn submit_manual(
2✔
990
        &mut self,
2✔
991
        epoch_id: StacksEpochId,
2✔
992
        operation: BlockstackOperationType,
2✔
993
        op_signer: &mut BurnchainOpSigner,
2✔
994
        utxo: Option<UTXO>,
2✔
995
    ) -> Result<Transaction, BurnchainControllerError> {
2✔
996
        let transaction = match operation {
2✔
997
            BlockstackOperationType::LeaderBlockCommit(_)
998
            | BlockstackOperationType::LeaderKeyRegister(_)
999
            | BlockstackOperationType::StackStx(_)
1000
            | BlockstackOperationType::DelegateStx(_)
1001
            | BlockstackOperationType::VoteForAggregateKey(_) => {
1002
                unimplemented!();
×
1003
            }
1004
            BlockstackOperationType::PreStx(payload) => {
1✔
1005
                self.build_pre_stacks_tx(epoch_id, payload, op_signer)
1✔
1006
            }
1007
            BlockstackOperationType::TransferStx(payload) => {
1✔
1008
                self.build_transfer_stacks_tx(epoch_id, payload, op_signer, utxo)
1✔
1009
            }
1010
        }?;
×
1011
        self.send_transaction(&transaction).map(|_| transaction)
2✔
1012
    }
2✔
1013

1014
    #[cfg(test)]
1015
    /// Build a transfer stacks tx.
1016
    ///   this *only* works if the only existant UTXO is from a PreStx Op
1017
    ///   this is okay for testing, but obviously not okay for actual use.
1018
    ///   The reason for this constraint is that the bitcoin_regtest_controller's UTXO
1019
    ///     and signing logic are fairly intertwined, and untangling the two seems excessive
1020
    ///     for a functionality that won't be implemented for production via this controller.
1021
    fn build_transfer_stacks_tx(
4✔
1022
        &mut self,
4✔
1023
        epoch_id: StacksEpochId,
4✔
1024
        payload: TransferStxOp,
4✔
1025
        signer: &mut BurnchainOpSigner,
4✔
1026
        utxo_to_use: Option<UTXO>,
4✔
1027
    ) -> Result<Transaction, BurnchainControllerError> {
4✔
1028
        let public_key = signer.get_public_key();
4✔
1029
        let max_tx_size = OP_TX_TRANSFER_STACKS_ESTIM_SIZE;
4✔
1030
        let (mut tx, mut utxos) = if let Some(utxo) = utxo_to_use {
4✔
1031
            (
1✔
1032
                Transaction {
1✔
1033
                    input: vec![],
1✔
1034
                    output: vec![],
1✔
1035
                    version: 1,
1✔
1036
                    lock_time: 0,
1✔
1037
                },
1✔
1038
                UTXOSet {
1✔
1039
                    bhh: BurnchainHeaderHash::zero(),
1✔
1040
                    utxos: vec![utxo],
1✔
1041
                },
1✔
1042
            )
1✔
1043
        } else {
1044
            self.prepare_tx(
3✔
1045
                epoch_id,
3✔
1046
                &public_key,
3✔
1047
                DUST_UTXO_LIMIT + max_tx_size * get_satoshis_per_byte(&self.config),
3✔
1048
                None,
3✔
1049
                None,
3✔
1050
                0,
1051
            )?
×
1052
        };
1053

1054
        // Serialize the payload
1055
        let op_bytes = {
4✔
1056
            let mut bytes = self.config.burnchain.magic_bytes.as_bytes().to_vec();
4✔
1057
            payload
4✔
1058
                .consensus_serialize(&mut bytes)
4✔
1059
                .map_err(BurnchainControllerError::SerializerError)?;
4✔
1060
            bytes
4✔
1061
        };
1062

1063
        let consensus_output = TxOut {
4✔
1064
            value: 0,
4✔
1065
            script_pubkey: Builder::new()
4✔
1066
                .push_opcode(opcodes::All::OP_RETURN)
4✔
1067
                .push_slice(&op_bytes)
4✔
1068
                .into_script(),
4✔
1069
        };
4✔
1070

1071
        tx.output = vec![consensus_output];
4✔
1072
        tx.output
4✔
1073
            .push(PoxAddress::Standard(payload.recipient, None).to_bitcoin_tx_out(DUST_UTXO_LIMIT));
4✔
1074

1075
        self.finalize_tx(
4✔
1076
            epoch_id,
4✔
1077
            &mut tx,
4✔
1078
            DUST_UTXO_LIMIT,
1079
            0,
1080
            max_tx_size,
4✔
1081
            get_satoshis_per_byte(&self.config),
4✔
1082
            &mut utxos,
4✔
1083
            signer,
4✔
1084
            false,
1085
        );
1086

1087
        increment_btc_ops_sent_counter();
4✔
1088

1089
        info!(
4✔
1090
            "Miner node: submitting stacks transfer op - {}",
1091
            public_key.to_hex()
4✔
1092
        );
1093

1094
        Ok(tx)
4✔
1095
    }
4✔
1096

1097
    #[cfg(test)]
1098
    /// Build a delegate stacks tx.
1099
    ///   this *only* works if the only existant UTXO is from a PreStx Op
1100
    ///   this is okay for testing, but obviously not okay for actual use.
1101
    ///   The reason for this constraint is that the bitcoin_regtest_controller's UTXO
1102
    ///     and signing logic are fairly intertwined, and untangling the two seems excessive
1103
    ///     for a functionality that won't be implemented for production via this controller.
1104
    fn build_delegate_stacks_tx(
2✔
1105
        &mut self,
2✔
1106
        epoch_id: StacksEpochId,
2✔
1107
        payload: DelegateStxOp,
2✔
1108
        signer: &mut BurnchainOpSigner,
2✔
1109
        utxo_to_use: Option<UTXO>,
2✔
1110
    ) -> Result<Transaction, BurnchainControllerError> {
2✔
1111
        let public_key = signer.get_public_key();
2✔
1112
        let max_tx_size = OP_TX_DELEGATE_STACKS_ESTIM_SIZE;
2✔
1113

1114
        let (mut tx, mut utxos) = if let Some(utxo) = utxo_to_use {
2✔
1115
            (
×
1116
                Transaction {
×
1117
                    input: vec![],
×
1118
                    output: vec![],
×
1119
                    version: 1,
×
1120
                    lock_time: 0,
×
1121
                },
×
1122
                UTXOSet {
×
1123
                    bhh: BurnchainHeaderHash::zero(),
×
1124
                    utxos: vec![utxo],
×
1125
                },
×
1126
            )
×
1127
        } else {
1128
            self.prepare_tx(
2✔
1129
                epoch_id,
2✔
1130
                &public_key,
2✔
1131
                DUST_UTXO_LIMIT + max_tx_size * get_satoshis_per_byte(&self.config),
2✔
1132
                None,
2✔
1133
                None,
2✔
1134
                0,
1135
            )?
×
1136
        };
1137

1138
        // Serialize the payload
1139
        let op_bytes = {
2✔
1140
            let mut bytes = self.config.burnchain.magic_bytes.as_bytes().to_vec();
2✔
1141
            payload
2✔
1142
                .consensus_serialize(&mut bytes)
2✔
1143
                .map_err(BurnchainControllerError::SerializerError)?;
2✔
1144
            bytes
2✔
1145
        };
1146

1147
        let consensus_output = TxOut {
2✔
1148
            value: 0,
2✔
1149
            script_pubkey: Builder::new()
2✔
1150
                .push_opcode(opcodes::All::OP_RETURN)
2✔
1151
                .push_slice(&op_bytes)
2✔
1152
                .into_script(),
2✔
1153
        };
2✔
1154

1155
        tx.output = vec![consensus_output];
2✔
1156
        tx.output.push(
2✔
1157
            PoxAddress::Standard(payload.delegate_to, None).to_bitcoin_tx_out(DUST_UTXO_LIMIT),
2✔
1158
        );
1159

1160
        self.finalize_tx(
2✔
1161
            epoch_id,
2✔
1162
            &mut tx,
2✔
1163
            DUST_UTXO_LIMIT,
1164
            0,
1165
            max_tx_size,
2✔
1166
            get_satoshis_per_byte(&self.config),
2✔
1167
            &mut utxos,
2✔
1168
            signer,
2✔
1169
            false,
1170
        );
1171

1172
        increment_btc_ops_sent_counter();
2✔
1173

1174
        info!(
2✔
1175
            "Miner node: submitting stacks delegate op - {}",
1176
            public_key.to_hex()
2✔
1177
        );
1178

1179
        Ok(tx)
2✔
1180
    }
2✔
1181

1182
    #[cfg(test)]
1183
    /// Build a vote-for-aggregate-key burn op tx
1184
    fn build_vote_for_aggregate_key_tx(
2✔
1185
        &mut self,
2✔
1186
        epoch_id: StacksEpochId,
2✔
1187
        payload: VoteForAggregateKeyOp,
2✔
1188
        signer: &mut BurnchainOpSigner,
2✔
1189
        utxo_to_use: Option<UTXO>,
2✔
1190
    ) -> Result<Transaction, BurnchainControllerError> {
2✔
1191
        let public_key = signer.get_public_key();
2✔
1192
        let max_tx_size = OP_TX_VOTE_AGG_ESTIM_SIZE;
2✔
1193

1194
        let (mut tx, mut utxos) = if let Some(utxo) = utxo_to_use {
2✔
1195
            (
×
1196
                Transaction {
×
1197
                    input: vec![],
×
1198
                    output: vec![],
×
1199
                    version: 1,
×
1200
                    lock_time: 0,
×
1201
                },
×
1202
                UTXOSet {
×
1203
                    bhh: BurnchainHeaderHash::zero(),
×
1204
                    utxos: vec![utxo],
×
1205
                },
×
1206
            )
×
1207
        } else {
1208
            self.prepare_tx(
2✔
1209
                epoch_id,
2✔
1210
                &public_key,
2✔
1211
                DUST_UTXO_LIMIT + max_tx_size * get_satoshis_per_byte(&self.config),
2✔
1212
                None,
2✔
1213
                None,
2✔
1214
                0,
1215
            )?
×
1216
        };
1217

1218
        // Serialize the payload
1219
        let op_bytes = {
2✔
1220
            let mut bytes = self.config.burnchain.magic_bytes.as_bytes().to_vec();
2✔
1221
            payload
2✔
1222
                .consensus_serialize(&mut bytes)
2✔
1223
                .map_err(BurnchainControllerError::SerializerError)?;
2✔
1224
            bytes
2✔
1225
        };
1226

1227
        let consensus_output = TxOut {
2✔
1228
            value: 0,
2✔
1229
            script_pubkey: Builder::new()
2✔
1230
                .push_opcode(opcodes::All::OP_RETURN)
2✔
1231
                .push_slice(&op_bytes)
2✔
1232
                .into_script(),
2✔
1233
        };
2✔
1234

1235
        tx.output = vec![consensus_output];
2✔
1236

1237
        self.finalize_tx(
2✔
1238
            epoch_id,
2✔
1239
            &mut tx,
2✔
1240
            DUST_UTXO_LIMIT,
1241
            0,
1242
            max_tx_size,
2✔
1243
            get_satoshis_per_byte(&self.config),
2✔
1244
            &mut utxos,
2✔
1245
            signer,
2✔
1246
            false,
1247
        );
1248

1249
        increment_btc_ops_sent_counter();
2✔
1250

1251
        info!(
2✔
1252
            "Miner node: submitting vote for aggregate key op - {}",
1253
            public_key.to_hex()
2✔
1254
        );
1255

1256
        Ok(tx)
2✔
1257
    }
2✔
1258

1259
    #[cfg(not(test))]
1260
    /// Build a vote-for-aggregate-key burn op tx
1261
    fn build_vote_for_aggregate_key_tx(
1262
        &mut self,
1263
        _epoch_id: StacksEpochId,
1264
        _payload: VoteForAggregateKeyOp,
1265
        _signer: &mut BurnchainOpSigner,
1266
        _utxo_to_use: Option<UTXO>,
1267
    ) -> Result<Transaction, BurnchainControllerError> {
1268
        unimplemented!()
1269
    }
1270

1271
    #[cfg(not(test))]
1272
    fn build_pre_stacks_tx(
1273
        &mut self,
1274
        _epoch_id: StacksEpochId,
1275
        _payload: PreStxOp,
1276
        _signer: &mut BurnchainOpSigner,
1277
    ) -> Result<Transaction, BurnchainControllerError> {
1278
        unimplemented!()
1279
    }
1280

1281
    #[cfg(test)]
1282
    fn build_pre_stacks_tx(
16✔
1283
        &mut self,
16✔
1284
        epoch_id: StacksEpochId,
16✔
1285
        payload: PreStxOp,
16✔
1286
        signer: &mut BurnchainOpSigner,
16✔
1287
    ) -> Result<Transaction, BurnchainControllerError> {
16✔
1288
        let public_key = signer.get_public_key();
16✔
1289
        let max_tx_size = OP_TX_PRE_STACKS_ESTIM_SIZE;
16✔
1290

1291
        let max_tx_size_any_op = OP_TX_ANY_ESTIM_SIZE;
16✔
1292
        let output_amt = DUST_UTXO_LIMIT + max_tx_size_any_op * get_satoshis_per_byte(&self.config);
16✔
1293

1294
        let (mut tx, mut utxos) =
15✔
1295
            self.prepare_tx(epoch_id, &public_key, output_amt, None, None, 0)?;
16✔
1296

1297
        // Serialize the payload
1298
        let op_bytes = {
15✔
1299
            let mut bytes = self.config.burnchain.magic_bytes.as_bytes().to_vec();
15✔
1300
            bytes.push(Opcodes::PreStx as u8);
15✔
1301
            bytes
15✔
1302
        };
1303

1304
        let consensus_output = TxOut {
15✔
1305
            value: 0,
15✔
1306
            script_pubkey: Builder::new()
15✔
1307
                .push_opcode(opcodes::All::OP_RETURN)
15✔
1308
                .push_slice(&op_bytes)
15✔
1309
                .into_script(),
15✔
1310
        };
15✔
1311

1312
        tx.output = vec![consensus_output];
15✔
1313
        tx.output
15✔
1314
            .push(PoxAddress::Standard(payload.output, None).to_bitcoin_tx_out(output_amt));
15✔
1315

1316
        self.finalize_tx(
15✔
1317
            epoch_id,
15✔
1318
            &mut tx,
15✔
1319
            output_amt,
15✔
1320
            0,
1321
            max_tx_size,
15✔
1322
            get_satoshis_per_byte(&self.config),
15✔
1323
            &mut utxos,
15✔
1324
            signer,
15✔
1325
            false,
1326
        );
1327

1328
        increment_btc_ops_sent_counter();
15✔
1329

1330
        info!(
15✔
1331
            "Miner node: submitting pre_stacks op - {}",
1332
            public_key.to_hex()
15✔
1333
        );
1334

1335
        Ok(tx)
15✔
1336
    }
16✔
1337

1338
    #[cfg_attr(test, mutants::skip)]
1339
    #[cfg(not(test))]
1340
    fn build_stack_stx_tx(
1341
        &mut self,
1342
        _epoch_id: StacksEpochId,
1343
        _payload: StackStxOp,
1344
        _signer: &mut BurnchainOpSigner,
1345
        _utxo_to_use: Option<UTXO>,
1346
    ) -> Result<Transaction, BurnchainControllerError> {
1347
        unimplemented!()
1348
    }
1349

1350
    #[cfg(test)]
1351
    fn build_stack_stx_tx(
4✔
1352
        &mut self,
4✔
1353
        epoch_id: StacksEpochId,
4✔
1354
        payload: StackStxOp,
4✔
1355
        signer: &mut BurnchainOpSigner,
4✔
1356
        utxo_to_use: Option<UTXO>,
4✔
1357
    ) -> Result<Transaction, BurnchainControllerError> {
4✔
1358
        let public_key = signer.get_public_key();
4✔
1359
        let max_tx_size = OP_TX_STACK_STX_ESTIM_SIZE;
4✔
1360

1361
        let (mut tx, mut utxos) = if let Some(utxo) = utxo_to_use {
4✔
1362
            (
×
1363
                Transaction {
×
1364
                    input: vec![],
×
1365
                    output: vec![],
×
1366
                    version: 1,
×
1367
                    lock_time: 0,
×
1368
                },
×
1369
                UTXOSet {
×
1370
                    bhh: BurnchainHeaderHash::zero(),
×
1371
                    utxos: vec![utxo],
×
1372
                },
×
1373
            )
×
1374
        } else {
1375
            self.prepare_tx(
4✔
1376
                epoch_id,
4✔
1377
                &public_key,
4✔
1378
                DUST_UTXO_LIMIT + max_tx_size * get_satoshis_per_byte(&self.config),
4✔
1379
                None,
4✔
1380
                None,
4✔
1381
                0,
1382
            )?
×
1383
        };
1384

1385
        // Serialize the payload
1386
        let op_bytes = {
4✔
1387
            let mut bytes = self.config.burnchain.magic_bytes.as_bytes().to_vec();
4✔
1388
            payload
4✔
1389
                .consensus_serialize(&mut bytes)
4✔
1390
                .map_err(BurnchainControllerError::SerializerError)?;
4✔
1391
            bytes
4✔
1392
        };
1393

1394
        let consensus_output = TxOut {
4✔
1395
            value: 0,
4✔
1396
            script_pubkey: Builder::new()
4✔
1397
                .push_opcode(opcodes::All::OP_RETURN)
4✔
1398
                .push_slice(&op_bytes)
4✔
1399
                .into_script(),
4✔
1400
        };
4✔
1401

1402
        tx.output = vec![consensus_output];
4✔
1403
        tx.output
4✔
1404
            .push(payload.reward_addr.to_bitcoin_tx_out(DUST_UTXO_LIMIT));
4✔
1405

1406
        self.finalize_tx(
4✔
1407
            epoch_id,
4✔
1408
            &mut tx,
4✔
1409
            DUST_UTXO_LIMIT,
1410
            0,
1411
            max_tx_size,
4✔
1412
            get_satoshis_per_byte(&self.config),
4✔
1413
            &mut utxos,
4✔
1414
            signer,
4✔
1415
            false,
1416
        );
1417

1418
        increment_btc_ops_sent_counter();
4✔
1419

1420
        info!(
4✔
1421
            "Miner node: submitting stack-stx op - {}",
1422
            public_key.to_hex()
4✔
1423
        );
1424

1425
        Ok(tx)
4✔
1426
    }
4✔
1427

1428
    fn magic_bytes(&self) -> Vec<u8> {
9,608✔
1429
        #[cfg(test)]
1430
        {
1431
            if let Some(set_bytes) = *TEST_MAGIC_BYTES
9,608✔
1432
                .lock()
9,608✔
1433
                .expect("FATAL: test magic bytes mutex poisoned")
9,608✔
1434
            {
1435
                return set_bytes.to_vec();
1✔
1436
            }
9,607✔
1437
        }
1438
        self.config.burnchain.magic_bytes.as_bytes().to_vec()
9,607✔
1439
    }
9,608✔
1440

1441
    #[allow(clippy::too_many_arguments)]
1442
    fn send_block_commit_operation(
10,588✔
1443
        &mut self,
10,588✔
1444
        epoch_id: StacksEpochId,
10,588✔
1445
        payload: LeaderBlockCommitOp,
10,588✔
1446
        signer: &mut BurnchainOpSigner,
10,588✔
1447
        utxos_to_include: Option<UTXOSet>,
10,588✔
1448
        utxos_to_exclude: Option<UTXOSet>,
10,588✔
1449
        previous_fees: Option<LeaderBlockCommitFees>,
10,588✔
1450
        previous_txids: &[Txid],
10,588✔
1451
    ) -> Result<Transaction, BurnchainControllerError> {
10,588✔
1452
        let _ = self.sortdb_mut();
10,588✔
1453
        let burn_chain_tip = self
10,588✔
1454
            .burnchain_db
10,588✔
1455
            .as_ref()
10,588✔
1456
            .ok_or(BurnchainControllerError::BurnchainError)?
10,588✔
1457
            .get_canonical_chain_tip()
10,588✔
1458
            .map_err(|_| BurnchainControllerError::BurnchainError)?;
10,588✔
1459
        let estimated_fees = match previous_fees {
10,588✔
1460
            Some(fees) => fees.fees_from_previous_tx(&payload, &self.config),
603✔
1461
            None => LeaderBlockCommitFees::estimated_fees_from_payload(&payload, &self.config),
9,985✔
1462
        };
1463

1464
        self.send_block_commit_operation_at_burnchain_height(
10,588✔
1465
            epoch_id,
10,588✔
1466
            payload,
10,588✔
1467
            signer,
10,588✔
1468
            utxos_to_include,
10,588✔
1469
            utxos_to_exclude,
10,588✔
1470
            estimated_fees,
10,588✔
1471
            previous_txids,
10,588✔
1472
            burn_chain_tip.block_height,
10,588✔
1473
        )
1474
    }
10,588✔
1475

1476
    #[allow(clippy::too_many_arguments)]
1477
    fn send_block_commit_operation_at_burnchain_height(
10,589✔
1478
        &mut self,
10,589✔
1479
        epoch_id: StacksEpochId,
10,589✔
1480
        payload: LeaderBlockCommitOp,
10,589✔
1481
        signer: &mut BurnchainOpSigner,
10,589✔
1482
        utxos_to_include: Option<UTXOSet>,
10,589✔
1483
        utxos_to_exclude: Option<UTXOSet>,
10,589✔
1484
        mut estimated_fees: LeaderBlockCommitFees,
10,589✔
1485
        previous_txids: &[Txid],
10,589✔
1486
        burnchain_block_height: u64,
10,589✔
1487
    ) -> Result<Transaction, BurnchainControllerError> {
10,589✔
1488
        let public_key = signer.get_public_key();
10,589✔
1489
        let (mut tx, mut utxos) = self.prepare_tx(
10,589✔
1490
            epoch_id,
10,589✔
1491
            &public_key,
10,589✔
1492
            estimated_fees.estimated_amount_required(),
10,589✔
1493
            utxos_to_include,
10,589✔
1494
            utxos_to_exclude,
10,589✔
1495
            burnchain_block_height,
10,589✔
1496
        )?;
977✔
1497

1498
        // Serialize the payload
1499
        let op_bytes = {
9,612✔
1500
            let mut buffer = vec![];
9,612✔
1501
            let mut magic_bytes = self.magic_bytes();
9,612✔
1502
            buffer.append(&mut magic_bytes);
9,612✔
1503
            payload
9,612✔
1504
                .consensus_serialize(&mut buffer)
9,612✔
1505
                .expect("FATAL: invalid operation");
9,612✔
1506
            buffer
9,612✔
1507
        };
1508

1509
        let consensus_output = TxOut {
9,612✔
1510
            value: estimated_fees.sunset_fee,
9,612✔
1511
            script_pubkey: Builder::new()
9,612✔
1512
                .push_opcode(opcodes::All::OP_RETURN)
9,612✔
1513
                .push_slice(&op_bytes)
9,612✔
1514
                .into_script(),
9,612✔
1515
        };
9,612✔
1516

1517
        tx.output = vec![consensus_output];
9,612✔
1518

1519
        for commit_to in payload.commit_outs.iter() {
16,873✔
1520
            tx.output
16,873✔
1521
                .push(commit_to.to_bitcoin_tx_out(estimated_fees.amount_per_output()));
16,873✔
1522
        }
16,873✔
1523

1524
        let fee_rate = estimated_fees.fee_rate;
9,612✔
1525
        self.finalize_tx(
9,612✔
1526
            epoch_id,
9,612✔
1527
            &mut tx,
9,612✔
1528
            estimated_fees.total_spent_in_outputs(),
9,612✔
1529
            estimated_fees.spent_in_attempts,
9,612✔
1530
            estimated_fees.min_tx_size(),
9,612✔
1531
            fee_rate,
9,612✔
1532
            &mut utxos,
9,612✔
1533
            signer,
9,612✔
1534
            true, // block commit op requires change output to exist
1535
        );
1536
        debug!("Transaction relying on UTXOs: {utxos:?}");
9,612✔
1537

1538
        let serialized_tx = serialize(&tx).expect("BUG: failed to serialize to a vec");
9,612✔
1539
        let tx_size = serialized_tx.len() as u64;
9,612✔
1540
        estimated_fees.register_replacement(tx_size);
9,612✔
1541

1542
        let txid = Txid::from_bitcoin_tx_hash(&tx.txid());
9,612✔
1543
        let mut txids = previous_txids.to_vec();
9,612✔
1544
        txids.push(txid.clone());
9,612✔
1545
        let ongoing_block_commit = OngoingBlockCommit {
9,612✔
1546
            payload,
9,612✔
1547
            utxos,
9,612✔
1548
            fees: estimated_fees,
9,612✔
1549
            txids,
9,612✔
1550
        };
9,612✔
1551

1552
        info!(
9,612✔
1553
            "Miner node: submitting leader_block_commit (txid: {}, rbf: {}, total spent: {}, size: {}, fee_rate: {fee_rate})",
1554
            txid.to_hex(),
9,608✔
1555
            ongoing_block_commit.fees.is_rbf_enabled,
1556
            ongoing_block_commit.fees.total_spent(),
9,608✔
1557
            ongoing_block_commit.fees.final_size
1558
        );
1559

1560
        self.ongoing_block_commit = Some(ongoing_block_commit);
9,612✔
1561

1562
        increment_btc_ops_sent_counter();
9,612✔
1563

1564
        Ok(tx)
9,612✔
1565
    }
10,589✔
1566

1567
    fn build_leader_block_commit_tx(
25,339✔
1568
        &mut self,
25,339✔
1569
        epoch_id: StacksEpochId,
25,339✔
1570
        payload: LeaderBlockCommitOp,
25,339✔
1571
        signer: &mut BurnchainOpSigner,
25,339✔
1572
    ) -> Result<Transaction, BurnchainControllerError> {
25,339✔
1573
        // Are we currently tracking an operation?
1574
        if self.ongoing_block_commit.is_none() {
25,339✔
1575
            // Good to go, let's build the transaction and send it.
1576
            let res =
1,479✔
1577
                self.send_block_commit_operation(epoch_id, payload, signer, None, None, None, &[]);
1,479✔
1578
            return res;
1,479✔
1579
        }
23,860✔
1580

1581
        let ongoing_op = self.ongoing_block_commit.take().unwrap();
23,860✔
1582

1583
        let _ = self.sortdb_mut();
23,860✔
1584
        let burnchain_db = self.burnchain_db.as_ref().expect("BurnchainDB not opened");
23,860✔
1585

1586
        for txid in ongoing_op.txids.iter() {
24,424✔
1587
            // check if ongoing_op is in the burnchain_db *or* has been confirmed via the bitcoin RPC
1588
            let mined_op = burnchain_db.find_burnchain_op(&self.indexer, txid);
24,423✔
1589
            let ongoing_tx_confirmed = mined_op.is_some() || self.is_transaction_confirmed(txid);
24,423✔
1590

1591
            test_debug!("Ongoing Tx confirmed: {ongoing_tx_confirmed} - TXID: {txid}");
24,423✔
1592
            if ongoing_tx_confirmed {
24,423✔
1593
                if ongoing_op.payload == payload {
9,716✔
1594
                    info!("Abort attempt to re-submit confirmed LeaderBlockCommit");
1,209✔
1595
                    self.ongoing_block_commit = Some(ongoing_op);
1,209✔
1596
                    return Err(BurnchainControllerError::IdenticalOperation);
1,209✔
1597
                }
8,507✔
1598

1599
                debug!("Was able to retrieve confirmation of ongoing burnchain TXID - {txid}");
8,507✔
1600
                let res = self.send_block_commit_operation(
8,507✔
1601
                    epoch_id,
8,507✔
1602
                    payload,
8,507✔
1603
                    signer,
8,507✔
1604
                    None,
8,507✔
1605
                    None,
8,507✔
1606
                    None,
8,507✔
1607
                    &[],
8,507✔
1608
                );
1609
                return res;
8,507✔
1610
            } else {
1611
                debug!("Was unable to retrieve ongoing TXID - {txid}");
14,707✔
1612
            };
1613
        }
1614

1615
        // Did a re-org occur since we fetched our UTXOs, or are the UTXOs so stale that they should be abandoned?
1616
        let mut traversal_depth = 0;
14,144✔
1617
        let mut burn_chain_tip = burnchain_db
14,144✔
1618
            .get_canonical_chain_tip()
14,144✔
1619
            .map_err(|_| BurnchainControllerError::BurnchainError)?;
14,144✔
1620
        let mut found_last_mined_at = false;
14,144✔
1621
        while traversal_depth < UTXO_CACHE_STALENESS_LIMIT {
14,218✔
1622
            if burn_chain_tip.block_hash == ongoing_op.utxos.bhh {
14,218✔
1623
                found_last_mined_at = true;
14,144✔
1624
                break;
14,144✔
1625
            }
74✔
1626

1627
            let parent = BurnchainDB::get_burnchain_block(
74✔
1628
                burnchain_db.conn(),
74✔
1629
                &burn_chain_tip.parent_block_hash,
74✔
1630
            )
1631
            .map_err(|_| BurnchainControllerError::BurnchainError)?;
74✔
1632

1633
            burn_chain_tip = parent.header;
74✔
1634
            traversal_depth += 1;
74✔
1635
        }
1636

1637
        if !found_last_mined_at {
14,144✔
UNCOV
1638
            info!(
×
1639
                "Possible presence of fork or stale UTXO cache, invalidating cached set of UTXOs.";
1640
                "cached_burn_block_hash" => %ongoing_op.utxos.bhh,
1641
            );
UNCOV
1642
            let res =
×
UNCOV
1643
                self.send_block_commit_operation(epoch_id, payload, signer, None, None, None, &[]);
×
UNCOV
1644
            return res;
×
1645
        }
14,144✔
1646

1647
        // Stop as soon as the fee_rate is ${self.config.burnchain.max_rbf} percent higher, stop RBF
1648
        if ongoing_op.fees.fee_rate
14,144✔
1649
            > (get_satoshis_per_byte(&self.config) * get_max_rbf(&self.config) / 100)
14,144✔
1650
        {
1651
            warn!(
×
1652
                "RBF'd block commits reached {}% satoshi per byte fee rate, not resubmitting",
1653
                get_max_rbf(&self.config)
×
1654
            );
1655
            self.ongoing_block_commit = Some(ongoing_op);
×
1656
            return Err(BurnchainControllerError::MaxFeeRateExceeded);
×
1657
        }
14,144✔
1658

1659
        // An ongoing operation is in the mempool and we received a new block. The desired behaviour is the following:
1660
        // (1) If the ongoing and the incoming operation are **strictly** identical, we will be idempotent and discard the incoming.
1661
        // (2) If the 2 operations are different, attempt to RBF the outgoing transaction:
1662

1663
        // Let's start by early returning (1)
1664
        if payload == ongoing_op.payload {
14,144✔
1665
            info!("Abort attempt to re-submit identical LeaderBlockCommit");
13,541✔
1666
            self.ongoing_block_commit = Some(ongoing_op);
13,541✔
1667
            return Err(BurnchainControllerError::IdenticalOperation);
13,541✔
1668
        }
603✔
1669

1670
        // If we reach this point, we are attempting to RBF the ongoing operation (2)
1671
        info!(
603✔
1672
            "Attempt to replace by fee an outdated leader block commit";
1673
            "ongoing_txids" => ?ongoing_op.txids
1674
        );
1675
        let res = self.send_block_commit_operation(
603✔
1676
            epoch_id,
603✔
1677
            payload,
603✔
1678
            signer,
603✔
1679
            Some(ongoing_op.utxos.clone()),
603✔
1680
            None,
603✔
1681
            Some(ongoing_op.fees.clone()),
603✔
1682
            &ongoing_op.txids,
603✔
1683
        );
1684

1685
        if res.is_err() {
603✔
1686
            self.ongoing_block_commit = Some(ongoing_op);
×
1687
        }
603✔
1688

1689
        res
603✔
1690
    }
25,339✔
1691

1692
    pub(crate) fn get_miner_address(
37,405✔
1693
        &self,
37,405✔
1694
        epoch_id: StacksEpochId,
37,405✔
1695
        public_key: &Secp256k1PublicKey,
37,405✔
1696
    ) -> BitcoinAddress {
37,405✔
1697
        let (_, network_id) = self.config.burnchain.get_bitcoin_network();
37,405✔
1698

1699
        if self.config.miner.segwit && epoch_id >= StacksEpochId::Epoch21 {
37,405✔
1700
            let hash160 = Hash160::from_data(&public_key.to_bytes_compressed());
1✔
1701
            BitcoinAddress::from_bytes_segwit_p2wpkh(network_id, &hash160.0)
1✔
1702
                .expect("Public key incorrect")
1✔
1703
        } else {
1704
            let hash160 = Hash160::from_data(&public_key.to_bytes());
37,404✔
1705
            BitcoinAddress::from_bytes_legacy(
37,404✔
1706
                network_id,
37,404✔
1707
                LegacyBitcoinAddressType::PublicKeyHash,
37,404✔
1708
                &hash160.0,
37,404✔
1709
            )
1710
            .expect("Public key incorrect")
37,404✔
1711
        }
1712
    }
37,405✔
1713

1714
    // TODO: add tests from mutation testing results #4865
1715
    #[cfg_attr(test, mutants::skip)]
1716
    fn prepare_tx(
10,908✔
1717
        &mut self,
10,908✔
1718
        epoch_id: StacksEpochId,
10,908✔
1719
        public_key: &Secp256k1PublicKey,
10,908✔
1720
        total_required: u64,
10,908✔
1721
        utxos_to_include: Option<UTXOSet>,
10,908✔
1722
        utxos_to_exclude: Option<UTXOSet>,
10,908✔
1723
        block_height: u64,
10,908✔
1724
    ) -> Result<(Transaction, UTXOSet), BurnchainControllerError> {
10,908✔
1725
        let utxos = if let Some(utxos) = utxos_to_include {
10,908✔
1726
            // in RBF, you have to consume the same UTXOs
1727
            utxos
604✔
1728
        } else {
1729
            // if mock mining, do not even bother requesting UTXOs
1730
            if self.config.node.mock_mining {
10,304✔
1731
                return Err(BurnchainControllerError::NoUTXOs);
977✔
1732
            }
9,327✔
1733

1734
            // Fetch some UTXOs
1735
            let addr = self.get_miner_address(epoch_id, public_key);
9,327✔
1736
            match self.get_utxos(
9,327✔
1737
                epoch_id,
9,327✔
1738
                public_key,
9,327✔
1739
                total_required,
9,327✔
1740
                utxos_to_exclude,
9,327✔
1741
                block_height,
9,327✔
1742
            ) {
9,327✔
1743
                Some(utxos) => utxos,
9,321✔
1744
                None => {
1745
                    warn!(
6✔
1746
                        "No UTXOs for {} ({addr}) in epoch {epoch_id}",
1747
                        &public_key.to_hex(),
2✔
1748
                    );
1749
                    return Err(BurnchainControllerError::NoUTXOs);
6✔
1750
                }
1751
            }
1752
        };
1753

1754
        // Prepare a backbone for the tx
1755
        let transaction = Transaction {
9,925✔
1756
            input: vec![],
9,925✔
1757
            output: vec![],
9,925✔
1758
            version: 1,
9,925✔
1759
            lock_time: 0,
9,925✔
1760
        };
9,925✔
1761

1762
        Ok((transaction, utxos))
9,925✔
1763
    }
10,908✔
1764

1765
    #[allow(clippy::too_many_arguments)]
1766
    fn finalize_tx(
9,926✔
1767
        &mut self,
9,926✔
1768
        epoch_id: StacksEpochId,
9,926✔
1769
        tx: &mut Transaction,
9,926✔
1770
        spent_in_outputs: u64,
9,926✔
1771
        spent_in_rbf: u64,
9,926✔
1772
        min_tx_size: u64,
9,926✔
1773
        fee_rate: u64,
9,926✔
1774
        utxos_set: &mut UTXOSet,
9,926✔
1775
        signer: &mut BurnchainOpSigner,
9,926✔
1776
        force_change_output: bool,
9,926✔
1777
    ) {
9,926✔
1778
        // spend UTXOs in order by confirmations.  Spend the least-confirmed UTXO first, and in the
1779
        // event of a tie, spend the smallest-value UTXO first.
1780
        utxos_set.utxos.sort_by(|u1, u2| {
7,337,015✔
1781
            if u1.confirmations != u2.confirmations {
7,336,999✔
1782
                u1.confirmations.cmp(&u2.confirmations)
7,336,998✔
1783
            } else {
1784
                // for block-commits, the smaller value is likely the UTXO-chained value, so
1785
                // continue to prioritize it as the first spend in order to avoid breaking the
1786
                // miner commit chain.
1787
                u1.amount.cmp(&u2.amount)
1✔
1788
            }
1789
        });
7,336,999✔
1790

1791
        let tx_size = {
9,926✔
1792
            // We will be calling 2 times serialize_tx, the first time with an estimated size,
1793
            // Second time with the actual size, computed thanks to the 1st attempt.
1794
            let estimated_rbf = if spent_in_rbf == 0 {
9,926✔
1795
                0
9,323✔
1796
            } else {
1797
                spent_in_rbf + min_tx_size // we're spending 1 sat / byte in RBF
603✔
1798
            };
1799
            let mut tx_cloned = tx.clone();
9,926✔
1800
            let mut utxos_cloned = utxos_set.clone();
9,926✔
1801
            self.serialize_tx(
9,926✔
1802
                epoch_id,
9,926✔
1803
                &mut tx_cloned,
9,926✔
1804
                spent_in_outputs + min_tx_size * fee_rate + estimated_rbf,
9,926✔
1805
                &mut utxos_cloned,
9,926✔
1806
                signer,
9,926✔
1807
                force_change_output,
9,926✔
1808
            );
1809
            let serialized_tx = serialize(&tx_cloned).expect("BUG: failed to serialize to a vec");
9,926✔
1810
            cmp::max(min_tx_size, serialized_tx.len() as u64)
9,926✔
1811
        };
1812

1813
        let rbf_fee = if spent_in_rbf == 0 {
9,926✔
1814
            0
9,323✔
1815
        } else {
1816
            spent_in_rbf + tx_size // we're spending 1 sat / byte in RBF
603✔
1817
        };
1818
        self.serialize_tx(
9,926✔
1819
            epoch_id,
9,926✔
1820
            tx,
9,926✔
1821
            spent_in_outputs + tx_size * fee_rate + rbf_fee,
9,926✔
1822
            utxos_set,
9,926✔
1823
            signer,
9,926✔
1824
            force_change_output,
9,926✔
1825
        );
1826
        signer.dispose();
9,926✔
1827
    }
9,926✔
1828

1829
    /// Sign and serialize a tx, consuming the UTXOs in utxo_set and spending total_to_spend
1830
    /// satoshis.  Uses the key in signer.
1831
    /// If self.config.miner.segwit is true, the transaction's change address will be a p2wpkh
1832
    /// output. Otherwise, it will be a p2pkh output.
1833
    fn serialize_tx(
19,853✔
1834
        &mut self,
19,853✔
1835
        epoch_id: StacksEpochId,
19,853✔
1836
        tx: &mut Transaction,
19,853✔
1837
        tx_cost: u64,
19,853✔
1838
        utxos_set: &mut UTXOSet,
19,853✔
1839
        signer: &mut BurnchainOpSigner,
19,853✔
1840
        force_change_output: bool,
19,853✔
1841
    ) -> bool {
19,853✔
1842
        let mut public_key = signer.get_public_key();
19,853✔
1843

1844
        let total_target = if force_change_output {
19,853✔
1845
            tx_cost + DUST_UTXO_LIMIT
19,799✔
1846
        } else {
1847
            tx_cost
54✔
1848
        };
1849

1850
        // select UTXOs until we have enough to cover the cost
1851
        let mut total_consumed = 0;
19,853✔
1852
        let mut available_utxos = vec![];
19,853✔
1853
        available_utxos.append(&mut utxos_set.utxos);
19,853✔
1854
        for utxo in available_utxos.into_iter() {
19,856✔
1855
            total_consumed += utxo.amount;
19,856✔
1856
            utxos_set.utxos.push(utxo);
19,856✔
1857

1858
            if total_consumed >= total_target {
19,856✔
1859
                break;
19,853✔
1860
            }
3✔
1861
        }
1862

1863
        if total_consumed < total_target {
19,853✔
1864
            warn!("Consumed total {total_consumed} is less than intended spend: {total_target}");
×
1865
            return false;
×
1866
        }
19,853✔
1867

1868
        // Append the change output
1869
        let value = total_consumed - tx_cost;
19,853✔
1870
        debug!(
19,853✔
1871
            "Payments value: {value:?}, total_consumed: {total_consumed:?}, total_spent: {total_target:?}"
1872
        );
1873
        if value >= DUST_UTXO_LIMIT {
19,853✔
1874
            let change_output = if self.config.miner.segwit && epoch_id >= StacksEpochId::Epoch21 {
19,844✔
1875
                // p2wpkh
1876
                public_key.set_compressed(true);
×
1877
                let change_address_hash = Hash160::from_data(&public_key.to_bytes());
×
1878
                SegwitBitcoinAddress::to_p2wpkh_tx_out(&change_address_hash.0, value)
×
1879
            } else {
1880
                // p2pkh
1881
                let change_address_hash = Hash160::from_data(&public_key.to_bytes());
19,844✔
1882
                LegacyBitcoinAddress::to_p2pkh_tx_out(&change_address_hash, value)
19,844✔
1883
            };
1884
            tx.output.push(change_output);
19,844✔
1885
        } else {
1886
            // Instead of leaving that change to the BTC miner, we could / should bump the sortition fee
1887
            debug!("Not enough change to clear dust limit. Not adding change address.");
9✔
1888
        }
1889

1890
        for utxo in utxos_set.utxos.iter() {
19,856✔
1891
            let input = TxIn {
19,856✔
1892
                previous_output: OutPoint {
19,856✔
1893
                    txid: utxo.txid.clone(),
19,856✔
1894
                    vout: utxo.vout,
19,856✔
1895
                },
19,856✔
1896
                script_sig: Script::new(),
19,856✔
1897
                sequence: 0xFFFFFFFD, // allow RBF
19,856✔
1898
                witness: vec![],
19,856✔
1899
            };
19,856✔
1900
            tx.input.push(input);
19,856✔
1901
        }
19,856✔
1902
        for (i, utxo) in utxos_set.utxos.iter().enumerate() {
19,856✔
1903
            let script_pub_key = utxo.script_pub_key.clone();
19,856✔
1904
            let sig_hash_all = 0x01;
19,856✔
1905

1906
            let (sig_hash, is_segwit) = if script_pub_key.as_bytes().len() == 22
19,856✔
1907
                && script_pub_key.as_bytes()[0..2] == [0x00, 0x14]
×
1908
            {
1909
                // p2wpkh
1910
                (
×
1911
                    tx.segwit_signature_hash(i, &script_pub_key, utxo.amount, sig_hash_all),
×
1912
                    true,
×
1913
                )
×
1914
            } else {
1915
                // p2pkh
1916
                (tx.signature_hash(i, &script_pub_key, sig_hash_all), false)
19,856✔
1917
            };
1918

1919
            let sig1_der = {
19,856✔
1920
                let message = signer
19,856✔
1921
                    .sign_message(sig_hash.as_bytes())
19,856✔
1922
                    .expect("Unable to sign message");
19,856✔
1923
                message
19,856✔
1924
                    .to_secp256k1_recoverable()
19,856✔
1925
                    .expect("Unable to get recoverable signature")
19,856✔
1926
                    .to_standard()
19,856✔
1927
                    .serialize_der()
19,856✔
1928
            };
1929

1930
            if is_segwit {
19,856✔
1931
                // segwit
×
1932
                public_key.set_compressed(true);
×
1933
                tx.input[i].script_sig = Script::from(vec![]);
×
1934
                tx.input[i].witness = vec![
×
1935
                    [&*sig1_der, &[sig_hash_all as u8][..]].concat().to_vec(),
×
1936
                    public_key.to_bytes(),
×
1937
                ];
×
1938
            } else {
19,856✔
1939
                // legacy scriptSig
19,856✔
1940
                tx.input[i].script_sig = Builder::new()
19,856✔
1941
                    .push_slice(&[&*sig1_der, &[sig_hash_all as u8][..]].concat())
19,856✔
1942
                    .push_slice(&public_key.to_bytes())
19,856✔
1943
                    .into_script();
19,856✔
1944
                tx.input[i].witness.clear();
19,856✔
1945
            }
19,856✔
1946
        }
1947
        true
19,853✔
1948
    }
19,853✔
1949

1950
    /// Broadcast a signed raw [`Transaction`] to the underlying Bitcoin node.
1951
    ///
1952
    /// The transaction is submitted with following parameters:
1953
    /// - `max_fee_rate = 0.0` (uncapped, accept any fee rate),
1954
    /// - `max_burn_amount = 1_000_000` (in sats).
1955
    ///
1956
    /// # Arguments
1957
    /// * `transaction` - A fully signed raw [`Transaction`] to broadcast.
1958
    ///
1959
    /// # Returns
1960
    /// On success, returns the [`Txid`] of the broadcasted transaction.
1961
    pub fn send_transaction(&self, tx: &Transaction) -> Result<Txid, BurnchainControllerError> {
9,915✔
1962
        debug!(
9,915✔
1963
            "Sending raw transaction: {}",
1964
            serialize_hex(tx).unwrap_or("SERIALIZATION FAILED".to_string())
×
1965
        );
1966

1967
        const UNCAPPED_FEE: f64 = 0.0;
1968
        const MAX_BURN_AMOUNT: u64 = 1_000_000;
1969
        self.get_rpc_client()
9,915✔
1970
            .send_raw_transaction(tx, Some(UNCAPPED_FEE), Some(MAX_BURN_AMOUNT))
9,915✔
1971
            .map(|txid| {
9,915✔
1972
                debug!("Transaction {txid} sent successfully");
9,913✔
1973
                txid
9,913✔
1974
            })
9,913✔
1975
            .map_err(|e| {
9,915✔
1976
                error!("Bitcoin RPC error: transaction submission failed - {e:?}");
2✔
1977
                BurnchainControllerError::TransactionSubmissionFailed(format!("{e:?}"))
2✔
1978
            })
2✔
1979
    }
9,915✔
1980

1981
    /// wait until the ChainsCoordinator has processed sortitions up to
1982
    /// height_to_wait
1983
    pub fn wait_for_sortitions(
486,099✔
1984
        &self,
486,099✔
1985
        coord_comms: CoordinatorChannels,
486,099✔
1986
        height_to_wait: u64,
486,099✔
1987
    ) -> Result<BurnchainTip, BurnchainControllerError> {
486,099✔
1988
        let mut debug_ctr = 0;
486,099✔
1989
        loop {
1990
            let canonical_sortition_tip =
493,677✔
1991
                SortitionDB::get_canonical_burn_chain_tip(self.sortdb_ref().conn()).unwrap();
493,677✔
1992

1993
            if debug_ctr % 10 == 0 {
493,677✔
1994
                debug!(
486,130✔
1995
                    "Waiting until canonical sortition height reaches {height_to_wait} (currently {})",
1996
                    canonical_sortition_tip.block_height
1997
                );
1998
            }
7,547✔
1999
            debug_ctr += 1;
493,677✔
2000

2001
            if canonical_sortition_tip.block_height >= height_to_wait {
493,677✔
2002
                let (_, state_transition) = self
486,098✔
2003
                    .sortdb_ref()
486,098✔
2004
                    .get_sortition_result(&canonical_sortition_tip.sortition_id)
486,098✔
2005
                    .expect("Sortition DB error.")
486,098✔
2006
                    .expect("BUG: no data for the canonical chain tip");
486,098✔
2007

2008
                return Ok(BurnchainTip {
486,098✔
2009
                    block_snapshot: canonical_sortition_tip,
486,098✔
2010
                    received_at: Instant::now(),
486,098✔
2011
                    state_transition,
486,098✔
2012
                });
486,098✔
2013
            }
7,579✔
2014

2015
            if !self.should_keep_running() {
7,579✔
2016
                return Err(BurnchainControllerError::CoordinatorClosed);
1✔
2017
            }
7,578✔
2018

2019
            // help the chains coordinator along
2020
            coord_comms.announce_new_burn_block();
7,578✔
2021
            coord_comms.announce_new_stacks_block();
7,578✔
2022

2023
            // yield some time
2024
            sleep_ms(1000);
7,578✔
2025
        }
2026
    }
486,099✔
2027

2028
    /// Instruct a regtest Bitcoin node to build the next block.
2029
    pub fn build_next_block(&self, num_blocks: u64) {
9,447✔
2030
        debug!("Generate {num_blocks} block(s)");
9,447✔
2031
        let public_key_bytes = match &self.config.burnchain.local_mining_public_key {
9,447✔
2032
            Some(public_key) => hex_bytes(public_key).expect("Invalid byte sequence"),
9,447✔
2033
            None => panic!("Unable to make new block, mining public key"),
×
2034
        };
2035

2036
        // NOTE: miner address is whatever the configured segwit setting is
2037
        let public_key = Secp256k1PublicKey::from_slice(&public_key_bytes)
9,447✔
2038
            .expect("FATAL: invalid public key bytes");
9,447✔
2039
        let address = self.get_miner_address(StacksEpochId::Epoch21, &public_key);
9,447✔
2040

2041
        let result = self
9,447✔
2042
            .get_rpc_client()
9,447✔
2043
            .generate_to_address(num_blocks, &address);
9,447✔
2044
        /*
2045
            Temporary: not using `BitcoinRpcClientResultExt::ok_or_log_panic` (test code related),
2046
            because we need this logic available outside `#[cfg(test)]` due to Helium network.
2047

2048
            After the Helium cleanup (https://github.com/stacks-network/stacks-core/issues/6408),
2049
            we can:
2050
              - move `build_next_block` behind `#[cfg(test)]`
2051
              - simplify this match by using `ok_or_log_panic`.
2052
        */
2053
        match result {
9,447✔
2054
            Ok(_) => {}
9,447✔
2055
            Err(e) => {
×
2056
                error!("Bitcoin RPC failure: error generating block {e:?}");
×
2057
                panic!();
×
2058
            }
2059
        }
2060
    }
9,447✔
2061

2062
    /// Instruct a regtest Bitcoin node to build an empty block.
2063
    #[cfg(test)]
2064
    pub fn build_empty_block(&self) {
4✔
2065
        info!("Generate empty block");
4✔
2066
        let public_key_bytes = match &self.config.burnchain.local_mining_public_key {
4✔
2067
            Some(public_key) => hex_bytes(public_key).expect("Invalid byte sequence"),
4✔
2068
            None => panic!("Unable to make new block, mining public key"),
×
2069
        };
2070

2071
        // NOTE: miner address is whatever the configured segwit setting is
2072
        let public_key = Secp256k1PublicKey::from_slice(&public_key_bytes)
4✔
2073
            .expect("FATAL: invalid public key bytes");
4✔
2074
        let address = self.get_miner_address(StacksEpochId::Epoch21, &public_key);
4✔
2075

2076
        self.get_rpc_client()
4✔
2077
            .generate_block(&address, &[])
4✔
2078
            .ok_or_log_panic("generating block")
4✔
2079
    }
4✔
2080

2081
    /// Invalidate a block given its hash as a [`BurnchainHeaderHash`].
2082
    #[cfg(test)]
2083
    pub fn invalidate_block(&self, block: &BurnchainHeaderHash) {
32✔
2084
        info!("Invalidating block {block}");
32✔
2085
        self.get_rpc_client()
32✔
2086
            .invalidate_block(block)
32✔
2087
            .ok_or_log_panic("invalidate block")
32✔
2088
    }
32✔
2089

2090
    /// Retrieve the hash (as a [`BurnchainHeaderHash`]) of the block at the given height.
2091
    #[cfg(test)]
2092
    pub fn get_block_hash(&self, height: u64) -> BurnchainHeaderHash {
37✔
2093
        self.get_rpc_client()
37✔
2094
            .get_block_hash(height)
37✔
2095
            .unwrap_or_log_panic("retrieve block")
37✔
2096
    }
37✔
2097

2098
    #[cfg(test)]
2099
    pub fn get_mining_pubkey(&self) -> Option<String> {
5✔
2100
        self.config.burnchain.local_mining_public_key.clone()
5✔
2101
    }
5✔
2102

2103
    #[cfg(test)]
2104
    pub fn set_mining_pubkey(&mut self, pubkey: String) -> Option<String> {
×
2105
        let old_key = self.config.burnchain.local_mining_public_key.take();
×
2106
        self.config.burnchain.local_mining_public_key = Some(pubkey);
×
2107
        old_key
×
2108
    }
×
2109

2110
    #[cfg(test)]
2111
    pub fn set_use_segwit(&mut self, segwit: bool) {
×
2112
        self.config.miner.segwit = segwit;
×
2113
    }
×
2114

2115
    // TODO: add tests from mutation testing results #4866
2116
    #[cfg_attr(test, mutants::skip)]
2117
    fn make_operation_tx(
25,642✔
2118
        &mut self,
25,642✔
2119
        epoch_id: StacksEpochId,
25,642✔
2120
        operation: BlockstackOperationType,
25,642✔
2121
        op_signer: &mut BurnchainOpSigner,
25,642✔
2122
    ) -> Result<Transaction, BurnchainControllerError> {
25,642✔
2123
        match operation {
25,642✔
2124
            BlockstackOperationType::LeaderBlockCommit(payload) => {
25,330✔
2125
                self.build_leader_block_commit_tx(epoch_id, payload, op_signer)
25,330✔
2126
            }
2127
            BlockstackOperationType::LeaderKeyRegister(payload) => {
288✔
2128
                self.build_leader_key_register_tx(epoch_id, payload, op_signer)
288✔
2129
            }
2130
            BlockstackOperationType::PreStx(payload) => {
13✔
2131
                self.build_pre_stacks_tx(epoch_id, payload, op_signer)
13✔
2132
            }
2133
            BlockstackOperationType::TransferStx(payload) => {
3✔
2134
                self.build_transfer_stacks_tx(epoch_id, payload, op_signer, None)
3✔
2135
            }
2136
            BlockstackOperationType::StackStx(_payload) => {
4✔
2137
                self.build_stack_stx_tx(epoch_id, _payload, op_signer, None)
4✔
2138
            }
2139
            BlockstackOperationType::DelegateStx(payload) => {
2✔
2140
                self.build_delegate_stacks_tx(epoch_id, payload, op_signer, None)
2✔
2141
            }
2142
            BlockstackOperationType::VoteForAggregateKey(payload) => {
2✔
2143
                self.build_vote_for_aggregate_key_tx(epoch_id, payload, op_signer, None)
2✔
2144
            }
2145
        }
2146
    }
25,642✔
2147

2148
    /// Retrieves a raw [`Transaction`] by its [`Txid`]
2149
    #[cfg(test)]
2150
    pub fn get_raw_transaction(&self, txid: &Txid) -> Transaction {
59✔
2151
        self.get_rpc_client()
59✔
2152
            .get_raw_transaction(txid)
59✔
2153
            .unwrap_or_log_panic("retrieve raw tx")
59✔
2154
    }
59✔
2155

2156
    /// Build, sign, and broadcast a regular Bitcoin payment from the
2157
    /// miner address to `recipient`.
2158
    ///
2159
    /// Internally this is the same UTXO-selection + signing flow used
2160
    /// by `build_leader_block_commit_tx` / `build_leader_key_register_tx`
2161
    /// — `prepare_tx` picks miner UTXOs, then `finalize_tx` adds a
2162
    /// change output, fills inputs, and signs each one with the keychain
2163
    /// `BurnchainOpSigner` the caller supplies. The resulting tx is
2164
    /// broadcast via `sendrawtransaction`; the next regtest block (e.g.
2165
    /// `build_next_block(1)`) confirms it.
2166
    ///
2167
    /// Intended for tests that need the bondholder / a third party to
2168
    /// receive BTC on regtest without relying on bitcoind's wallet to
2169
    /// sign for the miner (the wallet is created with
2170
    /// `disable_private_keys=true`, so it can't).
2171
    #[cfg(test)]
2172
    pub fn send_btc(
2✔
2173
        &mut self,
2✔
2174
        epoch_id: StacksEpochId,
2✔
2175
        op_signer: &mut BurnchainOpSigner,
2✔
2176
        recipient: &BitcoinAddress,
2✔
2177
        amount: u64,
2✔
2178
    ) -> Result<Txid, BurnchainControllerError> {
2✔
2179
        let public_key = op_signer.get_public_key();
2✔
2180

2181
        let fee_rate = get_satoshis_per_byte(&self.config);
2✔
2182
        // Rough upper-bound for a 1-input, 2-output P2PKH/P2WPKH tx. The
2183
        // `finalize_tx` flow re-serializes to compute the real size; this
2184
        // is just for UTXO selection / change accounting.
2185
        let min_tx_size: u64 = 250;
2✔
2186
        let total_required = amount + min_tx_size * fee_rate;
2✔
2187

2188
        let (mut tx, mut utxos) =
2✔
2189
            self.prepare_tx(epoch_id, &public_key, total_required, None, None, 0)?;
2✔
2190

2191
        let recipient_output = match recipient {
2✔
2192
            BitcoinAddress::Legacy(legacy) => match legacy.addrtype {
×
2193
                LegacyBitcoinAddressType::PublicKeyHash => {
2194
                    LegacyBitcoinAddress::to_p2pkh_tx_out(&legacy.bytes, amount)
×
2195
                }
2196
                LegacyBitcoinAddressType::ScriptHash => {
2197
                    LegacyBitcoinAddress::to_p2sh_tx_out(&legacy.bytes, amount)
×
2198
                }
2199
            },
2200
            BitcoinAddress::Segwit(segwit) => match segwit {
2✔
2201
                SegwitBitcoinAddress::P2WPKH(_, bytes) => {
2✔
2202
                    SegwitBitcoinAddress::to_p2wpkh_tx_out(bytes, amount)
2✔
2203
                }
2204
                SegwitBitcoinAddress::P2WSH(_, bytes) => {
×
2205
                    SegwitBitcoinAddress::to_p2wsh_tx_out(bytes, amount)
×
2206
                }
2207
                SegwitBitcoinAddress::P2TR(_, bytes) => {
×
2208
                    SegwitBitcoinAddress::to_p2tr_tx_out(bytes, amount)
×
2209
                }
2210
            },
2211
        };
2212
        tx.output = vec![recipient_output];
2✔
2213

2214
        self.finalize_tx(
2✔
2215
            epoch_id,
2✔
2216
            &mut tx,
2✔
2217
            amount,
2✔
2218
            0,
2219
            min_tx_size,
2✔
2220
            fee_rate,
2✔
2221
            &mut utxos,
2✔
2222
            op_signer,
2✔
2223
            true,
2224
        );
2225

2226
        info!(
2✔
2227
            "Test send_btc: paying {amount} sats to {recipient}";
2228
            "fee_rate" => fee_rate,
2✔
2229
        );
2230

2231
        self.send_transaction(&tx)
2✔
2232
    }
2✔
2233

2234
    /// Produce `num_blocks` regtest bitcoin blocks, sending the bitcoin coinbase rewards
2235
    ///  to the bitcoin single sig addresses corresponding to `pks` in a round robin fashion.
2236
    #[cfg(test)]
2237
    pub fn bootstrap_chain_to_pks(&self, num_blocks: u64, pks: &[Secp256k1PublicKey]) {
281✔
2238
        info!("Ensuring the test wallet is loaded, creating it if needed");
281✔
2239
        if let Err(e) = self.ensure_test_wallet_loaded() {
281✔
NEW
2240
            error!("Error ensuring wallet is loaded: {e:?}");
×
2241
        }
281✔
2242

2243
        for pk in pks {
322✔
2244
            debug!("Import public key '{}'", &pk.to_hex());
322✔
2245
            if let Err(e) = self.import_public_key(pk) {
322✔
2246
                warn!("Error when importing pubkey: {e:?}");
×
2247
            }
322✔
2248
        }
2249

2250
        if pks.len() == 1 {
281✔
2251
            // if we only have one pubkey, just generate all the blocks at once
2252
            let address = self.get_miner_address(StacksEpochId::Epoch21, &pks[0]);
240✔
2253
            debug!(
240✔
2254
                "Generate to address '{address}' for public key '{}'",
2255
                &pks[0].to_hex()
×
2256
            );
2257
            self.get_rpc_client()
240✔
2258
                .generate_to_address(num_blocks, &address)
240✔
2259
                .ok_or_log_panic("generating block");
240✔
2260
            return;
240✔
2261
        }
41✔
2262

2263
        // otherwise, round robin generate blocks
2264
        let num_blocks = num_blocks as usize;
41✔
2265
        for i in 0..num_blocks {
7,861✔
2266
            let pk = &pks[i % pks.len()];
7,861✔
2267
            let address = self.get_miner_address(StacksEpochId::Epoch21, pk);
7,861✔
2268
            if i < pks.len() {
7,861✔
2269
                debug!(
82✔
2270
                    "Generate to address '{}' for public key '{}'",
2271
                    address.to_string(),
×
2272
                    &pk.to_hex(),
×
2273
                );
2274
            }
7,779✔
2275
            self.get_rpc_client()
7,861✔
2276
                .generate_to_address(1, &address)
7,861✔
2277
                .ok_or_log_panic("generating block");
7,861✔
2278
        }
2279
    }
281✔
2280

2281
    /// Checks whether a transaction has been confirmed by the burnchain
2282
    ///
2283
    /// # Arguments
2284
    ///
2285
    /// * `txid` - The transaction ID to check (in big-endian order)
2286
    ///
2287
    /// # Returns
2288
    ///
2289
    /// * `true` if the transaction is confirmed (has at least one confirmation).
2290
    /// * `false` if the transaction is unconfirmed or could not be found.
2291
    pub fn is_transaction_confirmed(&self, txid: &Txid) -> bool {
15,972✔
2292
        match self
15,972✔
2293
            .get_rpc_client()
15,972✔
2294
            .get_transaction(self.get_wallet_name(), txid)
15,972✔
2295
        {
2296
            Ok(info) => info.confirmations > 0,
15,949✔
2297
            Err(e) => {
23✔
2298
                error!("Bitcoin RPC failure: checking tx confirmation {e:?}");
23✔
2299
                false
23✔
2300
            }
2301
        }
2302
    }
15,972✔
2303

2304
    /// Returns the configured wallet name used for wallet RPC routing.
2305
    ///
2306
    /// Panics if no wallet is configured. Only miner paths route wallet RPCs, and
2307
    /// [`Config::from_config_file`] rejects a miner without a wallet name, so an
2308
    /// absent name here is a bug. Same as [`Self::get_rpc_client`].
2309
    fn get_wallet_name(&self) -> &str {
27,535✔
2310
        self.config.burnchain.wallet_name.as_deref().expect(
27,535✔
2311
            "BUG: `burnchain.wallet_name` is required for miners, but it is not configured!",
27,535✔
2312
        )
27,535✔
2313
    }
27,535✔
2314

2315
    /// Imports a public key into configured wallet by registering its
2316
    /// corresponding addresses as descriptors.
2317
    ///
2318
    /// This computes both **legacy (P2PKH)** and, if the miner is configured
2319
    /// with `segwit` enabled, also **SegWit (P2WPKH)** addresses, then imports
2320
    /// the related descriptors into the wallet.
2321
    pub fn import_public_key(
430✔
2322
        &self,
430✔
2323
        public_key: &Secp256k1PublicKey,
430✔
2324
    ) -> BitcoinRegtestControllerResult<()> {
430✔
2325
        let pkh = Hash160::from_data(&public_key.to_bytes())
430✔
2326
            .to_bytes()
430✔
2327
            .to_vec();
430✔
2328
        let (_, network_id) = self.config.burnchain.get_bitcoin_network();
430✔
2329

2330
        // import both the legacy and segwit variants of this public key
2331
        let mut addresses = vec![BitcoinAddress::from_bytes_legacy(
430✔
2332
            network_id,
430✔
2333
            LegacyBitcoinAddressType::PublicKeyHash,
430✔
2334
            &pkh,
430✔
2335
        )
2336
        .map_err(BitcoinRegtestControllerError::InvalidPublicKey)?];
430✔
2337

2338
        if self.config.miner.segwit {
430✔
2339
            addresses.push(
1✔
2340
                BitcoinAddress::from_bytes_segwit_p2wpkh(network_id, &pkh)
1✔
2341
                    .map_err(BitcoinRegtestControllerError::InvalidPublicKey)?,
1✔
2342
            );
2343
        }
429✔
2344

2345
        for address in addresses.into_iter() {
431✔
2346
            debug!(
431✔
2347
                "Import address {address} for public key {}",
2348
                public_key.to_hex()
×
2349
            );
2350

2351
            let descriptor = format!("addr({address})");
431✔
2352
            let info = self.get_rpc_client().get_descriptor_info(&descriptor)?;
431✔
2353

2354
            let descr_req = ImportDescriptorsRequest {
431✔
2355
                descriptor: format!("addr({address})#{}", info.checksum),
431✔
2356
                timestamp: Timestamp::Time(0),
431✔
2357
                internal: Some(true),
431✔
2358
            };
431✔
2359

2360
            let results = self
431✔
2361
                .get_rpc_client()
431✔
2362
                .import_descriptors(self.get_wallet_name(), &[&descr_req])?;
431✔
2363
            // the RPC reports per-descriptor failures in the response body,
2364
            // e.g. when the target wallet has private keys enabled
2365
            for result in results {
431✔
2366
                if !result.success {
431✔
2367
                    return Err(BitcoinRegtestControllerError::ImportDescriptors(
2368
                        result.error.map_or_else(
1✔
NEW
2369
                            || format!("importing addr({address}) failed with no error message"),
×
2370
                            |e| format!("importing addr({address}) failed: {}", e.message),
1✔
2371
                        ),
2372
                    ));
2373
                }
430✔
2374
            }
2375
        }
2376
        Ok(())
429✔
2377
    }
430✔
2378

2379
    /// Returns a copy of the given public key adjusted to the current epoch rules.
2380
    ///
2381
    /// In particular:
2382
    /// - For epochs **before** [`StacksEpochId::Epoch21`], the public key is returned
2383
    ///   unchanged.
2384
    /// - Starting with [`StacksEpochId::Epoch21`], if **SegWit** is enabled in the miner
2385
    ///   configuration, the key is forced into compressed form.
2386
    ///
2387
    /// # Arguments
2388
    /// * `epoch_id` — The epoch identifier to check against protocol upgrade rules.
2389
    /// * `public_key` — The original public key to adjust.
2390
    ///
2391
    /// # Returns
2392
    /// A [`Secp256k1PublicKey`] that is either the same as the input or compressed,
2393
    /// depending on the epoch and miner configuration.
2394
    fn to_epoch_aware_pubkey(
9,966✔
2395
        &self,
9,966✔
2396
        epoch_id: StacksEpochId,
9,966✔
2397
        public_key: &Secp256k1PublicKey,
9,966✔
2398
    ) -> Secp256k1PublicKey {
9,966✔
2399
        let mut reviewed = public_key.clone();
9,966✔
2400
        if self.config.miner.segwit && epoch_id >= StacksEpochId::Epoch21 {
9,966✔
2401
            reviewed.set_compressed(true);
1✔
2402
        }
9,965✔
2403
        return reviewed;
9,966✔
2404
    }
9,966✔
2405

2406
    /// Retrieves the set of UTXOs for a given address at a specific block height.
2407
    ///
2408
    /// This method queries all unspent outputs belonging to the provided address:
2409
    /// 1. Using a confirmation window of `0..=9_999_999` for the RPC call.
2410
    /// 2. Filtering out UTXOs that:
2411
    ///    - Are present in the optional exclusion set (matched by transaction ID).
2412
    ///    - Have an amount below the specified `minimum_sum_amount`.
2413
    ///
2414
    /// Note: The `block_height` is only used to retrieve the corresponding block hash
2415
    /// and does not affect which UTXOs are included in the result.
2416
    ///
2417
    /// # Arguments
2418
    /// - `address`: The Bitcoin address whose UTXOs should be retrieved.
2419
    /// - `include_unsafe`: Whether to include unsafe UTXOs.
2420
    /// - `minimum_sum_amount`: Minimum amount (in satoshis) that a UTXO must have to be included in the final set.
2421
    /// - `utxos_to_exclude`: Optional set of UTXOs to exclude from the final result.
2422
    /// - `block_height`: The block height at which to resolve the block hash used in the result.
2423
    ///
2424
    /// # Returns
2425
    /// A [`UTXOSet`] containing the filtered UTXOs and the block hash corresponding to `block_height`.
2426
    fn retrieve_utxo_set(
10,021✔
2427
        &self,
10,021✔
2428
        address: &BitcoinAddress,
10,021✔
2429
        include_unsafe: bool,
10,021✔
2430
        minimum_sum_amount: u64,
10,021✔
2431
        utxos_to_exclude: &Option<UTXOSet>,
10,021✔
2432
        block_height: u64,
10,021✔
2433
    ) -> BitcoinRpcClientResult<UTXOSet> {
10,021✔
2434
        let bhh = self.get_rpc_client().get_block_hash(block_height)?;
10,021✔
2435

2436
        const MIN_CONFIRMATIONS: u64 = 0;
2437
        const MAX_CONFIRMATIONS: u64 = 9_999_999;
2438
        let unspents = self.get_rpc_client().list_unspent(
10,018✔
2439
            self.get_wallet_name(),
10,018✔
2440
            Some(MIN_CONFIRMATIONS),
10,018✔
2441
            Some(MAX_CONFIRMATIONS),
10,018✔
2442
            Some(&[address]),
10,018✔
2443
            Some(include_unsafe),
10,018✔
2444
            Some(minimum_sum_amount),
10,018✔
2445
            self.config.burnchain.max_unspent_utxos.clone(),
10,018✔
2446
        )?;
30✔
2447

2448
        let txids_to_exclude = utxos_to_exclude.as_ref().map_or_else(HashSet::new, |set| {
9,988✔
2449
            set.utxos
4✔
2450
                .iter()
4✔
2451
                .map(|utxo| Txid::from_bitcoin_tx_hash(&utxo.txid))
92✔
2452
                .collect()
4✔
2453
        });
4✔
2454

2455
        let utxos = unspents
9,988✔
2456
            .into_iter()
9,988✔
2457
            .filter(|each| !txids_to_exclude.contains(&each.txid))
1,063,710✔
2458
            .filter(|each| each.amount >= minimum_sum_amount)
1,063,619✔
2459
            .map(|each| UTXO {
9,988✔
2460
                txid: Txid::to_bitcoin_tx_hash(&each.txid),
1,063,593✔
2461
                vout: each.vout,
1,063,593✔
2462
                script_pub_key: each.script_pub_key,
1,063,593✔
2463
                amount: each.amount,
1,063,593✔
2464
                confirmations: each.confirmations,
1,063,593✔
2465
            })
1,063,593✔
2466
            .collect::<Vec<_>>();
9,988✔
2467
        Ok(UTXOSet { bhh, utxos })
9,988✔
2468
    }
10,021✔
2469
}
2470

2471
impl BurnchainController for BitcoinRegtestController {
2472
    fn sortdb_ref(&self) -> &SortitionDB {
2,494,904✔
2473
        self.db
2,494,904✔
2474
            .as_ref()
2,494,904✔
2475
            .expect("BUG: did not instantiate the burn DB")
2,494,904✔
2476
    }
2,494,904✔
2477

2478
    fn sortdb_mut(&mut self) -> &mut SortitionDB {
593,796✔
2479
        let burnchain = self.get_burnchain();
593,796✔
2480

2481
        let (db, burnchain_db) = burnchain.open_db(true).unwrap();
593,796✔
2482
        self.db = Some(db);
593,796✔
2483
        self.burnchain_db = Some(burnchain_db);
593,796✔
2484

2485
        match self.db {
593,796✔
2486
            Some(ref mut sortdb) => sortdb,
593,795✔
2487
            None => unreachable!(),
1✔
2488
        }
2489
    }
593,795✔
2490

2491
    fn get_chain_tip(&self) -> BurnchainTip {
×
2492
        match &self.chain_tip {
×
2493
            Some(chain_tip) => chain_tip.clone(),
×
2494
            None => {
2495
                unreachable!();
×
2496
            }
2497
        }
2498
    }
×
2499

2500
    fn get_headers_height(&self) -> u64 {
486,412✔
2501
        let (_, network_id) = self.config.burnchain.get_bitcoin_network();
486,412✔
2502
        let spv_client = SpvClient::new(
486,412✔
2503
            &self.config.get_spv_headers_file_path(),
486,412✔
2504
            0,
2505
            None,
486,412✔
2506
            network_id,
486,412✔
2507
            false,
2508
            false,
2509
        )
2510
        .expect("Unable to open burnchain headers DB");
486,412✔
2511
        spv_client
486,412✔
2512
            .get_headers_height()
486,412✔
2513
            .expect("Unable to query number of burnchain headers")
486,412✔
2514
    }
486,412✔
2515

2516
    fn connect_dbs(&mut self) -> Result<(), BurnchainControllerError> {
569✔
2517
        let burnchain = self.get_burnchain();
569✔
2518
        burnchain.connect_db(
569✔
2519
            true,
2520
            &self.indexer.get_first_block_header_hash()?,
569✔
2521
            self.indexer.get_first_block_header_timestamp()?,
569✔
2522
            self.indexer.get_stacks_epochs(),
569✔
2523
        )?;
×
2524
        Ok(())
569✔
2525
    }
569✔
2526

2527
    fn get_stacks_epochs(&self) -> EpochList {
562✔
2528
        self.indexer.get_stacks_epochs()
562✔
2529
    }
562✔
2530

2531
    fn start(
562✔
2532
        &mut self,
562✔
2533
        target_block_height_opt: Option<u64>,
562✔
2534
    ) -> Result<(BurnchainTip, u64), BurnchainControllerError> {
562✔
2535
        // if no target block height is given, just fetch the first burnchain block.
2536
        self.receive_blocks(false, target_block_height_opt.map_or_else(|| Some(1), Some))
562✔
2537
    }
562✔
2538

2539
    fn sync(
485,336✔
2540
        &mut self,
485,336✔
2541
        target_block_height_opt: Option<u64>,
485,336✔
2542
    ) -> Result<(BurnchainTip, u64), BurnchainControllerError> {
485,336✔
2543
        let (burnchain_tip, burnchain_height) = if self.config.burnchain.mode == "helium" {
485,336✔
2544
            // Helium: this node is responsible for mining new burnchain blocks
2545
            self.build_next_block(1);
×
2546
            self.receive_blocks(true, None)?
×
2547
        } else {
2548
            // Neon: this node is waiting on a block to be produced
2549
            self.receive_blocks(true, target_block_height_opt)?
485,336✔
2550
        };
2551

2552
        // Evaluate process_exit_at_block_height setting
2553
        if let Some(cap) = self.config.burnchain.process_exit_at_block_height {
485,276✔
2554
            if burnchain_tip.block_snapshot.block_height >= cap {
38✔
2555
                info!("Node succesfully reached the end of the ongoing {cap} blocks epoch!");
×
2556
                info!("This process will automatically terminate in 30s, restart your node for participating in the next epoch.");
×
2557
                sleep_ms(30000);
×
2558
                std::process::exit(0);
×
2559
            }
38✔
2560
        }
485,238✔
2561
        Ok((burnchain_tip, burnchain_height))
485,276✔
2562
    }
485,336✔
2563

2564
    /// Build and send a burnchain operation transaction.
2565
    /// Returns the [`Txid`] on success, [`BurnchainControllerError`] otherwise.
2566
    /// On [`BitcoinRegtestController::send_transaction`] failure for block commits,
2567
    /// clears `ongoing_block_commit` so the commit can be resubmitted.
2568
    fn submit_operation(
25,639✔
2569
        &mut self,
25,639✔
2570
        epoch_id: StacksEpochId,
25,639✔
2571
        operation: BlockstackOperationType,
25,639✔
2572
        op_signer: &mut BurnchainOpSigner,
25,639✔
2573
    ) -> Result<Txid, BurnchainControllerError> {
25,639✔
2574
        let is_block_commit = matches!(operation, BlockstackOperationType::LeaderBlockCommit(_));
25,639✔
2575
        let transaction = self.make_operation_tx(epoch_id, operation, op_signer)?;
25,639✔
2576
        self.send_transaction(&transaction).inspect_err(|_| {
9,914✔
2577
            if is_block_commit {
2✔
2578
                self.ongoing_block_commit = None;
2✔
2579
            }
2✔
2580
        })
2✔
2581
    }
25,639✔
2582

2583
    #[cfg(test)]
2584
    fn bootstrap_chain(&self, num_blocks: u64) {
133✔
2585
        let Some(ref local_mining_pubkey) = &self.config.burnchain.local_mining_public_key else {
133✔
2586
            warn!("No local mining pubkey while bootstrapping bitcoin regtest, will not generate bitcoin blocks");
1✔
2587
            return;
1✔
2588
        };
2589

2590
        // NOTE: miner address is whatever the miner's segwit setting says it is here
2591
        let mut local_mining_pubkey = Secp256k1PublicKey::from_hex(local_mining_pubkey).unwrap();
132✔
2592

2593
        if self.config.miner.segwit {
132✔
2594
            local_mining_pubkey.set_compressed(true);
×
2595
        }
132✔
2596

2597
        self.bootstrap_chain_to_pks(num_blocks, &[local_mining_pubkey])
132✔
2598
    }
133✔
2599
}
2600

2601
#[derive(Debug, Clone)]
2602
pub struct UTXOSet {
2603
    bhh: BurnchainHeaderHash,
2604
    utxos: Vec<UTXO>,
2605
}
2606

2607
impl UTXOSet {
2608
    pub fn is_empty(&self) -> bool {
9,906✔
2609
        self.utxos.len() == 0
9,906✔
2610
    }
9,906✔
2611

2612
    pub fn total_available(&self) -> u64 {
9,871✔
2613
        self.utxos.iter().map(|o| o.amount).sum()
9,871✔
2614
    }
9,871✔
2615

2616
    pub fn num_utxos(&self) -> usize {
6✔
2617
        self.utxos.len()
6✔
2618
    }
6✔
2619
}
2620

2621
#[derive(Clone, Debug, PartialEq)]
2622
pub struct UTXO {
2623
    pub txid: Sha256dHash,
2624
    pub vout: u32,
2625
    pub script_pub_key: Script,
2626
    pub amount: u64,
2627
    pub confirmations: u32,
2628
}
2629

2630
#[cfg(test)]
2631
mod tests {
2632
    use std::env::{self, temp_dir};
2633
    use std::fs::File;
2634
    use std::io::Write;
2635
    use std::panic::{self, AssertUnwindSafe};
2636

2637
    use stacks::burnchains::BurnchainSigner;
2638
    use stacks::config::DEFAULT_SATS_PER_VB;
2639
    use stacks_common::deps_common::bitcoin::blockdata::script::Builder;
2640
    use stacks_common::types::chainstate::{BlockHeaderHash, StacksAddress, VRFSeed};
2641
    use stacks_common::util::hash::to_hex;
2642
    use stacks_common::util::secp256k1::Secp256k1PrivateKey;
2643

2644
    use super::*;
2645
    use crate::burnchains::bitcoin::core_controller::BitcoinCoreController;
2646
    use crate::burnchains::bitcoin_regtest_controller::tests::utils::{
2647
        create_follower_config, create_miner_config, to_address_legacy,
2648
    };
2649
    use crate::Keychain;
2650

2651
    mod utils {
2652
        use std::net::TcpListener;
2653

2654
        use stacks::burnchains::MagicBytes;
2655
        use stacks::chainstate::burn::ConsensusHash;
2656
        use stacks::util::vrf::{VRFPrivateKey, VRFPublicKey};
2657

2658
        use super::*;
2659
        use crate::burnchains::bitcoin::core_controller::BURNCHAIN_CONFIG_PEER_PORT_DISABLED;
2660
        use crate::util::get_epoch_time_nanos;
2661

2662
        pub fn create_miner_config() -> Config {
41✔
2663
            let mut config = Config::default();
41✔
2664
            config.node.miner = true;
41✔
2665
            config.burnchain.wallet_name = Some("test-miner".to_string());
41✔
2666
            config.burnchain.magic_bytes = "T3".as_bytes().into();
41✔
2667
            config.burnchain.username = Some(String::from("user"));
41✔
2668
            config.burnchain.password = Some(String::from("12345"));
41✔
2669
            // overriding default "0.0.0.0" because doesn't play nicely on Windows.
2670
            config.burnchain.peer_host = String::from("127.0.0.1");
41✔
2671
            // avoiding peer port biding to reduce the number of ports to bind to.
2672
            config.burnchain.peer_port = BURNCHAIN_CONFIG_PEER_PORT_DISABLED;
41✔
2673

2674
            //Ask the OS for a free port. Not guaranteed to stay free,
2675
            //after TcpListner is dropped, but good enough for testing
2676
            //and starting bitcoind right after config is created
2677
            let tmp_listener =
41✔
2678
                TcpListener::bind("127.0.0.1:0").expect("Failed to bind to get a free port");
41✔
2679
            let port = tmp_listener.local_addr().unwrap().port();
41✔
2680

2681
            config.burnchain.rpc_port = port;
41✔
2682

2683
            let now = get_epoch_time_nanos();
41✔
2684
            let dir = format!("/tmp/regtest-ctrl-{port}-{now}");
41✔
2685
            config.node.working_dir = dir;
41✔
2686

2687
            config
41✔
2688
        }
41✔
2689

2690
        pub fn create_keychain() -> Keychain {
16✔
2691
            create_keychain_with_seed(1)
16✔
2692
        }
16✔
2693

2694
        pub fn create_keychain_with_seed(value: u8) -> Keychain {
35✔
2695
            let seed = vec![value; 4];
35✔
2696
            let keychain = Keychain::default(seed);
35✔
2697
            keychain
35✔
2698
        }
35✔
2699

2700
        pub fn create_miner1_pubkey() -> Secp256k1PublicKey {
17✔
2701
            create_keychain_with_seed(1).get_pub_key()
17✔
2702
        }
17✔
2703

2704
        pub fn create_miner2_pubkey() -> Secp256k1PublicKey {
2✔
2705
            create_keychain_with_seed(2).get_pub_key()
2✔
2706
        }
2✔
2707

2708
        pub fn to_address_legacy(pub_key: &Secp256k1PublicKey) -> BitcoinAddress {
6✔
2709
            let hash160 = Hash160::from_data(&pub_key.to_bytes());
6✔
2710
            BitcoinAddress::from_bytes_legacy(
6✔
2711
                BitcoinNetworkType::Regtest,
6✔
2712
                LegacyBitcoinAddressType::PublicKeyHash,
6✔
2713
                &hash160.0,
6✔
2714
            )
2715
            .expect("Public key incorrect")
6✔
2716
        }
6✔
2717

2718
        pub fn to_address_segwit_p2wpkh(pub_key: &Secp256k1PublicKey) -> BitcoinAddress {
1✔
2719
            // pub_key.to_byte_compressed() equivalent to pub_key.set_compressed(true) + pub_key.to_bytes()
2720
            let hash160 = Hash160::from_data(&pub_key.to_bytes_compressed());
1✔
2721
            BitcoinAddress::from_bytes_segwit_p2wpkh(BitcoinNetworkType::Regtest, &hash160.0)
1✔
2722
                .expect("Public key incorrect")
1✔
2723
        }
1✔
2724

2725
        pub fn mine_tx(btc_controller: &BitcoinRegtestController, tx: &Transaction) {
2✔
2726
            btc_controller
2✔
2727
                .send_transaction(tx)
2✔
2728
                .expect("Tx should be sent to the burnchain!");
2✔
2729
            btc_controller.build_next_block(1); // Now tx is confirmed
2✔
2730
        }
2✔
2731

2732
        pub fn create_templated_commit_op() -> LeaderBlockCommitOp {
8✔
2733
            LeaderBlockCommitOp {
8✔
2734
                block_header_hash: BlockHeaderHash::from_hex(
8✔
2735
                    "e88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32af",
8✔
2736
                )
8✔
2737
                .unwrap(),
8✔
2738
                new_seed: VRFSeed::from_hex(
8✔
2739
                    "d5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375",
8✔
2740
                )
8✔
2741
                .unwrap(),
8✔
2742
                parent_block_ptr: 2211, // 0x000008a3
8✔
2743
                parent_vtxindex: 1,     // 0x0001
8✔
2744
                key_block_ptr: 1432,    // 0x00000598
8✔
2745
                key_vtxindex: 1,        // 0x0001
8✔
2746
                memo: vec![11],         // 0x5a >> 3
8✔
2747

8✔
2748
                burn_fee: 110_000, //relevant for fee calculation when sending the tx
8✔
2749
                input: (Txid([0x00; 32]), 0),
8✔
2750
                burn_parent_modulus: 2, // 0x5a & 0b111
8✔
2751

8✔
2752
                apparent_sender: BurnchainSigner("mgbpit8FvkVJ9kuXY8QSM5P7eibnhcEMBk".to_string()),
8✔
2753
                commit_outs: vec![
8✔
2754
                    PoxAddress::Standard(StacksAddress::burn_address(false), None),
8✔
2755
                    PoxAddress::Standard(StacksAddress::burn_address(false), None),
8✔
2756
                ],
8✔
2757

8✔
2758
                treatment: vec![],
8✔
2759
                sunset_burn: 5_500, //relevant for fee calculation when sending the tx
8✔
2760

8✔
2761
                txid: Txid([0x00; 32]),
8✔
2762
                vtxindex: 0,
8✔
2763
                block_height: 2212,
8✔
2764
                burn_header_hash: BurnchainHeaderHash([0x01; 32]),
8✔
2765
            }
8✔
2766
        }
8✔
2767

2768
        pub fn txout_opreturn<T: StacksMessageCodec>(
5✔
2769
            op: &T,
5✔
2770
            magic: &MagicBytes,
5✔
2771
            value: u64,
5✔
2772
        ) -> TxOut {
5✔
2773
            let op_bytes = {
5✔
2774
                let mut buffer = vec![];
5✔
2775
                let mut magic_bytes = magic.as_bytes().to_vec();
5✔
2776
                buffer.append(&mut magic_bytes);
5✔
2777
                op.consensus_serialize(&mut buffer)
5✔
2778
                    .expect("FATAL: invalid operation");
5✔
2779
                buffer
5✔
2780
            };
2781

2782
            TxOut {
5✔
2783
                value,
5✔
2784
                script_pubkey: Builder::new()
5✔
2785
                    .push_opcode(opcodes::All::OP_RETURN)
5✔
2786
                    .push_slice(&op_bytes)
5✔
2787
                    .into_script(),
5✔
2788
            }
5✔
2789
        }
5✔
2790

2791
        pub fn txout_opdup_commit_to(addr: &PoxAddress, amount: u64) -> TxOut {
6✔
2792
            addr.to_bitcoin_tx_out(amount)
6✔
2793
        }
6✔
2794

2795
        pub fn txout_opdup_change_legacy(signer: &mut BurnchainOpSigner, amount: u64) -> TxOut {
5✔
2796
            let public_key = signer.get_public_key();
5✔
2797
            let change_address_hash = Hash160::from_data(&public_key.to_bytes());
5✔
2798
            LegacyBitcoinAddress::to_p2pkh_tx_out(&change_address_hash, amount)
5✔
2799
        }
5✔
2800

2801
        pub fn txin_at_index(
5✔
2802
            complete_tx: &Transaction,
5✔
2803
            signer: &BurnchainOpSigner,
5✔
2804
            utxos: &[UTXO],
5✔
2805
            index: usize,
5✔
2806
        ) -> TxIn {
5✔
2807
            //Refresh op signer
2808
            let mut signer = signer.undisposed();
5✔
2809
            let mut public_key = signer.get_public_key();
5✔
2810

2811
            let mut tx = Transaction {
5✔
2812
                version: complete_tx.version,
5✔
2813
                lock_time: complete_tx.lock_time,
5✔
2814
                input: vec![],
5✔
2815
                output: complete_tx.output.clone(),
5✔
2816
            };
5✔
2817

2818
            for utxo in utxos.iter() {
5✔
2819
                let input = TxIn {
5✔
2820
                    previous_output: OutPoint {
5✔
2821
                        txid: utxo.txid.clone(),
5✔
2822
                        vout: utxo.vout,
5✔
2823
                    },
5✔
2824
                    script_sig: Script::new(),
5✔
2825
                    sequence: 0xFFFFFFFD, // allow RBF
5✔
2826
                    witness: vec![],
5✔
2827
                };
5✔
2828
                tx.input.push(input);
5✔
2829
            }
5✔
2830

2831
            for (i, utxo) in utxos.iter().enumerate() {
5✔
2832
                let script_pub_key = utxo.script_pub_key.clone();
5✔
2833
                let sig_hash_all = 0x01;
5✔
2834

2835
                let (sig_hash, is_segwit) = if script_pub_key.as_bytes().len() == 22
5✔
2836
                    && script_pub_key.as_bytes()[0..2] == [0x00, 0x14]
×
2837
                {
2838
                    // p2wpkh
2839
                    (
×
2840
                        tx.segwit_signature_hash(i, &script_pub_key, utxo.amount, sig_hash_all),
×
2841
                        true,
×
2842
                    )
×
2843
                } else {
2844
                    // p2pkh
2845
                    (tx.signature_hash(i, &script_pub_key, sig_hash_all), false)
5✔
2846
                };
2847

2848
                let sig1_der = {
5✔
2849
                    let message = signer
5✔
2850
                        .sign_message(sig_hash.as_bytes())
5✔
2851
                        .expect("Unable to sign message");
5✔
2852
                    message
5✔
2853
                        .to_secp256k1_recoverable()
5✔
2854
                        .expect("Unable to get recoverable signature")
5✔
2855
                        .to_standard()
5✔
2856
                        .serialize_der()
5✔
2857
                };
2858

2859
                if is_segwit {
5✔
2860
                    // segwit
×
2861
                    public_key.set_compressed(true);
×
2862
                    tx.input[i].script_sig = Script::from(vec![]);
×
2863
                    tx.input[i].witness = vec![
×
2864
                        [&*sig1_der, &[sig_hash_all as u8][..]].concat().to_vec(),
×
2865
                        public_key.to_bytes(),
×
2866
                    ];
×
2867
                } else {
5✔
2868
                    // legacy scriptSig
5✔
2869
                    tx.input[i].script_sig = Builder::new()
5✔
2870
                        .push_slice(&[&*sig1_der, &[sig_hash_all as u8][..]].concat())
5✔
2871
                        .push_slice(&public_key.to_bytes())
5✔
2872
                        .into_script();
5✔
2873
                    tx.input[i].witness.clear();
5✔
2874
                }
5✔
2875
            }
2876

2877
            tx.input[index].clone()
5✔
2878
        }
5✔
2879

2880
        pub fn create_templated_leader_key_op() -> LeaderKeyRegisterOp {
4✔
2881
            LeaderKeyRegisterOp {
4✔
2882
                consensus_hash: ConsensusHash([0u8; 20]),
4✔
2883
                public_key: VRFPublicKey::from_private(
4✔
2884
                    &VRFPrivateKey::from_bytes(&[0u8; 32]).unwrap(),
4✔
2885
                ),
4✔
2886
                memo: vec![],
4✔
2887
                txid: Txid([3u8; 32]),
4✔
2888
                vtxindex: 0,
4✔
2889
                block_height: 1,
4✔
2890
                burn_header_hash: BurnchainHeaderHash([9u8; 32]),
4✔
2891
            }
4✔
2892
        }
4✔
2893

2894
        pub fn create_templated_pre_stx_op() -> PreStxOp {
4✔
2895
            PreStxOp {
4✔
2896
                output: StacksAddress::p2pkh_from_hash(false, Hash160::from_data(&[2u8; 20])),
4✔
2897
                txid: Txid([0u8; 32]),
4✔
2898
                vtxindex: 0,
4✔
2899
                block_height: 0,
4✔
2900
                burn_header_hash: BurnchainHeaderHash([0u8; 32]),
4✔
2901
            }
4✔
2902
        }
4✔
2903

2904
        pub fn create_follower_config() -> Config {
2✔
2905
            let mut config = Config::default();
2✔
2906
            config.node.miner = false;
2✔
2907
            config.burnchain.magic_bytes = "T3".as_bytes().into();
2✔
2908
            config.burnchain.username = None;
2✔
2909
            config.burnchain.password = None;
2✔
2910
            config.burnchain.peer_host = String::from("127.0.0.1");
2✔
2911
            config.burnchain.peer_port = 8333;
2✔
2912
            config.node.working_dir = format!("/tmp/follower");
2✔
2913
            config
2✔
2914
        }
2✔
2915
    }
2916

2917
    #[test]
2918
    fn test_get_satoshis_per_byte() {
1✔
2919
        let dir = temp_dir();
1✔
2920
        let file_path = dir.as_path().join("config.toml");
1✔
2921

2922
        let mut config = Config::default();
1✔
2923

2924
        let satoshis_per_byte = get_satoshis_per_byte(&config);
1✔
2925
        assert_eq!(satoshis_per_byte, DEFAULT_SATS_PER_VB);
1✔
2926

2927
        let mut file = File::create(&file_path).unwrap();
1✔
2928
        writeln!(file, "[burnchain]").unwrap();
1✔
2929
        writeln!(file, "satoshis_per_byte = 51").unwrap();
1✔
2930
        config.config_path = Some(file_path.to_str().unwrap().to_string());
1✔
2931

2932
        assert_eq!(get_satoshis_per_byte(&config), 51);
1✔
2933
    }
1✔
2934

2935
    /// Verify that we can build a valid Bitcoin transaction with multiple UTXOs.
2936
    /// Taken from production data.
2937
    /// Tests `serialize_tx()` and `send_block_commit_operation_at_burnchain_height()`
2938
    #[test]
2939
    fn test_multiple_inputs() {
1✔
2940
        let spend_utxos = vec![
1✔
2941
            UTXO {
1✔
2942
                txid: Sha256dHash::from_hex(
1✔
2943
                    "d3eafb3aba3cec925473550ed2e4d00bcb0d00744bb3212e4a8e72878909daee",
1✔
2944
                )
1✔
2945
                .unwrap(),
1✔
2946
                vout: 3,
1✔
2947
                script_pub_key: Builder::from(
1✔
2948
                    hex_bytes("76a9141dc27eba0247f8cc9575e7d45e50a0bc7e72427d88ac").unwrap(),
1✔
2949
                )
1✔
2950
                .into_script(),
1✔
2951
                amount: 42051,
1✔
2952
                confirmations: 1421,
1✔
2953
            },
1✔
2954
            UTXO {
1✔
2955
                txid: Sha256dHash::from_hex(
1✔
2956
                    "01132f2d4a98cc715624e033214c8d841098a1ee15b30188ab89589a320b3b24",
1✔
2957
                )
1✔
2958
                .unwrap(),
1✔
2959
                vout: 0,
1✔
2960
                script_pub_key: Builder::from(
1✔
2961
                    hex_bytes("76a9141dc27eba0247f8cc9575e7d45e50a0bc7e72427d88ac").unwrap(),
1✔
2962
                )
1✔
2963
                .into_script(),
1✔
2964
                amount: 326456,
1✔
2965
                confirmations: 1421,
1✔
2966
            },
1✔
2967
        ];
2968

2969
        // test serialize_tx()
2970
        let config = utils::create_miner_config();
1✔
2971

2972
        let mut btc_controller = BitcoinRegtestController::new(config, None);
1✔
2973
        let mut utxo_set = UTXOSet {
1✔
2974
            bhh: BurnchainHeaderHash([0x01; 32]),
1✔
2975
            utxos: spend_utxos.clone(),
1✔
2976
        };
1✔
2977
        let mut transaction = Transaction {
1✔
2978
            input: vec![],
1✔
2979
            output: vec![
1✔
2980
                TxOut {
1✔
2981
                    value: 0,
1✔
2982
                    script_pubkey: Builder::from(hex_bytes("6a4c5054335be88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32afd5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375000008a300010000059800015a").unwrap()).into_script(),
1✔
2983
                },
1✔
2984
                TxOut {
1✔
2985
                    value: 10000,
1✔
2986
                    script_pubkey: Builder::from(hex_bytes("76a914000000000000000000000000000000000000000088ac").unwrap()).into_script(),
1✔
2987
                },
1✔
2988
                TxOut {
1✔
2989
                    value: 10000,
1✔
2990
                    script_pubkey: Builder::from(hex_bytes("76a914000000000000000000000000000000000000000088ac").unwrap()).into_script(),
1✔
2991
                },
1✔
2992
            ],
1✔
2993
            version: 1,
1✔
2994
            lock_time: 0,
1✔
2995
        };
1✔
2996

2997
        let mut signer = BurnchainOpSigner::new(
1✔
2998
            Secp256k1PrivateKey::from_hex(
1✔
2999
                "9e446f6b0c6a96cf2190e54bcd5a8569c3e386f091605499464389b8d4e0bfc201",
1✔
3000
            )
3001
            .unwrap(),
1✔
3002
        );
3003
        assert!(btc_controller.serialize_tx(
1✔
3004
            StacksEpochId::Epoch25,
1✔
3005
            &mut transaction,
1✔
3006
            44950,
3007
            &mut utxo_set,
1✔
3008
            &mut signer,
1✔
3009
            true
3010
        ));
3011
        assert_eq!(transaction.output[3].value, 323557);
1✔
3012

3013
        // test send_block_commit_operation_at_burn_height()
3014
        let utxo_set = UTXOSet {
1✔
3015
            bhh: BurnchainHeaderHash([0x01; 32]),
1✔
3016
            utxos: spend_utxos,
1✔
3017
        };
1✔
3018

3019
        let commit_op = LeaderBlockCommitOp {
1✔
3020
            block_header_hash: BlockHeaderHash::from_hex(
1✔
3021
                "e88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32af",
1✔
3022
            )
1✔
3023
            .unwrap(),
1✔
3024
            new_seed: VRFSeed::from_hex(
1✔
3025
                "d5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375",
1✔
3026
            )
1✔
3027
            .unwrap(),
1✔
3028
            parent_block_ptr: 2211, // 0x000008a3
1✔
3029
            parent_vtxindex: 1,     // 0x0001
1✔
3030
            key_block_ptr: 1432,    // 0x00000598
1✔
3031
            key_vtxindex: 1,        // 0x0001
1✔
3032
            memo: vec![11],         // 0x5a >> 3
1✔
3033

1✔
3034
            burn_fee: 0,
1✔
3035
            input: (Txid([0x00; 32]), 0),
1✔
3036
            burn_parent_modulus: 2, // 0x5a & 0b111
1✔
3037

1✔
3038
            apparent_sender: BurnchainSigner("mgbpit8FvkVJ9kuXY8QSM5P7eibnhcEMBk".to_string()),
1✔
3039
            commit_outs: vec![
1✔
3040
                PoxAddress::Standard(StacksAddress::burn_address(false), None),
1✔
3041
                PoxAddress::Standard(StacksAddress::burn_address(false), None),
1✔
3042
            ],
1✔
3043

1✔
3044
            treatment: vec![],
1✔
3045
            sunset_burn: 0,
1✔
3046

1✔
3047
            txid: Txid([0x00; 32]),
1✔
3048
            vtxindex: 0,
1✔
3049
            block_height: 2212,
1✔
3050
            burn_header_hash: BurnchainHeaderHash([0x01; 32]),
1✔
3051
        };
1✔
3052

3053
        assert_eq!(to_hex(&commit_op.serialize_to_vec()), "5be88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32afd5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375000008a300010000059800015a".to_string());
1✔
3054

3055
        let leader_fees = LeaderBlockCommitFees {
1✔
3056
            sunset_fee: 0,
1✔
3057
            fee_rate: 50,
1✔
3058
            sortition_fee: 20000,
1✔
3059
            outputs_len: 2,
1✔
3060
            default_tx_size: 380,
1✔
3061
            spent_in_attempts: 0,
1✔
3062
            is_rbf_enabled: false,
1✔
3063
            final_size: 498,
1✔
3064
        };
1✔
3065

3066
        assert_eq!(leader_fees.amount_per_output(), 10000);
1✔
3067
        assert_eq!(leader_fees.total_spent(), 44900);
1✔
3068

3069
        let block_commit = btc_controller
1✔
3070
            .send_block_commit_operation_at_burnchain_height(
1✔
3071
                StacksEpochId::Epoch30,
1✔
3072
                commit_op,
1✔
3073
                &mut signer,
1✔
3074
                Some(utxo_set),
1✔
3075
                None,
1✔
3076
                leader_fees,
1✔
3077
                &[],
1✔
3078
                2212,
3079
            )
3080
            .unwrap();
1✔
3081

3082
        debug!("send_block_commit_operation:\n{block_commit:#?}");
1✔
3083
        assert_eq!(block_commit.output[3].value, 323507);
1✔
3084
        assert_eq!(serialize_hex(&block_commit).unwrap(), "0100000002eeda098987728e4a2e21b34b74000dcb0bd0e4d20e55735492ec3cba3afbead3030000006a4730440220558286e20e10ce31537f0625dae5cc62fac7961b9d2cf272c990de96323d7e2502202255adbea3d2e0509b80c5d8a3a4fe6397a87bcf18da1852740d5267d89a0cb20121035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7cfdffffff243b0b329a5889ab8801b315eea19810848d4c2133e0245671cc984a2d2f1301000000006a47304402206d9f8de107f9e1eb15aafac66c2bb34331a7523260b30e18779257e367048d34022013c7dabb32a5c281aa00d405e2ccbd00f34f03a65b2336553a4acd6c52c251ef0121035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7cfdffffff040000000000000000536a4c5054335be88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32afd5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375000008a300010000059800015a10270000000000001976a914000000000000000000000000000000000000000088ac10270000000000001976a914000000000000000000000000000000000000000088acb3ef0400000000001976a9141dc27eba0247f8cc9575e7d45e50a0bc7e72427d88ac00000000");
1✔
3085
    }
1✔
3086

3087
    #[test]
3088
    fn test_to_epoch_aware_pubkey() {
1✔
3089
        let mut config = utils::create_miner_config();
1✔
3090
        let pubkey = utils::create_miner1_pubkey();
1✔
3091

3092
        config.miner.segwit = false;
1✔
3093
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3094

3095
        let reviewed = btc_controller.to_epoch_aware_pubkey(StacksEpochId::Epoch20, &pubkey);
1✔
3096
        assert_eq!(
1✔
3097
            false,
3098
            reviewed.compressed(),
1✔
3099
            "Segwit disabled with Epoch < 2.1: not compressed"
3100
        );
3101
        let reviewed = btc_controller.to_epoch_aware_pubkey(StacksEpochId::Epoch21, &pubkey);
1✔
3102
        assert_eq!(
1✔
3103
            false,
3104
            reviewed.compressed(),
1✔
3105
            "Segwit disabled with Epoch >= 2.1: not compressed"
3106
        );
3107

3108
        config.miner.segwit = true;
1✔
3109
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3110

3111
        let reviewed = btc_controller.to_epoch_aware_pubkey(StacksEpochId::Epoch20, &pubkey);
1✔
3112
        assert_eq!(
1✔
3113
            false,
3114
            reviewed.compressed(),
1✔
3115
            "Segwit enabled with Epoch < 2.1: not compressed"
3116
        );
3117
        let reviewed = btc_controller.to_epoch_aware_pubkey(StacksEpochId::Epoch21, &pubkey);
1✔
3118
        assert_eq!(
1✔
3119
            true,
3120
            reviewed.compressed(),
1✔
3121
            "Segwit enabled with Epoch >= 2.1: compressed"
3122
        );
3123
    }
1✔
3124

3125
    #[test]
3126
    fn test_get_miner_address() {
1✔
3127
        let mut config = utils::create_miner_config();
1✔
3128
        let pub_key = utils::create_miner1_pubkey();
1✔
3129

3130
        config.miner.segwit = false;
1✔
3131
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3132

3133
        let expected = utils::to_address_legacy(&pub_key);
1✔
3134
        let address = btc_controller.get_miner_address(StacksEpochId::Epoch20, &pub_key);
1✔
3135
        assert_eq!(
1✔
3136
            expected, address,
3137
            "Segwit disabled with Epoch < 2.1: legacy addr"
3138
        );
3139

3140
        let expected = utils::to_address_legacy(&pub_key);
1✔
3141
        let address = btc_controller.get_miner_address(StacksEpochId::Epoch21, &pub_key);
1✔
3142
        assert_eq!(
1✔
3143
            expected, address,
3144
            "Segwit disabled with Epoch >= 2.1: legacy addr"
3145
        );
3146

3147
        config.miner.segwit = true;
1✔
3148
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3149

3150
        let expected = utils::to_address_legacy(&pub_key);
1✔
3151
        let address = btc_controller.get_miner_address(StacksEpochId::Epoch20, &pub_key);
1✔
3152
        assert_eq!(
1✔
3153
            expected, address,
3154
            "Segwit enabled with Epoch < 2.1: legacy addr"
3155
        );
3156

3157
        let expected = utils::to_address_segwit_p2wpkh(&pub_key);
1✔
3158
        let address = btc_controller.get_miner_address(StacksEpochId::Epoch21, &pub_key);
1✔
3159
        assert_eq!(
1✔
3160
            expected, address,
3161
            "Segwit enabled with Epoch >= 2.1: segwit addr"
3162
        );
3163
    }
1✔
3164

3165
    #[test]
3166
    fn test_instantiate_with_burnchain_on_follower_node_ok() {
1✔
3167
        let config = create_follower_config();
1✔
3168

3169
        let btc_controller = BitcoinRegtestController::with_burnchain(config, None, None, None);
1✔
3170

3171
        let result = panic::catch_unwind(AssertUnwindSafe(|| {
1✔
3172
            _ = btc_controller.get_rpc_client();
1✔
3173
        }));
1✔
3174
        assert!(
1✔
3175
            result.is_err(),
1✔
3176
            "Invoking any Bitcoin RPC related method should panic."
3177
        );
3178
    }
1✔
3179

3180
    #[test]
3181
    fn test_instantiate_with_burnchain_on_miner_node_ok() {
1✔
3182
        let config = create_miner_config();
1✔
3183

3184
        let btc_controller = BitcoinRegtestController::with_burnchain(config, None, None, None);
1✔
3185

3186
        let _ = btc_controller.get_rpc_client();
1✔
3187
        assert!(true, "Invoking any Bitcoin RPC related method should work.");
1✔
3188
    }
1✔
3189

3190
    #[test]
3191
    fn test_instantiate_with_burnchain_on_miner_node_failure() {
1✔
3192
        let mut config = create_miner_config();
1✔
3193
        config.burnchain.username = None;
1✔
3194
        config.burnchain.password = None;
1✔
3195

3196
        let result = panic::catch_unwind(|| {
1✔
3197
            _ = BitcoinRegtestController::with_burnchain(config, None, None, None);
1✔
3198
        });
1✔
3199
        assert!(
1✔
3200
            result.is_err(),
1✔
3201
            "Bitcoin RPC credentials are mandatory for miner node."
3202
        );
3203
    }
1✔
3204

3205
    #[test]
3206
    fn test_instantiate_new_dummy_on_follower_node_ok() {
1✔
3207
        let config = create_follower_config();
1✔
3208

3209
        let btc_controller = BitcoinRegtestController::new_dummy(config);
1✔
3210

3211
        let result = panic::catch_unwind(AssertUnwindSafe(|| {
1✔
3212
            _ = btc_controller.get_rpc_client();
1✔
3213
        }));
1✔
3214
        assert!(
1✔
3215
            result.is_err(),
1✔
3216
            "Invoking any Bitcoin RPC related method should panic."
3217
        );
3218
    }
1✔
3219

3220
    #[test]
3221
    fn test_instantiate_new_dummy_on_miner_node_ok() {
1✔
3222
        let config = create_miner_config();
1✔
3223

3224
        let btc_controller = BitcoinRegtestController::new_dummy(config);
1✔
3225

3226
        let _ = btc_controller.get_rpc_client();
1✔
3227
        assert!(true, "Invoking any Bitcoin RPC related method should work.");
1✔
3228
    }
1✔
3229

3230
    #[test]
3231
    fn test_instantiate_new_dummy_on_miner_node_failure() {
1✔
3232
        let mut config = create_miner_config();
1✔
3233
        config.burnchain.username = None;
1✔
3234
        config.burnchain.password = None;
1✔
3235

3236
        let result = panic::catch_unwind(|| {
1✔
3237
            _ = BitcoinRegtestController::new_dummy(config);
1✔
3238
        });
1✔
3239
        assert!(
1✔
3240
            result.is_err(),
1✔
3241
            "Bitcoin RPC credentials are mandatory for miner node."
3242
        );
3243
    }
1✔
3244

3245
    // `Config::from_config_file` rejects a miner without a wallet name, so
3246
    // reaching a wallet RPC without one is a bug, not a recoverable error
3247
    #[test]
3248
    #[should_panic(expected = "burnchain.wallet_name")]
3249
    fn test_ensure_wallet_loaded_panics_without_wallet_name() {
1✔
3250
        let mut config = utils::create_miner_config();
1✔
3251
        config.burnchain.wallet_name = None;
1✔
3252

3253
        let btc_controller = BitcoinRegtestController::new(config, None);
1✔
3254

3255
        _ = btc_controller.ensure_wallet_loaded();
1✔
3256
    }
1✔
3257

3258
    #[test]
3259
    #[ignore]
3260
    fn test_ensure_wallet_loaded_reloads_unloaded_wallet() {
1✔
3261
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3262
            return;
×
3263
        }
1✔
3264

3265
        let config = utils::create_miner_config();
1✔
3266
        let wallet_name = config
1✔
3267
            .burnchain
1✔
3268
            .wallet_name
1✔
3269
            .clone()
1✔
3270
            .expect("miner config sets a wallet name");
1✔
3271

3272
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3273
        btcd_controller
1✔
3274
            .start_bitcoind()
1✔
3275
            .expect("bitcoind should be started!");
1✔
3276

3277
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3278
        btc_controller
1✔
3279
            .ensure_test_wallet_loaded()
1✔
3280
            .expect("Test wallet should be created!");
1✔
3281

3282
        // simulate a bitcoind restart, after which no wallet is loaded
3283
        btc_controller
1✔
3284
            .get_rpc_client()
1✔
3285
            .unload_wallet(&wallet_name)
1✔
3286
            .expect("Wallet should be unloaded!");
1✔
3287
        assert_eq!(0, btc_controller.list_wallets().unwrap().len());
1✔
3288

3289
        btc_controller
1✔
3290
            .ensure_wallet_loaded()
1✔
3291
            .expect("Wallet should be loaded from disk, not re-created!");
1✔
3292

3293
        let wallets = btc_controller.list_wallets().unwrap();
1✔
3294
        assert_eq!(vec![wallet_name], wallets);
1✔
3295
    }
1✔
3296

3297
    #[test]
3298
    #[ignore]
3299
    fn test_wallet_rpcs_route_to_configured_wallet() {
1✔
3300
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3301
            return;
×
3302
        }
1✔
3303

3304
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3305

3306
        let config = utils::create_miner_config();
1✔
3307

3308
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3309
        btcd_controller
1✔
3310
            .start_bitcoind()
1✔
3311
            .expect("bitcoind should be started!");
1✔
3312

3313
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3314
        btc_controller
1✔
3315
            .ensure_test_wallet_loaded()
1✔
3316
            .expect("Test wallet should be created!");
1✔
3317

3318
        // A second loaded wallet breaks node-level routing. The miner's RPCs
3319
        // must continue to use its configured wallet.
3320
        btc_controller
1✔
3321
            .get_rpc_client()
1✔
3322
            .create_wallet("other_wallet", Some(true))
1✔
3323
            .expect("other_wallet should be created!");
1✔
3324

3325
        btc_controller
1✔
3326
            .import_public_key(&miner_pubkey)
1✔
3327
            .expect("Import should succeed via explicit routing to the configured wallet!");
1✔
3328
    }
1✔
3329

3330
    #[test]
3331
    #[ignore]
3332
    fn test_import_public_key_fails_on_wallet_with_private_keys() {
1✔
3333
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
NEW
3334
            return;
×
3335
        }
1✔
3336

3337
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3338

3339
        let mut config = utils::create_miner_config();
1✔
3340
        config.burnchain.wallet_name = Some("keyed".to_string());
1✔
3341

3342
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3343
        btcd_controller
1✔
3344
            .start_bitcoind()
1✔
3345
            .expect("bitcoind should be started!");
1✔
3346

3347
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3348
        btc_controller
1✔
3349
            .get_rpc_client()
1✔
3350
            .create_wallet("keyed", Some(false))
1✔
3351
            .expect("keyed wallet should be created!");
1✔
3352

3353
        btc_controller
1✔
3354
            .ensure_wallet_loaded()
1✔
3355
            .expect("Configured wallet should already be loaded!");
1✔
3356

3357
        let result = btc_controller.import_public_key(&miner_pubkey);
1✔
3358
        assert!(
1✔
NEW
3359
            matches!(
×
3360
                result,
1✔
3361
                Err(BitcoinRegtestControllerError::ImportDescriptors(_))
3362
            ),
3363
            "Watch-only import into a wallet with private keys should fail, got: {result:?}"
3364
        );
3365
    }
1✔
3366

3367
    #[test]
3368
    #[ignore]
3369
    fn test_ensure_wallet_loaded_fails_if_wallet_missing() {
1✔
3370
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
NEW
3371
            return;
×
3372
        }
1✔
3373

3374
        let mut config = utils::create_miner_config();
1✔
3375
        config.burnchain.wallet_name = Some(String::from("mywallet"));
1✔
3376

3377
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3378
        btcd_controller
1✔
3379
            .start_bitcoind()
1✔
3380
            .expect("bitcoind should be started!");
1✔
3381

3382
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3383

3384
        assert!(matches!(
1✔
3385
            btc_controller.ensure_wallet_loaded(),
1✔
3386
            Err(BitcoinRegtestControllerError::WalletNotFound(ref name)) if name == "mywallet"
1✔
3387
        ));
3388
        assert!(btc_controller.list_wallets().unwrap().is_empty());
1✔
3389
    }
1✔
3390

3391
    #[test]
3392
    #[ignore]
3393
    fn test_retrieve_utxo_set_with_all_utxos() {
1✔
3394
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3395
            return;
×
3396
        }
1✔
3397

3398
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3399

3400
        let mut config = utils::create_miner_config();
1✔
3401
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3402

3403
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3404
        btcd_controller
1✔
3405
            .start_bitcoind()
1✔
3406
            .expect("Failed starting bitcoind");
1✔
3407

3408
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3409
        btc_controller.bootstrap_chain(150); //produces 50 spendable utxos
1✔
3410

3411
        let address = to_address_legacy(&miner_pubkey);
1✔
3412
        let utxo_set = btc_controller
1✔
3413
            .retrieve_utxo_set(&address, false, 0, &None, 0)
1✔
3414
            .expect("Failed to get utxos");
1✔
3415
        assert_eq!(btc_controller.get_block_hash(0), utxo_set.bhh);
1✔
3416
        assert_eq!(50, utxo_set.num_utxos());
1✔
3417
    }
1✔
3418

3419
    #[test]
3420
    #[ignore]
3421
    fn test_retrive_utxo_set_excluding_some_utxo() {
1✔
3422
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3423
            return;
×
3424
        }
1✔
3425

3426
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3427

3428
        let mut config = utils::create_miner_config();
1✔
3429
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3430

3431
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3432
        btcd_controller
1✔
3433
            .start_bitcoind()
1✔
3434
            .expect("Failed starting bitcoind");
1✔
3435

3436
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3437
        btc_controller.bootstrap_chain(150); //produces 50 spendable utxos
1✔
3438

3439
        let address = to_address_legacy(&miner_pubkey);
1✔
3440
        let mut all_utxos = btc_controller
1✔
3441
            .retrieve_utxo_set(&address, false, 0, &None, 0)
1✔
3442
            .expect("Failed to get utxos (50)");
1✔
3443

3444
        let filtered_utxos = btc_controller
1✔
3445
            .retrieve_utxo_set(&address, false, 0, &Some(all_utxos.clone()), 0)
1✔
3446
            .expect("Failed to get utxos");
1✔
3447
        assert_eq!(0, filtered_utxos.num_utxos(), "all utxos filtered out!");
1✔
3448

3449
        all_utxos.utxos.drain(0..10);
1✔
3450
        let filtered_utxos = btc_controller
1✔
3451
            .retrieve_utxo_set(&address, false, 0, &Some(all_utxos), 0)
1✔
3452
            .expect("Failed to get utxos");
1✔
3453
        assert_eq!(10, filtered_utxos.num_utxos(), "40 utxos filtered out!");
1✔
3454
    }
1✔
3455

3456
    #[test]
3457
    #[ignore]
3458
    fn test_list_unspent_with_max_utxos_config() {
1✔
3459
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3460
            return;
×
3461
        }
1✔
3462

3463
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3464

3465
        let mut config = utils::create_miner_config();
1✔
3466
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3467
        config.burnchain.max_unspent_utxos = Some(10);
1✔
3468

3469
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3470
        btcd_controller
1✔
3471
            .start_bitcoind()
1✔
3472
            .expect("Failed starting bitcoind");
1✔
3473

3474
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3475
        btc_controller.bootstrap_chain(150); //produces 50 spendable utxos
1✔
3476

3477
        let address = to_address_legacy(&miner_pubkey);
1✔
3478
        let utxos = btc_controller
1✔
3479
            .retrieve_utxo_set(&address, false, 1, &None, 0)
1✔
3480
            .expect("Failed to get utxos");
1✔
3481
        assert_eq!(10, utxos.num_utxos());
1✔
3482
    }
1✔
3483

3484
    #[test]
3485
    #[ignore]
3486
    fn test_get_all_utxos_with_confirmation() {
1✔
3487
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3488
            return;
×
3489
        }
1✔
3490

3491
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3492

3493
        let mut config = utils::create_miner_config();
1✔
3494
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3495

3496
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3497
        btcd_controller
1✔
3498
            .start_bitcoind()
1✔
3499
            .expect("bitcoind should be started!");
1✔
3500

3501
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3502

3503
        btc_controller.bootstrap_chain(100);
1✔
3504
        let utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
3505
        assert_eq!(0, utxos.len());
1✔
3506

3507
        btc_controller.build_next_block(1);
1✔
3508
        let utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
3509
        assert_eq!(1, utxos.len());
1✔
3510
        assert_eq!(101, utxos[0].confirmations);
1✔
3511
        assert_eq!(5_000_000_000, utxos[0].amount);
1✔
3512

3513
        btc_controller.build_next_block(1);
1✔
3514
        let mut utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
3515
        utxos.sort_by(|a, b| b.confirmations.cmp(&a.confirmations));
1✔
3516

3517
        assert_eq!(2, utxos.len());
1✔
3518
        assert_eq!(102, utxos[0].confirmations);
1✔
3519
        assert_eq!(5_000_000_000, utxos[0].amount);
1✔
3520
        assert_eq!(101, utxos[1].confirmations);
1✔
3521
        assert_eq!(5_000_000_000, utxos[1].amount);
1✔
3522
    }
1✔
3523

3524
    #[test]
3525
    #[ignore]
3526
    fn test_get_all_utxos_for_other_pubkey() {
1✔
3527
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3528
            return;
×
3529
        }
1✔
3530

3531
        let miner1_pubkey = utils::create_miner1_pubkey();
1✔
3532
        let miner2_pubkey = utils::create_miner2_pubkey();
1✔
3533

3534
        let mut config = utils::create_miner_config();
1✔
3535
        config.burnchain.local_mining_public_key = Some(miner1_pubkey.to_hex());
1✔
3536

3537
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3538
        btcd_controller
1✔
3539
            .start_bitcoind()
1✔
3540
            .expect("bitcoind should be started!");
1✔
3541

3542
        // Miner RPC routing must survive another miner's wallet being loaded.
3543
        let miner1_btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3544
        miner1_btc_controller.bootstrap_chain(1); // one utxo for miner_pubkey related address
1✔
3545

3546
        config.burnchain.local_mining_public_key = Some(miner2_pubkey.to_hex());
1✔
3547
        config.burnchain.wallet_name = Some("miner2_wallet".to_string());
1✔
3548
        let miner2_btc_controller = BitcoinRegtestController::new(config, None);
1✔
3549
        miner2_btc_controller.bootstrap_chain(102); // two utxo for other_pubkeys related address
1✔
3550

3551
        let utxos = miner1_btc_controller.get_all_utxos(&miner1_pubkey);
1✔
3552
        assert_eq!(1, utxos.len(), "miner1 see its own utxos");
1✔
3553

3554
        let utxos = miner1_btc_controller.get_all_utxos(&miner2_pubkey);
1✔
3555
        assert_eq!(2, utxos.len(), "miner1 see miner2 utxos");
1✔
3556

3557
        let utxos = miner2_btc_controller.get_all_utxos(&miner2_pubkey);
1✔
3558
        assert_eq!(2, utxos.len(), "miner2 see its own utxos");
1✔
3559

3560
        let utxos = miner2_btc_controller.get_all_utxos(&miner1_pubkey);
1✔
3561
        assert_eq!(1, utxos.len(), "miner2 see miner1 own utxos");
1✔
3562
    }
1✔
3563

3564
    #[test]
3565
    #[ignore]
3566
    fn test_get_utxos_ok_with_confirmation() {
1✔
3567
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3568
            return;
×
3569
        }
1✔
3570

3571
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3572

3573
        let mut config = utils::create_miner_config();
1✔
3574
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3575

3576
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3577
        btcd_controller
1✔
3578
            .start_bitcoind()
1✔
3579
            .expect("bitcoind should be started!");
1✔
3580

3581
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3582
        btc_controller.bootstrap_chain(101);
1✔
3583

3584
        let utxos_opt =
1✔
3585
            btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 1, None, 101);
1✔
3586
        let uxto_set = utxos_opt.expect("Shouldn't be None at height 101!");
1✔
3587

3588
        assert_eq!(btc_controller.get_block_hash(101), uxto_set.bhh);
1✔
3589
        assert_eq!(1, uxto_set.num_utxos());
1✔
3590
        assert_eq!(5_000_000_000, uxto_set.total_available());
1✔
3591
        let utxos = uxto_set.utxos;
1✔
3592
        assert_eq!(101, utxos[0].confirmations);
1✔
3593
        assert_eq!(5_000_000_000, utxos[0].amount);
1✔
3594

3595
        btc_controller.build_next_block(1);
1✔
3596

3597
        let utxos_opt =
1✔
3598
            btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 1, None, 102);
1✔
3599
        let uxto_set = utxos_opt.expect("Shouldn't be None at height 102!");
1✔
3600

3601
        assert_eq!(btc_controller.get_block_hash(102), uxto_set.bhh);
1✔
3602
        assert_eq!(2, uxto_set.num_utxos());
1✔
3603
        assert_eq!(10_000_000_000, uxto_set.total_available());
1✔
3604
        let mut utxos = uxto_set.utxos;
1✔
3605
        utxos.sort_by(|a, b| b.confirmations.cmp(&a.confirmations));
1✔
3606
        assert_eq!(102, utxos[0].confirmations);
1✔
3607
        assert_eq!(5_000_000_000, utxos[0].amount);
1✔
3608
        assert_eq!(101, utxos[1].confirmations);
1✔
3609
        assert_eq!(5_000_000_000, utxos[1].amount);
1✔
3610
    }
1✔
3611

3612
    #[test]
3613
    #[ignore]
3614
    fn test_get_utxos_none_due_to_filter_total_required() {
1✔
3615
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3616
            return;
×
3617
        }
1✔
3618

3619
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3620

3621
        let mut config = utils::create_miner_config();
1✔
3622
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3623

3624
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3625
        btcd_controller
1✔
3626
            .start_bitcoind()
1✔
3627
            .expect("bitcoind should be started!");
1✔
3628

3629
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3630
        btc_controller.bootstrap_chain(101); // one utxo exists
1✔
3631

3632
        let too_much_required = 10_000_000_000;
1✔
3633
        let utxos = btc_controller.get_utxos(
1✔
3634
            StacksEpochId::Epoch31,
1✔
3635
            &miner_pubkey,
1✔
3636
            too_much_required,
1✔
3637
            None,
1✔
3638
            0,
3639
        );
3640
        assert!(utxos.is_none(), "None because too much required");
1✔
3641
    }
1✔
3642

3643
    #[test]
3644
    #[ignore]
3645
    fn test_get_utxos_none_due_to_filter_pubkey() {
1✔
3646
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3647
            return;
×
3648
        }
1✔
3649

3650
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3651

3652
        let mut config = utils::create_miner_config();
1✔
3653
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3654

3655
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3656
        btcd_controller
1✔
3657
            .start_bitcoind()
1✔
3658
            .expect("bitcoind should be started!");
1✔
3659

3660
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3661
        btc_controller.bootstrap_chain(101); // one utxo exists
1✔
3662

3663
        let other_pubkey = utils::create_miner2_pubkey();
1✔
3664
        let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &other_pubkey, 1, None, 0);
1✔
3665
        assert!(
1✔
3666
            utxos.is_none(),
1✔
3667
            "None because utxos for other pubkey don't exist"
3668
        );
3669
    }
1✔
3670

3671
    #[test]
3672
    #[ignore]
3673
    fn test_get_utxos_none_due_to_filter_utxo_exclusion() {
1✔
3674
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3675
            return;
×
3676
        }
1✔
3677

3678
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3679

3680
        let mut config = utils::create_miner_config();
1✔
3681
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3682

3683
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3684
        btcd_controller
1✔
3685
            .start_bitcoind()
1✔
3686
            .expect("bitcoind should be started!");
1✔
3687

3688
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3689
        btc_controller.bootstrap_chain(101); // one utxo exists
1✔
3690

3691
        let existent_utxo = btc_controller
1✔
3692
            .get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 0, None, 0)
1✔
3693
            .expect("utxo set should exist");
1✔
3694
        let utxos = btc_controller.get_utxos(
1✔
3695
            StacksEpochId::Epoch31,
1✔
3696
            &miner_pubkey,
1✔
3697
            0,
3698
            Some(existent_utxo),
1✔
3699
            0,
3700
        );
3701
        assert!(
1✔
3702
            utxos.is_none(),
1✔
3703
            "None because filtering exclude existent utxo set"
3704
        );
3705
    }
1✔
3706

3707
    #[test]
3708
    #[ignore]
3709
    fn test_tx_confirmed_from_utxo_ok() {
1✔
3710
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3711
            return;
×
3712
        }
1✔
3713

3714
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3715

3716
        let mut config = utils::create_miner_config();
1✔
3717
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3718

3719
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3720
        btcd_controller
1✔
3721
            .start_bitcoind()
1✔
3722
            .expect("bitcoind should be started!");
1✔
3723

3724
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3725

3726
        btc_controller.bootstrap_chain(101);
1✔
3727
        let utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
3728
        assert_eq!(1, utxos.len(), "One UTXO should be confirmed!");
1✔
3729

3730
        let txid = Txid::from_bitcoin_tx_hash(&utxos[0].txid);
1✔
3731
        assert!(
1✔
3732
            btc_controller.is_transaction_confirmed(&txid),
1✔
3733
            "UTXO tx should be confirmed!"
3734
        );
3735
    }
1✔
3736

3737
    #[test]
3738
    #[ignore]
3739
    fn test_import_public_key_ok() {
1✔
3740
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3741
            return;
×
3742
        }
1✔
3743

3744
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3745

3746
        let config = utils::create_miner_config();
1✔
3747

3748
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3749
        btcd_controller
1✔
3750
            .start_bitcoind()
1✔
3751
            .expect("bitcoind should be started!");
1✔
3752

3753
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3754
        btc_controller
1✔
3755
            .ensure_test_wallet_loaded()
1✔
3756
            .expect("Test wallet should be created!");
1✔
3757

3758
        let result = btc_controller.import_public_key(&miner_pubkey);
1✔
3759
        assert!(
1✔
3760
            result.is_ok(),
1✔
3761
            "Should be ok, got err instead: {:?}",
3762
            result.unwrap_err()
×
3763
        );
3764
    }
1✔
3765

3766
    #[test]
3767
    #[ignore]
3768
    fn test_import_public_key_twice_ok() {
1✔
3769
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3770
            return;
×
3771
        }
1✔
3772

3773
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3774

3775
        let config = utils::create_miner_config();
1✔
3776

3777
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3778
        btcd_controller
1✔
3779
            .start_bitcoind()
1✔
3780
            .expect("bitcoind should be started!");
1✔
3781

3782
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3783
        btc_controller
1✔
3784
            .ensure_test_wallet_loaded()
1✔
3785
            .expect("Test wallet should be created!");
1✔
3786

3787
        btc_controller
1✔
3788
            .import_public_key(&miner_pubkey)
1✔
3789
            .expect("Import should be ok: first time!");
1✔
3790

3791
        //ok, but it is basically a no-op
3792
        let result = btc_controller.import_public_key(&miner_pubkey);
1✔
3793
        assert!(
1✔
3794
            result.is_ok(),
1✔
3795
            "Should be ok, got err instead: {:?}",
3796
            result.unwrap_err()
×
3797
        );
3798
    }
1✔
3799

3800
    #[test]
3801
    #[ignore]
3802
    fn test_import_public_key_segwit_ok() {
1✔
3803
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3804
            return;
×
3805
        }
1✔
3806

3807
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3808

3809
        let mut config = utils::create_miner_config();
1✔
3810
        config.miner.segwit = true;
1✔
3811

3812
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3813
        btcd_controller
1✔
3814
            .start_bitcoind()
1✔
3815
            .expect("bitcoind should be started!");
1✔
3816

3817
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3818
        btc_controller
1✔
3819
            .ensure_test_wallet_loaded()
1✔
3820
            .expect("Test wallet should be created!");
1✔
3821

3822
        let result = btc_controller.import_public_key(&miner_pubkey);
1✔
3823
        assert!(
1✔
3824
            result.is_ok(),
1✔
3825
            "Should be ok, got err instead: {:?}",
3826
            result.unwrap_err()
×
3827
        );
3828
    }
1✔
3829

3830
    /// Tests related to Leader Block Commit operation
3831
    mod leader_commit_op {
3832
        use super::*;
3833

3834
        #[test]
3835
        #[ignore]
3836
        fn test_build_leader_block_commit_tx_ok_with_new_commit_op() {
1✔
3837
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3838
                return;
×
3839
            }
1✔
3840

3841
            let keychain = utils::create_keychain();
1✔
3842
            let miner_pubkey = keychain.get_pub_key();
1✔
3843
            let mut op_signer = keychain.generate_op_signer();
1✔
3844

3845
            let mut config = utils::create_miner_config();
1✔
3846
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3847
            config.burnchain.pox_reward_length = Some(11);
1✔
3848

3849
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3850
            btcd_controller
1✔
3851
                .start_bitcoind()
1✔
3852
                .expect("bitcoind should be started!");
1✔
3853

3854
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3855
            btc_controller
1✔
3856
                .connect_dbs()
1✔
3857
                .expect("Dbs initialization required!");
1✔
3858
            btc_controller.bootstrap_chain(101); // now, one utxo exists
1✔
3859

3860
            let mut commit_op = utils::create_templated_commit_op();
1✔
3861
            commit_op.sunset_burn = 5_500;
1✔
3862
            commit_op.burn_fee = 110_000;
1✔
3863

3864
            let tx = btc_controller
1✔
3865
                .build_leader_block_commit_tx(
1✔
3866
                    StacksEpochId::Epoch31,
1✔
3867
                    commit_op.clone(),
1✔
3868
                    &mut op_signer,
1✔
3869
                )
3870
                .expect("Build leader block commit should work");
1✔
3871

3872
            assert!(op_signer.is_disposed());
1✔
3873

3874
            assert_eq!(1, tx.version);
1✔
3875
            assert_eq!(0, tx.lock_time);
1✔
3876
            assert_eq!(1, tx.input.len());
1✔
3877
            assert_eq!(4, tx.output.len());
1✔
3878

3879
            // utxos list contains the only existing utxo
3880
            let used_utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
3881
            let input_0 = utils::txin_at_index(&tx, &op_signer, &used_utxos, 0);
1✔
3882
            assert_eq!(input_0, tx.input[0]);
1✔
3883

3884
            let op_return = utils::txout_opreturn(&commit_op, &config.burnchain.magic_bytes, 5_500);
1✔
3885
            let op_commit_1 = utils::txout_opdup_commit_to(&commit_op.commit_outs[0], 55_000);
1✔
3886
            let op_commit_2 = utils::txout_opdup_commit_to(&commit_op.commit_outs[1], 55_000);
1✔
3887
            let op_change = utils::txout_opdup_change_legacy(&mut op_signer, 4_999_865_300);
1✔
3888
            assert_eq!(op_return, tx.output[0]);
1✔
3889
            assert_eq!(op_commit_1, tx.output[1]);
1✔
3890
            assert_eq!(op_commit_2, tx.output[2]);
1✔
3891
            assert_eq!(op_change, tx.output[3]);
1✔
3892
        }
1✔
3893

3894
        #[test]
3895
        #[ignore]
3896
        fn test_build_leader_block_commit_tx_fails_resub_same_commit_op_while_prev_not_confirmed() {
1✔
3897
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3898
                return;
×
3899
            }
1✔
3900

3901
            let keychain = utils::create_keychain();
1✔
3902
            let miner_pubkey = keychain.get_pub_key();
1✔
3903
            let mut op_signer = keychain.generate_op_signer();
1✔
3904

3905
            let mut config = utils::create_miner_config();
1✔
3906
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3907
            config.burnchain.pox_reward_length = Some(11);
1✔
3908

3909
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3910
            btcd_controller
1✔
3911
                .start_bitcoind()
1✔
3912
                .expect("bitcoind should be started!");
1✔
3913

3914
            let mut btc_controller = BitcoinRegtestController::new(config, None);
1✔
3915
            btc_controller
1✔
3916
                .connect_dbs()
1✔
3917
                .expect("Dbs initialization required!");
1✔
3918
            btc_controller.bootstrap_chain(101); // now, one utxo exists
1✔
3919

3920
            let commit_op = utils::create_templated_commit_op();
1✔
3921

3922
            let _first_tx_ok = btc_controller
1✔
3923
                .build_leader_block_commit_tx(
1✔
3924
                    StacksEpochId::Epoch31,
1✔
3925
                    commit_op.clone(),
1✔
3926
                    &mut op_signer,
1✔
3927
                )
3928
                .expect("At first, building leader block commit should work");
1✔
3929

3930
            // re-submitting same commit while previous it is not confirmed by the burnchain
3931
            let resubmit = btc_controller.build_leader_block_commit_tx(
1✔
3932
                StacksEpochId::Epoch31,
1✔
3933
                commit_op,
1✔
3934
                &mut op_signer,
1✔
3935
            );
3936

3937
            assert!(resubmit.is_err());
1✔
3938
            assert_eq!(
1✔
3939
                BurnchainControllerError::IdenticalOperation,
3940
                resubmit.unwrap_err()
1✔
3941
            );
3942
        }
1✔
3943

3944
        #[test]
3945
        #[ignore]
3946
        fn test_build_leader_block_commit_tx_fails_resub_same_commit_op_while_prev_is_confirmed() {
1✔
3947
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
3948
                return;
×
3949
            }
1✔
3950

3951
            let keychain = utils::create_keychain();
1✔
3952
            let miner_pubkey = keychain.get_pub_key();
1✔
3953
            let mut op_signer = keychain.generate_op_signer();
1✔
3954

3955
            let mut config = utils::create_miner_config();
1✔
3956
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3957
            config.burnchain.pox_reward_length = Some(11);
1✔
3958

3959
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3960
            btcd_controller
1✔
3961
                .start_bitcoind()
1✔
3962
                .expect("bitcoind should be started!");
1✔
3963

3964
            let mut btc_controller = BitcoinRegtestController::new(config, None);
1✔
3965
            btc_controller
1✔
3966
                .connect_dbs()
1✔
3967
                .expect("Dbs initialization required!");
1✔
3968
            btc_controller.bootstrap_chain(101); // now, one utxo exists
1✔
3969

3970
            let commit_op = utils::create_templated_commit_op();
1✔
3971

3972
            let first_tx_ok = btc_controller
1✔
3973
                .build_leader_block_commit_tx(
1✔
3974
                    StacksEpochId::Epoch31,
1✔
3975
                    commit_op.clone(),
1✔
3976
                    &mut op_signer,
1✔
3977
                )
3978
                .expect("At first, building leader block commit should work");
1✔
3979

3980
            utils::mine_tx(&btc_controller, &first_tx_ok); // Now tx is confirmed
1✔
3981

3982
            // re-submitting same commit while previous it is confirmed by the burnchain
3983
            let resubmit = btc_controller.build_leader_block_commit_tx(
1✔
3984
                StacksEpochId::Epoch31,
1✔
3985
                commit_op,
1✔
3986
                &mut op_signer,
1✔
3987
            );
3988

3989
            assert!(resubmit.is_err());
1✔
3990
            assert_eq!(
1✔
3991
                BurnchainControllerError::IdenticalOperation,
3992
                resubmit.unwrap_err()
1✔
3993
            );
3994
        }
1✔
3995

3996
        #[test]
3997
        #[ignore]
3998
        fn test_build_leader_block_commit_tx_ok_while_prev_is_confirmed() {
1✔
3999
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
4000
                return;
×
4001
            }
1✔
4002

4003
            let keychain = utils::create_keychain();
1✔
4004
            let miner_pubkey = keychain.get_pub_key();
1✔
4005
            let mut op_signer = keychain.generate_op_signer();
1✔
4006

4007
            let mut config = utils::create_miner_config();
1✔
4008
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4009
            config.burnchain.pox_reward_length = Some(11);
1✔
4010

4011
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4012
            btcd_controller
1✔
4013
                .start_bitcoind()
1✔
4014
                .expect("bitcoind should be started!");
1✔
4015

4016
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4017
            btc_controller
1✔
4018
                .connect_dbs()
1✔
4019
                .expect("Dbs initialization required!");
1✔
4020
            btc_controller.bootstrap_chain(101); // now, one utxo exists
1✔
4021

4022
            let mut commit_op = utils::create_templated_commit_op();
1✔
4023
            commit_op.sunset_burn = 5_500;
1✔
4024
            commit_op.burn_fee = 110_000;
1✔
4025

4026
            let first_tx_ok = btc_controller
1✔
4027
                .build_leader_block_commit_tx(
1✔
4028
                    StacksEpochId::Epoch31,
1✔
4029
                    commit_op.clone(),
1✔
4030
                    &mut op_signer,
1✔
4031
                )
4032
                .expect("At first, building leader block commit should work");
1✔
4033

4034
            let first_txid = first_tx_ok.txid();
1✔
4035

4036
            // Now tx is confirmed: prev utxo is updated and one more utxo is generated
4037
            utils::mine_tx(&btc_controller, &first_tx_ok);
1✔
4038

4039
            // re-gen signer othewise fails because it will be disposed during previous commit tx.
4040
            let mut signer = keychain.generate_op_signer();
1✔
4041
            // Modify the commit operation payload slightly, so it no longer matches the confirmed version.
4042
            commit_op.burn_fee += 10;
1✔
4043

4044
            let new_tx = btc_controller
1✔
4045
                .build_leader_block_commit_tx(
1✔
4046
                    StacksEpochId::Epoch31,
1✔
4047
                    commit_op.clone(),
1✔
4048
                    &mut signer,
1✔
4049
                )
4050
                .expect("Commit tx should be created!");
1✔
4051

4052
            assert!(op_signer.is_disposed());
1✔
4053

4054
            assert_eq!(1, new_tx.version);
1✔
4055
            assert_eq!(0, new_tx.lock_time);
1✔
4056
            assert_eq!(1, new_tx.input.len());
1✔
4057
            assert_eq!(4, new_tx.output.len());
1✔
4058

4059
            // utxos list contains the sole utxo used by prev commit operation
4060
            // because has enough amount to cover the new commit
4061
            let used_utxos: Vec<UTXO> = btc_controller
1✔
4062
                .get_all_utxos(&miner_pubkey)
1✔
4063
                .into_iter()
1✔
4064
                .filter(|utxo| utxo.txid == first_txid)
2✔
4065
                .collect();
1✔
4066

4067
            let input_0 = utils::txin_at_index(&new_tx, &op_signer, &used_utxos, 0);
1✔
4068
            assert_eq!(input_0, new_tx.input[0]);
1✔
4069

4070
            let op_return = utils::txout_opreturn(&commit_op, &config.burnchain.magic_bytes, 5_500);
1✔
4071
            let op_commit_1 = utils::txout_opdup_commit_to(&commit_op.commit_outs[0], 55_005);
1✔
4072
            let op_commit_2 = utils::txout_opdup_commit_to(&commit_op.commit_outs[1], 55_005);
1✔
4073
            let op_change = utils::txout_opdup_change_legacy(&mut signer, 4_999_730_590);
1✔
4074
            assert_eq!(op_return, new_tx.output[0]);
1✔
4075
            assert_eq!(op_commit_1, new_tx.output[1]);
1✔
4076
            assert_eq!(op_commit_2, new_tx.output[2]);
1✔
4077
            assert_eq!(op_change, new_tx.output[3]);
1✔
4078
        }
1✔
4079

4080
        #[test]
4081
        #[ignore]
4082
        fn test_build_leader_block_commit_tx_ok_rbf_while_prev_not_confirmed() {
1✔
4083
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
4084
                return;
×
4085
            }
1✔
4086

4087
            let keychain = utils::create_keychain();
1✔
4088
            let miner_pubkey = keychain.get_pub_key();
1✔
4089
            let mut op_signer = keychain.generate_op_signer();
1✔
4090

4091
            let mut config = utils::create_miner_config();
1✔
4092
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4093
            config.burnchain.pox_reward_length = Some(11);
1✔
4094

4095
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4096
            btcd_controller
1✔
4097
                .start_bitcoind()
1✔
4098
                .expect("bitcoind should be started!");
1✔
4099

4100
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4101
            btc_controller
1✔
4102
                .connect_dbs()
1✔
4103
                .expect("Dbs initialization required!");
1✔
4104
            btc_controller.bootstrap_chain(101); // Now, one utxo exists
1✔
4105

4106
            let mut commit_op = utils::create_templated_commit_op();
1✔
4107
            commit_op.sunset_burn = 5_500;
1✔
4108
            commit_op.burn_fee = 110_000;
1✔
4109

4110
            let _first_tx_ok = btc_controller
1✔
4111
                .build_leader_block_commit_tx(
1✔
4112
                    StacksEpochId::Epoch31,
1✔
4113
                    commit_op.clone(),
1✔
4114
                    &mut op_signer,
1✔
4115
                )
4116
                .expect("At first, building leader block commit should work");
1✔
4117

4118
            //re-gen signer othewise fails because it will be disposed during previous commit tx.
4119
            let mut signer = keychain.generate_op_signer();
1✔
4120
            //small change to the commit op payload
4121
            commit_op.burn_fee += 10;
1✔
4122

4123
            let rbf_tx = btc_controller
1✔
4124
                .build_leader_block_commit_tx(
1✔
4125
                    StacksEpochId::Epoch31,
1✔
4126
                    commit_op.clone(),
1✔
4127
                    &mut signer,
1✔
4128
                )
4129
                .expect("Commit tx should be rbf-ed");
1✔
4130

4131
            assert!(op_signer.is_disposed());
1✔
4132

4133
            assert_eq!(1, rbf_tx.version);
1✔
4134
            assert_eq!(0, rbf_tx.lock_time);
1✔
4135
            assert_eq!(1, rbf_tx.input.len());
1✔
4136
            assert_eq!(4, rbf_tx.output.len());
1✔
4137

4138
            // utxos list contains the only existing utxo
4139
            let used_utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
4140

4141
            let input_0 = utils::txin_at_index(&rbf_tx, &op_signer, &used_utxos, 0);
1✔
4142
            assert_eq!(input_0, rbf_tx.input[0]);
1✔
4143

4144
            let op_return = utils::txout_opreturn(&commit_op, &config.burnchain.magic_bytes, 5_500);
1✔
4145
            let op_commit_1 = utils::txout_opdup_commit_to(&commit_op.commit_outs[0], 55_005);
1✔
4146
            let op_commit_2 = utils::txout_opdup_commit_to(&commit_op.commit_outs[1], 55_005);
1✔
4147
            let op_change = utils::txout_opdup_change_legacy(&mut signer, 4_999_862_985);
1✔
4148
            assert_eq!(op_return, rbf_tx.output[0]);
1✔
4149
            assert_eq!(op_commit_1, rbf_tx.output[1]);
1✔
4150
            assert_eq!(op_commit_2, rbf_tx.output[2]);
1✔
4151
            assert_eq!(op_change, rbf_tx.output[3]);
1✔
4152
        }
1✔
4153

4154
        #[test]
4155
        #[ignore]
4156
        fn test_make_operation_leader_block_commit_tx_ok() {
1✔
4157
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
4158
                return;
×
4159
            }
1✔
4160

4161
            let keychain = utils::create_keychain();
1✔
4162
            let miner_pubkey = keychain.get_pub_key();
1✔
4163
            let mut op_signer = keychain.generate_op_signer();
1✔
4164

4165
            let mut config = utils::create_miner_config();
1✔
4166
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4167
            config.burnchain.pox_reward_length = Some(11);
1✔
4168

4169
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4170
            btcd_controller
1✔
4171
                .start_bitcoind()
1✔
4172
                .expect("bitcoind should be started!");
1✔
4173

4174
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4175
            btc_controller
1✔
4176
                .connect_dbs()
1✔
4177
                .expect("Dbs initialization required!");
1✔
4178
            btc_controller.bootstrap_chain(101); // now, one utxo exists
1✔
4179

4180
            let mut commit_op = utils::create_templated_commit_op();
1✔
4181
            commit_op.sunset_burn = 5_500;
1✔
4182
            commit_op.burn_fee = 110_000;
1✔
4183

4184
            let tx = btc_controller
1✔
4185
                .make_operation_tx(
1✔
4186
                    StacksEpochId::Epoch31,
1✔
4187
                    BlockstackOperationType::LeaderBlockCommit(commit_op),
1✔
4188
                    &mut op_signer,
1✔
4189
                )
4190
                .expect("Make op should work");
1✔
4191

4192
            assert!(op_signer.is_disposed());
1✔
4193

4194
            assert_eq!(
1✔
4195
                "1a74106bd760117892fbd90fca11646b4de46f99fd2b065c9e0706cfdcea0336",
4196
                tx.txid().to_string()
1✔
4197
            );
4198
        }
1✔
4199

4200
        #[test]
4201
        #[ignore]
4202
        fn test_submit_leader_block_commit_tx_ok() {
1✔
4203
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
4204
                return;
×
4205
            }
1✔
4206

4207
            let keychain = utils::create_keychain();
1✔
4208
            let miner_pubkey = keychain.get_pub_key();
1✔
4209
            let mut op_signer = keychain.generate_op_signer();
1✔
4210

4211
            let mut config = utils::create_miner_config();
1✔
4212
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4213
            config.burnchain.pox_reward_length = Some(11);
1✔
4214

4215
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4216
            btcd_controller
1✔
4217
                .start_bitcoind()
1✔
4218
                .expect("bitcoind should be started!");
1✔
4219

4220
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4221
            btc_controller
1✔
4222
                .connect_dbs()
1✔
4223
                .expect("Dbs initialization required!");
1✔
4224
            btc_controller.bootstrap_chain(101); // now, one utxo exists
1✔
4225

4226
            let mut commit_op = utils::create_templated_commit_op();
1✔
4227
            commit_op.sunset_burn = 5_500;
1✔
4228
            commit_op.burn_fee = 110_000;
1✔
4229

4230
            let tx_id = btc_controller
1✔
4231
                .submit_operation(
1✔
4232
                    StacksEpochId::Epoch31,
1✔
4233
                    BlockstackOperationType::LeaderBlockCommit(commit_op),
1✔
4234
                    &mut op_signer,
1✔
4235
                )
4236
                .expect("Submit op should work");
1✔
4237

4238
            assert!(op_signer.is_disposed());
1✔
4239

4240
            assert_eq!(
1✔
4241
                "1a74106bd760117892fbd90fca11646b4de46f99fd2b065c9e0706cfdcea0336",
4242
                tx_id.to_hex()
1✔
4243
            );
4244
        }
1✔
4245

4246
        #[test]
4247
        #[ignore]
4248
        fn test_submit_operation_block_commit_clears_ongoing_on_send_failure() {
1✔
4249
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
4250
                return;
×
4251
            }
1✔
4252

4253
            let keychain = utils::create_keychain();
1✔
4254
            let miner_pubkey = keychain.get_pub_key();
1✔
4255
            let mut op_signer = keychain.generate_op_signer();
1✔
4256

4257
            let mut config = utils::create_miner_config();
1✔
4258
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4259
            config.burnchain.pox_reward_length = Some(11);
1✔
4260

4261
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4262
            btcd_controller
1✔
4263
                .start_bitcoind()
1✔
4264
                .expect("bitcoind should be started!");
1✔
4265

4266
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4267
            btc_controller
1✔
4268
                .connect_dbs()
1✔
4269
                .expect("Dbs initialization required!");
1✔
4270
            btc_controller.bootstrap_chain(101);
1✔
4271

4272
            let mut commit_op = utils::create_templated_commit_op();
1✔
4273
            commit_op.sunset_burn = 5_500;
1✔
4274
            commit_op.burn_fee = 110_000;
1✔
4275

4276
            // First submit succeeds and sets ongoing_block_commit
4277
            btc_controller
1✔
4278
                .submit_operation(
1✔
4279
                    StacksEpochId::Epoch31,
1✔
4280
                    BlockstackOperationType::LeaderBlockCommit(commit_op.clone()),
1✔
4281
                    &mut op_signer,
1✔
4282
                )
4283
                .expect("First submit should succeed");
1✔
4284

4285
            assert!(
1✔
4286
                btc_controller.get_ongoing_commit().is_some(),
1✔
4287
                "ongoing_block_commit should be set after successful submit"
4288
            );
4289

4290
            // Corrupt the cached UTXOs so the RBF transaction references
4291
            // non-existent inputs, causing send_transaction to be rejected
4292
            // by bitcoind immediately.
4293
            let mut ongoing = btc_controller.get_ongoing_commit().unwrap();
1✔
4294
            for utxo in ongoing.utxos.utxos.iter_mut() {
1✔
4295
                utxo.txid = Sha256dHash::default();
1✔
4296
            }
1✔
4297
            btc_controller.set_ongoing_commit(Some(ongoing));
1✔
4298

4299
            // Second submit with different payload triggers the RBF path.
4300
            // make_operation_tx builds the tx using the corrupted UTXOs,
4301
            // then send_transaction fails because the inputs don't exist.
4302
            let mut op_signer = keychain.generate_op_signer();
1✔
4303
            commit_op.burn_fee += 10;
1✔
4304

4305
            let err = btc_controller
1✔
4306
                .submit_operation(
1✔
4307
                    StacksEpochId::Epoch31,
1✔
4308
                    BlockstackOperationType::LeaderBlockCommit(commit_op),
1✔
4309
                    &mut op_signer,
1✔
4310
                )
4311
                .unwrap_err();
1✔
4312

4313
            assert!(
1✔
4314
                matches!(
×
4315
                    err,
1✔
4316
                    BurnchainControllerError::TransactionSubmissionFailed(_)
4317
                ),
4318
                "Error should be TransactionSubmissionFailed, but was {err}"
4319
            );
4320
            assert!(
1✔
4321
                btc_controller.get_ongoing_commit().is_none(),
1✔
4322
                "ongoing_block_commit should be cleared after send failure"
4323
            );
4324
        }
1✔
4325
    }
4326

4327
    /// Tests related to Leader Key Register operation
4328
    mod leader_key_op {
4329
        use super::*;
4330

4331
        #[test]
4332
        #[ignore]
4333
        fn test_build_leader_key_tx_ok() {
1✔
4334
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
4335
                return;
×
4336
            }
1✔
4337

4338
            let keychain = utils::create_keychain();
1✔
4339
            let miner_pubkey = keychain.get_pub_key();
1✔
4340
            let mut op_signer = keychain.generate_op_signer();
1✔
4341

4342
            let mut config = utils::create_miner_config();
1✔
4343
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4344

4345
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4346
            btcd_controller
1✔
4347
                .start_bitcoind()
1✔
4348
                .expect("bitcoind should be started!");
1✔
4349

4350
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4351
            btc_controller.bootstrap_chain(101); // now, one utxo exists
1✔
4352

4353
            let leader_key_op = utils::create_templated_leader_key_op();
1✔
4354

4355
            let tx = btc_controller
1✔
4356
                .build_leader_key_register_tx(
1✔
4357
                    StacksEpochId::Epoch31,
1✔
4358
                    leader_key_op.clone(),
1✔
4359
                    &mut op_signer,
1✔
4360
                )
4361
                .expect("Build leader key should work");
1✔
4362

4363
            assert!(op_signer.is_disposed());
1✔
4364

4365
            assert_eq!(1, tx.version);
1✔
4366
            assert_eq!(0, tx.lock_time);
1✔
4367
            assert_eq!(1, tx.input.len());
1✔
4368
            assert_eq!(2, tx.output.len());
1✔
4369

4370
            // utxos list contains the only existing utxo
4371
            let used_utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
4372
            let input_0 = utils::txin_at_index(&tx, &op_signer, &used_utxos, 0);
1✔
4373
            assert_eq!(input_0, tx.input[0]);
1✔
4374

4375
            let op_return = utils::txout_opreturn(&leader_key_op, &config.burnchain.magic_bytes, 0);
1✔
4376
            let op_change = utils::txout_opdup_change_legacy(&mut op_signer, 4_999_980_000);
1✔
4377
            assert_eq!(op_return, tx.output[0]);
1✔
4378
            assert_eq!(op_change, tx.output[1]);
1✔
4379
        }
1✔
4380

4381
        #[test]
4382
        #[ignore]
4383
        fn test_build_leader_key_tx_fails_due_to_no_utxos() {
1✔
4384
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
4385
                return;
×
4386
            }
1✔
4387

4388
            let keychain = utils::create_keychain();
1✔
4389
            let miner_pubkey = keychain.get_pub_key();
1✔
4390
            let mut op_signer = keychain.generate_op_signer();
1✔
4391

4392
            let mut config = utils::create_miner_config();
1✔
4393
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4394

4395
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4396
            btcd_controller
1✔
4397
                .start_bitcoind()
1✔
4398
                .expect("bitcoind should be started!");
1✔
4399

4400
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4401
            btc_controller.bootstrap_chain(100); // no utxos exist
1✔
4402

4403
            let leader_key_op = utils::create_templated_leader_key_op();
1✔
4404

4405
            let error = btc_controller
1✔
4406
                .build_leader_key_register_tx(
1✔
4407
                    StacksEpochId::Epoch31,
1✔
4408
                    leader_key_op.clone(),
1✔
4409
                    &mut op_signer,
1✔
4410
                )
4411
                .expect_err("Leader key build should fail!");
1✔
4412

4413
            assert!(!op_signer.is_disposed());
1✔
4414
            assert_eq!(BurnchainControllerError::NoUTXOs, error);
1✔
4415
        }
1✔
4416

4417
        #[test]
4418
        #[ignore]
4419
        fn test_make_operation_leader_key_register_tx_ok() {
1✔
4420
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
4421
                return;
×
4422
            }
1✔
4423

4424
            let keychain = utils::create_keychain();
1✔
4425
            let miner_pubkey = keychain.get_pub_key();
1✔
4426
            let mut op_signer = keychain.generate_op_signer();
1✔
4427

4428
            let mut config = utils::create_miner_config();
1✔
4429
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4430
            config.burnchain.pox_reward_length = Some(11);
1✔
4431

4432
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4433
            btcd_controller
1✔
4434
                .start_bitcoind()
1✔
4435
                .expect("bitcoind should be started!");
1✔
4436

4437
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4438
            btc_controller.bootstrap_chain(101); // now, one utxo exists
1✔
4439

4440
            let leader_key_op = utils::create_templated_leader_key_op();
1✔
4441

4442
            let tx = btc_controller
1✔
4443
                .make_operation_tx(
1✔
4444
                    StacksEpochId::Epoch31,
1✔
4445
                    BlockstackOperationType::LeaderKeyRegister(leader_key_op),
1✔
4446
                    &mut op_signer,
1✔
4447
                )
4448
                .expect("Make op should work");
1✔
4449

4450
            assert!(op_signer.is_disposed());
1✔
4451

4452
            assert_eq!(
1✔
4453
                "4ecd7ba71bebd1aaed49dd63747ee424473f1c571bb9a576361607a669191024",
4454
                tx.txid().to_string()
1✔
4455
            );
4456
        }
1✔
4457

4458
        #[test]
4459
        #[ignore]
4460
        fn test_submit_operation_leader_key_register_tx_ok() {
1✔
4461
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
4462
                return;
×
4463
            }
1✔
4464

4465
            let keychain = utils::create_keychain();
1✔
4466
            let miner_pubkey = keychain.get_pub_key();
1✔
4467
            let mut op_signer = keychain.generate_op_signer();
1✔
4468

4469
            let mut config = utils::create_miner_config();
1✔
4470
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4471

4472
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4473
            btcd_controller
1✔
4474
                .start_bitcoind()
1✔
4475
                .expect("bitcoind should be started!");
1✔
4476

4477
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4478
            btc_controller.bootstrap_chain(101); // now, one utxo exists
1✔
4479

4480
            let leader_key_op = utils::create_templated_leader_key_op();
1✔
4481

4482
            let tx_id = btc_controller
1✔
4483
                .submit_operation(
1✔
4484
                    StacksEpochId::Epoch31,
1✔
4485
                    BlockstackOperationType::LeaderKeyRegister(leader_key_op),
1✔
4486
                    &mut op_signer,
1✔
4487
                )
4488
                .expect("Submit op should work");
1✔
4489

4490
            assert!(op_signer.is_disposed());
1✔
4491

4492
            assert_eq!(
1✔
4493
                "4ecd7ba71bebd1aaed49dd63747ee424473f1c571bb9a576361607a669191024",
4494
                tx_id.to_hex()
1✔
4495
            );
4496
        }
1✔
4497
    }
4498

4499
    /// Tests related to Pre Stacks operation
4500
    mod pre_stx_op {
4501
        use super::*;
4502

4503
        #[test]
4504
        #[ignore]
4505
        fn test_build_pre_stx_tx_ok() {
1✔
4506
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
4507
                return;
×
4508
            }
1✔
4509

4510
            let keychain = utils::create_keychain();
1✔
4511
            let miner_pubkey = keychain.get_pub_key();
1✔
4512
            let mut op_signer = keychain.generate_op_signer();
1✔
4513

4514
            let mut config = utils::create_miner_config();
1✔
4515
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4516

4517
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4518
            btcd_controller
1✔
4519
                .start_bitcoind()
1✔
4520
                .expect("bitcoind should be started!");
1✔
4521

4522
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4523
            btc_controller.bootstrap_chain(101); // now, one utxo exists
1✔
4524

4525
            let mut pre_stx_op = utils::create_templated_pre_stx_op();
1✔
4526
            pre_stx_op.output = keychain.get_address(false);
1✔
4527

4528
            let tx = btc_controller
1✔
4529
                .build_pre_stacks_tx(StacksEpochId::Epoch31, pre_stx_op.clone(), &mut op_signer)
1✔
4530
                .expect("Build leader key should work");
1✔
4531

4532
            assert!(op_signer.is_disposed());
1✔
4533

4534
            assert_eq!(1, tx.version);
1✔
4535
            assert_eq!(0, tx.lock_time);
1✔
4536
            assert_eq!(1, tx.input.len());
1✔
4537
            assert_eq!(3, tx.output.len());
1✔
4538

4539
            // utxos list contains the only existing utxo
4540
            let used_utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
4541
            let input_0 = utils::txin_at_index(&tx, &op_signer, &used_utxos, 0);
1✔
4542
            assert_eq!(input_0, tx.input[0]);
1✔
4543

4544
            let op_return = utils::txout_opreturn(&pre_stx_op, &config.burnchain.magic_bytes, 0);
1✔
4545
            let op_change = utils::txout_opdup_change_legacy(&mut op_signer, 24_500);
1✔
4546
            assert_eq!(op_return, tx.output[0]);
1✔
4547
            assert_eq!(op_change, tx.output[1]);
1✔
4548
        }
1✔
4549

4550
        #[test]
4551
        #[ignore]
4552
        fn test_build_pre_stx_tx_fails_due_to_no_utxos() {
1✔
4553
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
4554
                return;
×
4555
            }
1✔
4556

4557
            let keychain = utils::create_keychain();
1✔
4558
            let miner_pubkey = keychain.get_pub_key();
1✔
4559
            let mut op_signer = keychain.generate_op_signer();
1✔
4560

4561
            let mut config = utils::create_miner_config();
1✔
4562
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4563

4564
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4565
            btcd_controller
1✔
4566
                .start_bitcoind()
1✔
4567
                .expect("bitcoind should be started!");
1✔
4568

4569
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4570
            btc_controller.bootstrap_chain(100); // no utxo exists
1✔
4571

4572
            let mut pre_stx_op = utils::create_templated_pre_stx_op();
1✔
4573
            pre_stx_op.output = keychain.get_address(false);
1✔
4574

4575
            let error = btc_controller
1✔
4576
                .build_pre_stacks_tx(StacksEpochId::Epoch31, pre_stx_op.clone(), &mut op_signer)
1✔
4577
                .expect_err("Leader key build should fail!");
1✔
4578

4579
            assert!(!op_signer.is_disposed());
1✔
4580
            assert_eq!(BurnchainControllerError::NoUTXOs, error);
1✔
4581
        }
1✔
4582

4583
        #[test]
4584
        #[ignore]
4585
        fn test_make_operation_pre_stx_tx_ok() {
1✔
4586
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
4587
                return;
×
4588
            }
1✔
4589

4590
            let keychain = utils::create_keychain();
1✔
4591
            let miner_pubkey = keychain.get_pub_key();
1✔
4592
            let mut op_signer = keychain.generate_op_signer();
1✔
4593

4594
            let mut config = utils::create_miner_config();
1✔
4595
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4596

4597
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4598
            btcd_controller
1✔
4599
                .start_bitcoind()
1✔
4600
                .expect("bitcoind should be started!");
1✔
4601

4602
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4603
            btc_controller.bootstrap_chain(101); // now, one utxo exists
1✔
4604

4605
            let mut pre_stx_op = utils::create_templated_pre_stx_op();
1✔
4606
            pre_stx_op.output = keychain.get_address(false);
1✔
4607

4608
            let tx = btc_controller
1✔
4609
                .make_operation_tx(
1✔
4610
                    StacksEpochId::Epoch31,
1✔
4611
                    BlockstackOperationType::PreStx(pre_stx_op),
1✔
4612
                    &mut op_signer,
1✔
4613
                )
4614
                .expect("Make op should work");
1✔
4615

4616
            assert!(op_signer.is_disposed());
1✔
4617

4618
            assert_eq!(
1✔
4619
                "2d061c42c6f13a62fd9d80dc9fdcd19bdb4f9e4a07f786e42530c64c52ed9d1d",
4620
                tx.txid().to_string()
1✔
4621
            );
4622
        }
1✔
4623

4624
        #[test]
4625
        #[ignore]
4626
        fn test_submit_operation_pre_stx_tx_ok() {
1✔
4627
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
4628
                return;
×
4629
            }
1✔
4630

4631
            let keychain = utils::create_keychain();
1✔
4632
            let miner_pubkey = keychain.get_pub_key();
1✔
4633
            let mut op_signer = keychain.generate_op_signer();
1✔
4634

4635
            let mut config = utils::create_miner_config();
1✔
4636
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4637

4638
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4639
            btcd_controller
1✔
4640
                .start_bitcoind()
1✔
4641
                .expect("bitcoind should be started!");
1✔
4642

4643
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4644
            btc_controller.bootstrap_chain(101); // now, one utxo exists
1✔
4645

4646
            let mut pre_stx_op = utils::create_templated_pre_stx_op();
1✔
4647
            pre_stx_op.output = keychain.get_address(false);
1✔
4648

4649
            let tx_id = btc_controller
1✔
4650
                .submit_operation(
1✔
4651
                    StacksEpochId::Epoch31,
1✔
4652
                    BlockstackOperationType::PreStx(pre_stx_op),
1✔
4653
                    &mut op_signer,
1✔
4654
                )
4655
                .expect("submit op should work");
1✔
4656

4657
            assert!(op_signer.is_disposed());
1✔
4658

4659
            assert_eq!(
1✔
4660
                "2d061c42c6f13a62fd9d80dc9fdcd19bdb4f9e4a07f786e42530c64c52ed9d1d",
4661
                tx_id.to_hex()
1✔
4662
            );
4663
        }
1✔
4664
    }
4665
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc