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

stacks-network / stacks-core / 30097370812-1

24 Jul 2026 01:33PM UTC coverage: 86.528% (+0.8%) from 85.712%
30097370812-1

Pull #7433

github

68bb89
web-flow
Merge a0d69b3d4 into 6dfedec46
Pull Request #7433: fix: bitcoind wallet setup for version >= 31 (miner wallet resolution & startup hardening)

206 of 225 new or added lines in 6 files covered. (91.56%)

12142 existing lines in 169 files now uncovered.

200408 of 231610 relevant lines covered (86.53%)

18939416.41 hits per line

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

92.32
/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

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

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

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

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

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

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

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

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

169
pub fn get_satoshis_per_byte(config: &Config) -> u64 {
24,916✔
170
    config.get_burnchain_config().satoshis_per_byte
24,916✔
171
}
24,916✔
172

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

177
pub fn get_max_rbf(config: &Config) -> u64 {
13,704✔
178
    config.get_burnchain_config().max_rbf
13,704✔
179
}
13,704✔
180

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

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

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

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

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

228
    pub fn rbf_fee(&self) -> u64 {
10,578✔
229
        if self.is_rbf_enabled {
10,578✔
230
            self.spent_in_attempts + self.default_tx_size
572✔
231
        } else {
232
            0
10,006✔
233
        }
234
    }
10,578✔
235

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

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

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

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

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

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

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

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

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

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

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

324
impl BitcoinRegtestController {
325
    pub fn new(config: Config, coordinator_channel: Option<CoordinatorChannels>) -> Self {
288✔
326
        BitcoinRegtestController::with_burnchain(config, coordinator_channel, None, None)
288✔
327
    }
288✔
328

329
    // TODO: add tests from mutation testing results #4864
330
    #[cfg_attr(test, mutants::skip)]
331
    pub fn with_burnchain(
861✔
332
        config: Config,
861✔
333
        coordinator_channel: Option<CoordinatorChannels>,
861✔
334
        burnchain: Option<Burnchain>,
861✔
335
        should_keep_running: Option<Arc<AtomicBool>>,
861✔
336
    ) -> Self {
861✔
337
        std::fs::create_dir_all(config.get_burnchain_path_str()).expect("Unable to create workdir");
861✔
338
        let (_, network_id) = config.burnchain.get_bitcoin_network();
861✔
339

340
        let res = SpvClient::new(
861✔
341
            &config.get_spv_headers_file_path(),
861✔
342
            0,
343
            None,
861✔
344
            network_id,
861✔
345
            true,
346
            false,
347
        );
348
        if let Err(err) = res {
861✔
349
            error!("Unable to init block headers: {err}");
×
350
            panic!()
×
351
        }
861✔
352

353
        let burnchain_params = burnchain_params_from_config(&config.burnchain);
861✔
354

355
        if network_id == BitcoinNetworkType::Mainnet && config.burnchain.epochs.is_some() {
861✔
356
            panic!("It is an error to set custom epochs while running on Mainnet: network_id {network_id:?} config.burnchain {:#?}",
×
357
                   &config.burnchain);
×
358
        }
861✔
359

360
        let indexer_config = {
861✔
361
            let burnchain_config = config.burnchain.clone();
861✔
362
            BitcoinIndexerConfig {
861✔
363
                peer_host: burnchain_config.peer_host,
861✔
364
                peer_port: burnchain_config.peer_port,
861✔
365
                rpc_port: burnchain_config.rpc_port,
861✔
366
                rpc_ssl: burnchain_config.rpc_ssl,
861✔
367
                username: burnchain_config.username,
861✔
368
                password: burnchain_config.password,
861✔
369
                timeout: burnchain_config.timeout,
861✔
370
                socket_timeout: burnchain_config.socket_timeout,
861✔
371
                spv_headers_path: config.get_spv_headers_file_path(),
861✔
372
                first_block: burnchain_params.first_block_height,
861✔
373
                magic_bytes: burnchain_config.magic_bytes,
861✔
374
                epochs: burnchain_config.epochs,
861✔
375
            }
861✔
376
        };
377

378
        let (_, network_type) = config.burnchain.get_bitcoin_network();
861✔
379
        let indexer_runtime = BitcoinIndexerRuntime::new(network_type, config.burnchain.timeout);
861✔
380
        let burnchain_indexer = BitcoinIndexer {
861✔
381
            config: indexer_config,
861✔
382
            runtime: indexer_runtime,
861✔
383
            should_keep_running: should_keep_running.clone(),
861✔
384
        };
861✔
385

386
        let rpc_client = Self::create_rpc_client_unchecked(&config);
861✔
387

388
        Self {
861✔
389
            use_coordinator: coordinator_channel,
861✔
390
            config,
861✔
391
            indexer: burnchain_indexer,
861✔
392
            db: None,
861✔
393
            burnchain_db: None,
861✔
394
            chain_tip: None,
861✔
395
            burnchain_config: burnchain,
861✔
396
            ongoing_block_commit: None,
861✔
397
            should_keep_running,
861✔
398
            rpc_client,
861✔
399
        }
861✔
400
    }
861✔
401

402
    // TODO: add tests from mutation testing results #4864
403
    #[cfg_attr(test, mutants::skip)]
404
    /// create a dummy bitcoin regtest controller.
405
    ///   used just for submitting bitcoin ops.
406
    pub fn new_dummy(config: Config) -> Self {
243,234✔
407
        let burnchain_params = burnchain_params_from_config(&config.burnchain);
243,234✔
408

409
        let indexer_config = {
243,234✔
410
            let burnchain_config = config.burnchain.clone();
243,234✔
411
            BitcoinIndexerConfig {
243,234✔
412
                peer_host: burnchain_config.peer_host,
243,234✔
413
                peer_port: burnchain_config.peer_port,
243,234✔
414
                rpc_port: burnchain_config.rpc_port,
243,234✔
415
                rpc_ssl: burnchain_config.rpc_ssl,
243,234✔
416
                username: burnchain_config.username,
243,234✔
417
                password: burnchain_config.password,
243,234✔
418
                timeout: burnchain_config.timeout,
243,234✔
419
                socket_timeout: burnchain_config.socket_timeout,
243,234✔
420
                spv_headers_path: config.get_spv_headers_file_path(),
243,234✔
421
                first_block: burnchain_params.first_block_height,
243,234✔
422
                magic_bytes: burnchain_config.magic_bytes,
243,234✔
423
                epochs: burnchain_config.epochs,
243,234✔
424
            }
243,234✔
425
        };
426

427
        let (_, network_type) = config.burnchain.get_bitcoin_network();
243,234✔
428
        let indexer_runtime = BitcoinIndexerRuntime::new(network_type, config.burnchain.timeout);
243,234✔
429
        let burnchain_indexer = BitcoinIndexer {
243,234✔
430
            config: indexer_config,
243,234✔
431
            runtime: indexer_runtime,
243,234✔
432
            should_keep_running: None,
243,234✔
433
        };
243,234✔
434

435
        let rpc_client = Self::create_rpc_client_unchecked(&config);
243,234✔
436

437
        Self {
243,234✔
438
            use_coordinator: None,
243,234✔
439
            config,
243,234✔
440
            indexer: burnchain_indexer,
243,234✔
441
            db: None,
243,234✔
442
            burnchain_db: None,
243,234✔
443
            chain_tip: None,
243,234✔
444
            burnchain_config: None,
243,234✔
445
            ongoing_block_commit: None,
243,234✔
446
            should_keep_running: None,
243,234✔
447
            rpc_client,
243,234✔
448
        }
243,234✔
449
    }
243,234✔
450

451
    /// Creates a dummy bitcoin regtest controller, with the given ongoing block-commits
452
    pub fn new_ongoing_dummy(config: Config, ongoing: Option<OngoingBlockCommit>) -> Self {
242,673✔
453
        let mut ret = Self::new_dummy(config);
242,673✔
454
        ret.ongoing_block_commit = ongoing;
242,673✔
455
        ret
242,673✔
456
    }
242,673✔
457

458
    /// Get an owned copy of the ongoing block commit state
459
    pub fn get_ongoing_commit(&self) -> Option<OngoingBlockCommit> {
249,928✔
460
        self.ongoing_block_commit.clone()
249,928✔
461
    }
249,928✔
462

463
    /// Set the ongoing block commit state
464
    pub fn set_ongoing_commit(&mut self, ongoing: Option<OngoingBlockCommit>) {
7,248✔
465
        self.ongoing_block_commit = ongoing;
7,248✔
466
    }
7,248✔
467

468
    /// Get the default Burnchain instance from our config
469
    fn default_burnchain(&self) -> Burnchain {
1,028,845✔
470
        match &self.burnchain_config {
1,028,845✔
471
            Some(burnchain) => burnchain.clone(),
×
472
            None => self.config.get_burnchain(),
1,028,845✔
473
        }
474
    }
1,028,845✔
475

476
    /// Get the PoX constants in use
477
    pub fn get_pox_constants(&self) -> PoxConstants {
×
478
        let burnchain = self.get_burnchain();
×
479
        burnchain.pox_constants
×
480
    }
×
481

482
    /// Get the Burnchain in use
483
    pub fn get_burnchain(&self) -> Burnchain {
1,063,671✔
484
        match self.burnchain_config {
1,063,671✔
485
            Some(ref burnchain) => burnchain.clone(),
34,826✔
486
            None => self.default_burnchain(),
1,028,845✔
487
        }
488
    }
1,063,671✔
489

490
    /// Attempt to create a new [`BitcoinRpcClient`] from the given [`Config`].
491
    ///
492
    /// If the provided config indicates that the node is a **miner**,
493
    /// tries to instantiate it or **panics** otherwise.
494
    /// If the node is **not** a miner, returns None (e.g. follower node).
495
    fn create_rpc_client_unchecked(config: &Config) -> Option<BitcoinRpcClient> {
244,095✔
496
        config.node.miner.then(|| {
244,095✔
497
            BitcoinRpcClient::from_stx_config(&config)
244,074✔
498
                .expect("unable to instantiate the RPC client for miner node!")
244,074✔
499
        })
244,074✔
500
    }
244,095✔
501

502
    /// Attempt to get a reference to the underlying [`BitcoinRpcClient`].
503
    ///
504
    /// This function will panic if the RPC client has not been configured
505
    /// (i.e. [`Self::create_rpc_client_unchecked`] returned `None` during initialization),
506
    /// but an attempt is made to use it anyway.
507
    ///
508
    /// In practice, this means the node is expected to act as a miner,
509
    /// yet no [`BitcoinRpcClient`] was created or properly configured.
510
    fn get_rpc_client(&self) -> &BitcoinRpcClient {
65,146✔
511
        self.rpc_client
65,146✔
512
            .as_ref()
65,146✔
513
            .expect("BUG: BitcoinRpcClient is required, but it has not been configured properly!")
65,146✔
514
    }
65,146✔
515

516
    /// Helium (devnet) blocks receiver.  Returns the new burnchain tip.
517
    fn receive_blocks_helium(&mut self) -> BurnchainTip {
×
518
        let mut burnchain = self.get_burnchain();
×
519
        let (block_snapshot, state_transition) = loop {
×
520
            match burnchain.sync_with_indexer_deprecated(&mut self.indexer) {
×
521
                Ok(x) => {
×
522
                    break x;
×
523
                }
524
                Err(e) => {
×
525
                    // keep trying
526
                    error!("Unable to sync with burnchain: {e}");
×
527
                    match e {
×
528
                        burnchain_error::TrySyncAgain => {
529
                            // try again immediately
530
                            continue;
×
531
                        }
532
                        burnchain_error::BurnchainPeerBroken => {
533
                            // remote burnchain peer broke, and produced a shorter blockchain fork.
534
                            // just keep trying
535
                            sleep_ms(5000);
×
536
                            continue;
×
537
                        }
538
                        _ => {
539
                            // delay and try again
540
                            sleep_ms(5000);
×
541
                            continue;
×
542
                        }
543
                    }
544
                }
545
            }
546
        };
547

548
        let rest = match (state_transition, &self.chain_tip) {
×
549
            (None, Some(chain_tip)) => chain_tip.clone(),
×
550
            (Some(state_transition), _) => {
×
551
                let burnchain_tip = BurnchainTip {
×
552
                    block_snapshot,
×
553
                    state_transition: BurnchainStateTransitionOps::from(state_transition),
×
554
                    received_at: Instant::now(),
×
555
                };
×
556
                self.chain_tip = Some(burnchain_tip.clone());
×
557
                burnchain_tip
×
558
            }
559
            (None, None) => {
560
                // can happen at genesis
561
                let burnchain_tip = BurnchainTip {
×
562
                    block_snapshot,
×
563
                    state_transition: BurnchainStateTransitionOps::noop(),
×
564
                    received_at: Instant::now(),
×
565
                };
×
566
                self.chain_tip = Some(burnchain_tip.clone());
×
567
                burnchain_tip
×
568
            }
569
        };
570

571
        debug!("Done receiving blocks");
×
572
        rest
×
573
    }
×
574

575
    fn receive_blocks(
476,480✔
576
        &mut self,
476,480✔
577
        block_for_sortitions: bool,
476,480✔
578
        target_block_height_opt: Option<u64>,
476,480✔
579
    ) -> Result<(BurnchainTip, u64), BurnchainControllerError> {
476,480✔
580
        let coordinator_comms = match self.use_coordinator.as_ref() {
476,480✔
581
            Some(x) => x.clone(),
476,480✔
582
            None => {
583
                // pre-PoX helium node
584
                let tip = self.receive_blocks_helium();
×
585
                let height = tip.block_snapshot.block_height;
×
586
                return Ok((tip, height));
×
587
            }
588
        };
589

590
        let mut burnchain = self.get_burnchain();
476,480✔
591
        let (block_snapshot, burnchain_height, state_transition) = loop {
476,413✔
592
            if !self.should_keep_running() {
476,555✔
593
                return Err(BurnchainControllerError::CoordinatorClosed);
62✔
594
            }
476,493✔
595

596
            match burnchain.sync_with_indexer(
476,493✔
597
                &mut self.indexer,
476,493✔
598
                coordinator_comms.clone(),
476,493✔
599
                target_block_height_opt,
476,493✔
600
                Some(burnchain.pox_constants.reward_cycle_length as u64),
476,493✔
601
                self.should_keep_running.clone(),
476,493✔
602
            ) {
476,493✔
603
                Ok(x) => {
476,414✔
604
                    increment_btc_blocks_received_counter();
476,414✔
605

606
                    // initialize the dbs...
607
                    self.sortdb_mut();
476,414✔
608

609
                    // wait for the chains coordinator to catch up with us.
610
                    // don't wait for heights beyond the burnchain tip.
611
                    if block_for_sortitions {
476,414✔
612
                        self.wait_for_sortitions(
475,821✔
613
                            coordinator_comms,
475,821✔
614
                            target_block_height_opt.unwrap_or(x.block_height),
475,821✔
615
                        )?;
1✔
616
                    }
593✔
617

618
                    // NOTE: This is the latest _sortition_ on the canonical sortition history, not the latest burnchain block!
619
                    let sort_tip =
476,413✔
620
                        SortitionDB::get_canonical_burn_chain_tip(self.sortdb_ref().conn())
476,413✔
621
                            .expect("Sortition DB error.");
476,413✔
622

623
                    let (snapshot, state_transition) = self
476,413✔
624
                        .sortdb_ref()
476,413✔
625
                        .get_sortition_result(&sort_tip.sortition_id)
476,413✔
626
                        .expect("Sortition DB error.")
476,413✔
627
                        .expect("BUG: no data for the canonical chain tip");
476,413✔
628

629
                    let burnchain_height = self
476,413✔
630
                        .indexer
476,413✔
631
                        .get_highest_header_height()
476,413✔
632
                        .map_err(BurnchainControllerError::IndexerError)?;
476,413✔
633
                    break (snapshot, burnchain_height, state_transition);
476,413✔
634
                }
635
                Err(e) => {
79✔
636
                    // keep trying
637
                    error!("Unable to sync with burnchain: {e}");
79✔
638
                    match e {
79✔
639
                        burnchain_error::CoordinatorClosed => {
640
                            return Err(BurnchainControllerError::CoordinatorClosed)
4✔
641
                        }
642
                        burnchain_error::TrySyncAgain => {
643
                            // try again immediately
644
                            continue;
63✔
645
                        }
646
                        burnchain_error::BurnchainPeerBroken => {
647
                            // remote burnchain peer broke, and produced a shorter blockchain fork.
648
                            // just keep trying
649
                            sleep_ms(5000);
1✔
650
                            continue;
1✔
651
                        }
652
                        _ => {
653
                            // delay and try again
654
                            sleep_ms(5000);
16✔
655
                            continue;
16✔
656
                        }
657
                    }
658
                }
659
            }
660
        };
661

662
        let burnchain_tip = BurnchainTip {
476,413✔
663
            block_snapshot,
476,413✔
664
            state_transition,
476,413✔
665
            received_at: Instant::now(),
476,413✔
666
        };
476,413✔
667

668
        let received = self
476,413✔
669
            .chain_tip
476,413✔
670
            .as_ref()
476,413✔
671
            .map(|tip| tip.block_snapshot.block_height)
476,413✔
672
            .unwrap_or(0)
476,413✔
673
            == burnchain_tip.block_snapshot.block_height;
476,413✔
674
        self.chain_tip = Some(burnchain_tip.clone());
476,413✔
675
        debug!("Done receiving blocks");
476,413✔
676

677
        if self.config.burnchain.fault_injection_burnchain_block_delay > 0 && received {
476,413✔
678
            info!(
×
679
                "Fault injection: delaying burnchain blocks by {} milliseconds",
680
                self.config.burnchain.fault_injection_burnchain_block_delay
681
            );
682
            sleep_ms(self.config.burnchain.fault_injection_burnchain_block_delay);
×
683
        }
476,413✔
684

685
        Ok((burnchain_tip, burnchain_height))
476,413✔
686
    }
476,480✔
687

688
    fn should_keep_running(&self) -> bool {
483,905✔
689
        match self.should_keep_running {
483,905✔
690
            Some(ref should_keep_running) => should_keep_running.load(Ordering::SeqCst),
483,905✔
691
            _ => true,
×
692
        }
693
    }
483,905✔
694

695
    /// Retrieves all UTXOs associated with the given public key.
696
    ///
697
    /// The address to query is computed from the public key,
698
    /// disregard the epoch we're in and currently set to [`StacksEpochId::Epoch21`].
699
    ///
700
    /// Automatically imports descriptors into the wallet for the public_key
701
    #[cfg(test)]
702
    pub fn get_all_utxos(&self, public_key: &Secp256k1PublicKey) -> Vec<UTXO> {
80✔
703
        const EPOCH: StacksEpochId = StacksEpochId::Epoch21;
704
        let address = self.get_miner_address(EPOCH, public_key);
80✔
705
        let pub_key_rev = self.to_epoch_aware_pubkey(EPOCH, public_key);
80✔
706

707
        test_debug!("Import public key '{}'", &pub_key_rev.to_hex());
80✔
708
        self.import_public_key(&pub_key_rev)
80✔
709
            .unwrap_or_else(|error| {
80✔
710
                panic!(
×
711
                    "Import public key '{}' failed: {error:?}",
712
                    pub_key_rev.to_hex()
×
713
                )
714
            });
715

716
        sleep_ms(1000);
80✔
717

718
        self.retrieve_utxo_set(&address, true, 1, &None, 0)
80✔
719
            .unwrap_or_log_panic("retrieve all utxos")
80✔
720
            .utxos
80✔
721
    }
80✔
722

723
    /// Retrieve all loaded wallets.
724
    pub fn list_wallets(&self) -> BitcoinRegtestControllerResult<Vec<String>> {
831✔
725
        Ok(self.get_rpc_client().list_wallets()?)
831✔
726
    }
831✔
727

728
    /// Ensures the configured wallet exists and is loaded in the connected
729
    /// bitcoin node. Loads it from disk when needed and configures bitcoind to
730
    /// load it again after a restart.
731
    pub fn ensure_wallet_loaded(&self) -> BitcoinRegtestControllerResult<()> {
829✔
732
        let wallet_name = self.get_wallet_name();
829✔
733
        if wallet_name.trim().is_empty() {
829✔
734
            return Err(BitcoinRegtestControllerError::MissingWalletName);
1✔
735
        }
828✔
736

737
        if self.list_wallets()?.iter().any(|name| name == wallet_name) {
828✔
738
            return Ok(());
545✔
739
        }
283✔
740

741
        // load_on_startup, or the wallet is gone after a bitcoind restart
742
        let on_disk_wallets = self.get_rpc_client().list_wallet_dir()?;
283✔
743
        if on_disk_wallets.iter().any(|name| name == wallet_name) {
283✔
744
            self.get_rpc_client().load_wallet(wallet_name, Some(true))?;
1✔
745
        } else {
746
            return Err(BitcoinRegtestControllerError::WalletNotFound(
282✔
747
                wallet_name.to_string(),
282✔
748
            ));
282✔
749
        }
750
        Ok(())
1✔
751
    }
829✔
752

753
    /// Creates the configured test wallet when absent, then ensures it is
754
    /// loaded. Production miners must provision their wallet before startup.
755
    #[cfg(test)]
756
    fn ensure_test_wallet_loaded(&self) -> BitcoinRegtestControllerResult<()> {
285✔
757
        match self.ensure_wallet_loaded() {
285✔
758
            Err(BitcoinRegtestControllerError::WalletNotFound(_)) => {
759
                self.get_rpc_client().create_wallet(
281✔
760
                    self.get_wallet_name(),
281✔
761
                    Some(true),
281✔
762
                    Some(true),
281✔
NEW
763
                )?;
×
764
                Ok(())
281✔
765
            }
766
            result => result,
4✔
767
        }
768
    }
285✔
769

770
    pub fn get_utxos(
9,841✔
771
        &self,
9,841✔
772
        epoch_id: StacksEpochId,
9,841✔
773
        public_key: &Secp256k1PublicKey,
9,841✔
774
        total_required: u64,
9,841✔
775
        utxos_to_exclude: Option<UTXOSet>,
9,841✔
776
        block_height: u64,
9,841✔
777
    ) -> Option<UTXOSet> {
9,841✔
778
        let pub_key_rev = self.to_epoch_aware_pubkey(epoch_id, public_key);
9,841✔
779

780
        // Configure UTXO filter
781
        let address = self.get_miner_address(epoch_id, &pub_key_rev);
9,841✔
782
        test_debug!("Get UTXOs for {} ({address})", pub_key_rev.to_hex());
9,841✔
783

784
        let mut utxos = loop {
9,841✔
785
            let result = self.retrieve_utxo_set(
9,841✔
786
                &address,
9,841✔
787
                false,
788
                total_required,
9,841✔
789
                &utxos_to_exclude,
9,841✔
790
                block_height,
9,841✔
791
            );
792

793
            // Perform request
794
            match result {
9,841✔
795
                Ok(utxos) => {
9,841✔
796
                    break utxos;
9,841✔
797
                }
798
                Err(e) => {
×
799
                    error!("Bitcoin RPC failure: error listing utxos {e:?}");
×
800
                    sleep_ms(5000);
×
801
                    continue;
×
802
                }
803
            };
804
        };
805

806
        let utxos = if utxos.is_empty() {
9,841✔
807
            let (_, network) = self.config.burnchain.get_bitcoin_network();
25✔
808
            loop {
809
                if let BitcoinNetworkType::Regtest = network {
25✔
810
                    // Performing this operation on Mainnet / Testnet is very expensive, and can be longer than bitcoin block time.
811
                    // Assuming that miners are in charge of correctly operating their bitcoind nodes sounds
812
                    // reasonable to me.
813
                    // $ bitcoin-cli importaddress mxVFsFW5N4mu1HPkxPttorvocvzeZ7KZyk
814
                    let result = self.import_public_key(&pub_key_rev);
25✔
815
                    if let Err(error) = result {
25✔
816
                        warn!(
×
817
                            "Import public key '{}' failed: {error:?}",
818
                            &pub_key_rev.to_hex()
×
819
                        );
820
                    }
25✔
821
                    sleep_ms(1000);
25✔
822
                }
×
823

824
                let result = self.retrieve_utxo_set(
25✔
825
                    &address,
25✔
826
                    false,
827
                    total_required,
25✔
828
                    &utxos_to_exclude,
25✔
829
                    block_height,
25✔
830
                );
831

832
                utxos = match result {
25✔
833
                    Ok(utxos) => utxos,
25✔
834
                    Err(e) => {
×
835
                        error!("Bitcoin RPC failure: error listing utxos {e:?}");
×
836
                        sleep_ms(5000);
×
837
                        continue;
×
838
                    }
839
                };
840

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

843
                if utxos.is_empty() {
25✔
844
                    return None;
14✔
845
                } else {
846
                    break utxos;
11✔
847
                }
848
            }
849
        } else {
850
            debug!("Got {} UTXOs for {address:?}", utxos.utxos.len(),);
9,816✔
851
            utxos
9,816✔
852
        };
853

854
        let total_unspent = utxos.total_available();
9,827✔
855
        if total_unspent < total_required {
9,827✔
856
            warn!(
×
857
                "Total unspent {total_unspent} < {total_required} for {:?}",
858
                &pub_key_rev.to_hex()
×
859
            );
860
            return None;
×
861
        }
9,827✔
862

863
        Some(utxos)
9,827✔
864
    }
9,841✔
865

866
    fn build_leader_key_register_tx(
289✔
867
        &mut self,
289✔
868
        epoch_id: StacksEpochId,
289✔
869
        payload: LeaderKeyRegisterOp,
289✔
870
        signer: &mut BurnchainOpSigner,
289✔
871
    ) -> Result<Transaction, BurnchainControllerError> {
289✔
872
        let public_key = signer.get_public_key();
289✔
873

874
        // reload the config to find satoshis_per_byte changes
875
        let btc_miner_fee = self.config.burnchain.leader_key_tx_estimated_size
289✔
876
            * get_satoshis_per_byte(&self.config);
289✔
877
        let budget_for_outputs = DUST_UTXO_LIMIT;
289✔
878
        let total_required = btc_miner_fee + budget_for_outputs;
289✔
879

880
        let (mut tx, mut utxos) =
288✔
881
            self.prepare_tx(epoch_id, &public_key, total_required, None, None, 0)?;
289✔
882

883
        // Serialize the payload
884
        let op_bytes = {
288✔
885
            let mut buffer = vec![];
288✔
886
            let mut magic_bytes = self.config.burnchain.magic_bytes.as_bytes().to_vec();
288✔
887
            buffer.append(&mut magic_bytes);
288✔
888
            payload
288✔
889
                .consensus_serialize(&mut buffer)
288✔
890
                .expect("FATAL: invalid operation");
288✔
891
            buffer
288✔
892
        };
893

894
        let consensus_output = TxOut {
288✔
895
            value: 0,
288✔
896
            script_pubkey: Builder::new()
288✔
897
                .push_opcode(opcodes::All::OP_RETURN)
288✔
898
                .push_slice(&op_bytes)
288✔
899
                .into_script(),
288✔
900
        };
288✔
901

902
        tx.output = vec![consensus_output];
288✔
903

904
        let fee_rate = get_satoshis_per_byte(&self.config);
288✔
905

906
        self.finalize_tx(
288✔
907
            epoch_id,
288✔
908
            &mut tx,
288✔
909
            budget_for_outputs,
288✔
910
            0,
911
            self.config.burnchain.leader_key_tx_estimated_size,
288✔
912
            fee_rate,
288✔
913
            &mut utxos,
288✔
914
            signer,
288✔
915
            true, // key register op requires change output to exist
916
        );
917

918
        increment_btc_ops_sent_counter();
288✔
919

920
        info!(
288✔
921
            "Miner node: submitting leader_key_register op - {}, waiting for its inclusion in the next Bitcoin block",
922
            public_key.to_hex()
288✔
923
        );
924

925
        Ok(tx)
288✔
926
    }
289✔
927

928
    #[cfg(not(test))]
929
    fn build_transfer_stacks_tx(
930
        &mut self,
931
        _epoch_id: StacksEpochId,
932
        _payload: TransferStxOp,
933
        _signer: &mut BurnchainOpSigner,
934
        _utxo: Option<UTXO>,
935
    ) -> Result<Transaction, BurnchainControllerError> {
936
        unimplemented!()
937
    }
938

939
    #[cfg(not(test))]
940
    fn build_delegate_stacks_tx(
941
        &mut self,
942
        _epoch_id: StacksEpochId,
943
        _payload: DelegateStxOp,
944
        _signer: &mut BurnchainOpSigner,
945
        _utxo: Option<UTXO>,
946
    ) -> Result<Transaction, BurnchainControllerError> {
947
        unimplemented!()
948
    }
949

950
    #[cfg(test)]
951
    pub fn submit_manual(
2✔
952
        &mut self,
2✔
953
        epoch_id: StacksEpochId,
2✔
954
        operation: BlockstackOperationType,
2✔
955
        op_signer: &mut BurnchainOpSigner,
2✔
956
        utxo: Option<UTXO>,
2✔
957
    ) -> Result<Transaction, BurnchainControllerError> {
2✔
958
        let transaction = match operation {
2✔
959
            BlockstackOperationType::LeaderBlockCommit(_)
960
            | BlockstackOperationType::LeaderKeyRegister(_)
961
            | BlockstackOperationType::StackStx(_)
962
            | BlockstackOperationType::DelegateStx(_)
963
            | BlockstackOperationType::VoteForAggregateKey(_) => {
964
                unimplemented!();
×
965
            }
966
            BlockstackOperationType::PreStx(payload) => {
1✔
967
                self.build_pre_stacks_tx(epoch_id, payload, op_signer)
1✔
968
            }
969
            BlockstackOperationType::TransferStx(payload) => {
1✔
970
                self.build_transfer_stacks_tx(epoch_id, payload, op_signer, utxo)
1✔
971
            }
972
        }?;
×
973
        self.send_transaction(&transaction).map(|_| transaction)
2✔
974
    }
2✔
975

976
    #[cfg(test)]
977
    /// Build a transfer stacks tx.
978
    ///   this *only* works if the only existant UTXO is from a PreStx Op
979
    ///   this is okay for testing, but obviously not okay for actual use.
980
    ///   The reason for this constraint is that the bitcoin_regtest_controller's UTXO
981
    ///     and signing logic are fairly intertwined, and untangling the two seems excessive
982
    ///     for a functionality that won't be implemented for production via this controller.
983
    fn build_transfer_stacks_tx(
4✔
984
        &mut self,
4✔
985
        epoch_id: StacksEpochId,
4✔
986
        payload: TransferStxOp,
4✔
987
        signer: &mut BurnchainOpSigner,
4✔
988
        utxo_to_use: Option<UTXO>,
4✔
989
    ) -> Result<Transaction, BurnchainControllerError> {
4✔
990
        let public_key = signer.get_public_key();
4✔
991
        let max_tx_size = OP_TX_TRANSFER_STACKS_ESTIM_SIZE;
4✔
992
        let (mut tx, mut utxos) = if let Some(utxo) = utxo_to_use {
4✔
993
            (
1✔
994
                Transaction {
1✔
995
                    input: vec![],
1✔
996
                    output: vec![],
1✔
997
                    version: 1,
1✔
998
                    lock_time: 0,
1✔
999
                },
1✔
1000
                UTXOSet {
1✔
1001
                    bhh: BurnchainHeaderHash::zero(),
1✔
1002
                    utxos: vec![utxo],
1✔
1003
                },
1✔
1004
            )
1✔
1005
        } else {
1006
            self.prepare_tx(
3✔
1007
                epoch_id,
3✔
1008
                &public_key,
3✔
1009
                DUST_UTXO_LIMIT + max_tx_size * get_satoshis_per_byte(&self.config),
3✔
1010
                None,
3✔
1011
                None,
3✔
1012
                0,
1013
            )?
×
1014
        };
1015

1016
        // Serialize the payload
1017
        let op_bytes = {
4✔
1018
            let mut bytes = self.config.burnchain.magic_bytes.as_bytes().to_vec();
4✔
1019
            payload
4✔
1020
                .consensus_serialize(&mut bytes)
4✔
1021
                .map_err(BurnchainControllerError::SerializerError)?;
4✔
1022
            bytes
4✔
1023
        };
1024

1025
        let consensus_output = TxOut {
4✔
1026
            value: 0,
4✔
1027
            script_pubkey: Builder::new()
4✔
1028
                .push_opcode(opcodes::All::OP_RETURN)
4✔
1029
                .push_slice(&op_bytes)
4✔
1030
                .into_script(),
4✔
1031
        };
4✔
1032

1033
        tx.output = vec![consensus_output];
4✔
1034
        tx.output
4✔
1035
            .push(PoxAddress::Standard(payload.recipient, None).to_bitcoin_tx_out(DUST_UTXO_LIMIT));
4✔
1036

1037
        self.finalize_tx(
4✔
1038
            epoch_id,
4✔
1039
            &mut tx,
4✔
1040
            DUST_UTXO_LIMIT,
1041
            0,
1042
            max_tx_size,
4✔
1043
            get_satoshis_per_byte(&self.config),
4✔
1044
            &mut utxos,
4✔
1045
            signer,
4✔
1046
            false,
1047
        );
1048

1049
        increment_btc_ops_sent_counter();
4✔
1050

1051
        info!(
4✔
1052
            "Miner node: submitting stacks transfer op - {}",
1053
            public_key.to_hex()
4✔
1054
        );
1055

1056
        Ok(tx)
4✔
1057
    }
4✔
1058

1059
    #[cfg(test)]
1060
    /// Build a delegate stacks tx.
1061
    ///   this *only* works if the only existant UTXO is from a PreStx Op
1062
    ///   this is okay for testing, but obviously not okay for actual use.
1063
    ///   The reason for this constraint is that the bitcoin_regtest_controller's UTXO
1064
    ///     and signing logic are fairly intertwined, and untangling the two seems excessive
1065
    ///     for a functionality that won't be implemented for production via this controller.
1066
    fn build_delegate_stacks_tx(
2✔
1067
        &mut self,
2✔
1068
        epoch_id: StacksEpochId,
2✔
1069
        payload: DelegateStxOp,
2✔
1070
        signer: &mut BurnchainOpSigner,
2✔
1071
        utxo_to_use: Option<UTXO>,
2✔
1072
    ) -> Result<Transaction, BurnchainControllerError> {
2✔
1073
        let public_key = signer.get_public_key();
2✔
1074
        let max_tx_size = OP_TX_DELEGATE_STACKS_ESTIM_SIZE;
2✔
1075

1076
        let (mut tx, mut utxos) = if let Some(utxo) = utxo_to_use {
2✔
1077
            (
×
1078
                Transaction {
×
1079
                    input: vec![],
×
1080
                    output: vec![],
×
1081
                    version: 1,
×
1082
                    lock_time: 0,
×
1083
                },
×
1084
                UTXOSet {
×
1085
                    bhh: BurnchainHeaderHash::zero(),
×
1086
                    utxos: vec![utxo],
×
1087
                },
×
1088
            )
×
1089
        } else {
1090
            self.prepare_tx(
2✔
1091
                epoch_id,
2✔
1092
                &public_key,
2✔
1093
                DUST_UTXO_LIMIT + max_tx_size * get_satoshis_per_byte(&self.config),
2✔
1094
                None,
2✔
1095
                None,
2✔
1096
                0,
1097
            )?
×
1098
        };
1099

1100
        // Serialize the payload
1101
        let op_bytes = {
2✔
1102
            let mut bytes = self.config.burnchain.magic_bytes.as_bytes().to_vec();
2✔
1103
            payload
2✔
1104
                .consensus_serialize(&mut bytes)
2✔
1105
                .map_err(BurnchainControllerError::SerializerError)?;
2✔
1106
            bytes
2✔
1107
        };
1108

1109
        let consensus_output = TxOut {
2✔
1110
            value: 0,
2✔
1111
            script_pubkey: Builder::new()
2✔
1112
                .push_opcode(opcodes::All::OP_RETURN)
2✔
1113
                .push_slice(&op_bytes)
2✔
1114
                .into_script(),
2✔
1115
        };
2✔
1116

1117
        tx.output = vec![consensus_output];
2✔
1118
        tx.output.push(
2✔
1119
            PoxAddress::Standard(payload.delegate_to, None).to_bitcoin_tx_out(DUST_UTXO_LIMIT),
2✔
1120
        );
1121

1122
        self.finalize_tx(
2✔
1123
            epoch_id,
2✔
1124
            &mut tx,
2✔
1125
            DUST_UTXO_LIMIT,
1126
            0,
1127
            max_tx_size,
2✔
1128
            get_satoshis_per_byte(&self.config),
2✔
1129
            &mut utxos,
2✔
1130
            signer,
2✔
1131
            false,
1132
        );
1133

1134
        increment_btc_ops_sent_counter();
2✔
1135

1136
        info!(
2✔
1137
            "Miner node: submitting stacks delegate op - {}",
1138
            public_key.to_hex()
2✔
1139
        );
1140

1141
        Ok(tx)
2✔
1142
    }
2✔
1143

1144
    #[cfg(test)]
1145
    /// Build a vote-for-aggregate-key burn op tx
1146
    fn build_vote_for_aggregate_key_tx(
2✔
1147
        &mut self,
2✔
1148
        epoch_id: StacksEpochId,
2✔
1149
        payload: VoteForAggregateKeyOp,
2✔
1150
        signer: &mut BurnchainOpSigner,
2✔
1151
        utxo_to_use: Option<UTXO>,
2✔
1152
    ) -> Result<Transaction, BurnchainControllerError> {
2✔
1153
        let public_key = signer.get_public_key();
2✔
1154
        let max_tx_size = OP_TX_VOTE_AGG_ESTIM_SIZE;
2✔
1155

1156
        let (mut tx, mut utxos) = if let Some(utxo) = utxo_to_use {
2✔
1157
            (
×
1158
                Transaction {
×
1159
                    input: vec![],
×
1160
                    output: vec![],
×
1161
                    version: 1,
×
1162
                    lock_time: 0,
×
1163
                },
×
1164
                UTXOSet {
×
1165
                    bhh: BurnchainHeaderHash::zero(),
×
1166
                    utxos: vec![utxo],
×
1167
                },
×
1168
            )
×
1169
        } else {
1170
            self.prepare_tx(
2✔
1171
                epoch_id,
2✔
1172
                &public_key,
2✔
1173
                DUST_UTXO_LIMIT + max_tx_size * get_satoshis_per_byte(&self.config),
2✔
1174
                None,
2✔
1175
                None,
2✔
1176
                0,
1177
            )?
×
1178
        };
1179

1180
        // Serialize the payload
1181
        let op_bytes = {
2✔
1182
            let mut bytes = self.config.burnchain.magic_bytes.as_bytes().to_vec();
2✔
1183
            payload
2✔
1184
                .consensus_serialize(&mut bytes)
2✔
1185
                .map_err(BurnchainControllerError::SerializerError)?;
2✔
1186
            bytes
2✔
1187
        };
1188

1189
        let consensus_output = TxOut {
2✔
1190
            value: 0,
2✔
1191
            script_pubkey: Builder::new()
2✔
1192
                .push_opcode(opcodes::All::OP_RETURN)
2✔
1193
                .push_slice(&op_bytes)
2✔
1194
                .into_script(),
2✔
1195
        };
2✔
1196

1197
        tx.output = vec![consensus_output];
2✔
1198

1199
        self.finalize_tx(
2✔
1200
            epoch_id,
2✔
1201
            &mut tx,
2✔
1202
            DUST_UTXO_LIMIT,
1203
            0,
1204
            max_tx_size,
2✔
1205
            get_satoshis_per_byte(&self.config),
2✔
1206
            &mut utxos,
2✔
1207
            signer,
2✔
1208
            false,
1209
        );
1210

1211
        increment_btc_ops_sent_counter();
2✔
1212

1213
        info!(
2✔
1214
            "Miner node: submitting vote for aggregate key op - {}",
1215
            public_key.to_hex()
2✔
1216
        );
1217

1218
        Ok(tx)
2✔
1219
    }
2✔
1220

1221
    #[cfg(not(test))]
1222
    /// Build a vote-for-aggregate-key burn op tx
1223
    fn build_vote_for_aggregate_key_tx(
1224
        &mut self,
1225
        _epoch_id: StacksEpochId,
1226
        _payload: VoteForAggregateKeyOp,
1227
        _signer: &mut BurnchainOpSigner,
1228
        _utxo_to_use: Option<UTXO>,
1229
    ) -> Result<Transaction, BurnchainControllerError> {
1230
        unimplemented!()
1231
    }
1232

1233
    #[cfg(not(test))]
1234
    fn build_pre_stacks_tx(
1235
        &mut self,
1236
        _epoch_id: StacksEpochId,
1237
        _payload: PreStxOp,
1238
        _signer: &mut BurnchainOpSigner,
1239
    ) -> Result<Transaction, BurnchainControllerError> {
1240
        unimplemented!()
1241
    }
1242

1243
    #[cfg(test)]
1244
    fn build_pre_stacks_tx(
16✔
1245
        &mut self,
16✔
1246
        epoch_id: StacksEpochId,
16✔
1247
        payload: PreStxOp,
16✔
1248
        signer: &mut BurnchainOpSigner,
16✔
1249
    ) -> Result<Transaction, BurnchainControllerError> {
16✔
1250
        let public_key = signer.get_public_key();
16✔
1251
        let max_tx_size = OP_TX_PRE_STACKS_ESTIM_SIZE;
16✔
1252

1253
        let max_tx_size_any_op = OP_TX_ANY_ESTIM_SIZE;
16✔
1254
        let output_amt = DUST_UTXO_LIMIT + max_tx_size_any_op * get_satoshis_per_byte(&self.config);
16✔
1255

1256
        let (mut tx, mut utxos) =
15✔
1257
            self.prepare_tx(epoch_id, &public_key, output_amt, None, None, 0)?;
16✔
1258

1259
        // Serialize the payload
1260
        let op_bytes = {
15✔
1261
            let mut bytes = self.config.burnchain.magic_bytes.as_bytes().to_vec();
15✔
1262
            bytes.push(Opcodes::PreStx as u8);
15✔
1263
            bytes
15✔
1264
        };
1265

1266
        let consensus_output = TxOut {
15✔
1267
            value: 0,
15✔
1268
            script_pubkey: Builder::new()
15✔
1269
                .push_opcode(opcodes::All::OP_RETURN)
15✔
1270
                .push_slice(&op_bytes)
15✔
1271
                .into_script(),
15✔
1272
        };
15✔
1273

1274
        tx.output = vec![consensus_output];
15✔
1275
        tx.output
15✔
1276
            .push(PoxAddress::Standard(payload.output, None).to_bitcoin_tx_out(output_amt));
15✔
1277

1278
        self.finalize_tx(
15✔
1279
            epoch_id,
15✔
1280
            &mut tx,
15✔
1281
            output_amt,
15✔
1282
            0,
1283
            max_tx_size,
15✔
1284
            get_satoshis_per_byte(&self.config),
15✔
1285
            &mut utxos,
15✔
1286
            signer,
15✔
1287
            false,
1288
        );
1289

1290
        increment_btc_ops_sent_counter();
15✔
1291

1292
        info!(
15✔
1293
            "Miner node: submitting pre_stacks op - {}",
1294
            public_key.to_hex()
15✔
1295
        );
1296

1297
        Ok(tx)
15✔
1298
    }
16✔
1299

1300
    #[cfg_attr(test, mutants::skip)]
1301
    #[cfg(not(test))]
1302
    fn build_stack_stx_tx(
1303
        &mut self,
1304
        _epoch_id: StacksEpochId,
1305
        _payload: StackStxOp,
1306
        _signer: &mut BurnchainOpSigner,
1307
        _utxo_to_use: Option<UTXO>,
1308
    ) -> Result<Transaction, BurnchainControllerError> {
1309
        unimplemented!()
1310
    }
1311

1312
    #[cfg(test)]
1313
    fn build_stack_stx_tx(
4✔
1314
        &mut self,
4✔
1315
        epoch_id: StacksEpochId,
4✔
1316
        payload: StackStxOp,
4✔
1317
        signer: &mut BurnchainOpSigner,
4✔
1318
        utxo_to_use: Option<UTXO>,
4✔
1319
    ) -> Result<Transaction, BurnchainControllerError> {
4✔
1320
        let public_key = signer.get_public_key();
4✔
1321
        let max_tx_size = OP_TX_STACK_STX_ESTIM_SIZE;
4✔
1322

1323
        let (mut tx, mut utxos) = if let Some(utxo) = utxo_to_use {
4✔
1324
            (
×
1325
                Transaction {
×
1326
                    input: vec![],
×
1327
                    output: vec![],
×
1328
                    version: 1,
×
1329
                    lock_time: 0,
×
1330
                },
×
1331
                UTXOSet {
×
1332
                    bhh: BurnchainHeaderHash::zero(),
×
1333
                    utxos: vec![utxo],
×
1334
                },
×
1335
            )
×
1336
        } else {
1337
            self.prepare_tx(
4✔
1338
                epoch_id,
4✔
1339
                &public_key,
4✔
1340
                DUST_UTXO_LIMIT + max_tx_size * get_satoshis_per_byte(&self.config),
4✔
1341
                None,
4✔
1342
                None,
4✔
1343
                0,
1344
            )?
×
1345
        };
1346

1347
        // Serialize the payload
1348
        let op_bytes = {
4✔
1349
            let mut bytes = self.config.burnchain.magic_bytes.as_bytes().to_vec();
4✔
1350
            payload
4✔
1351
                .consensus_serialize(&mut bytes)
4✔
1352
                .map_err(BurnchainControllerError::SerializerError)?;
4✔
1353
            bytes
4✔
1354
        };
1355

1356
        let consensus_output = TxOut {
4✔
1357
            value: 0,
4✔
1358
            script_pubkey: Builder::new()
4✔
1359
                .push_opcode(opcodes::All::OP_RETURN)
4✔
1360
                .push_slice(&op_bytes)
4✔
1361
                .into_script(),
4✔
1362
        };
4✔
1363

1364
        tx.output = vec![consensus_output];
4✔
1365
        tx.output
4✔
1366
            .push(payload.reward_addr.to_bitcoin_tx_out(DUST_UTXO_LIMIT));
4✔
1367

1368
        self.finalize_tx(
4✔
1369
            epoch_id,
4✔
1370
            &mut tx,
4✔
1371
            DUST_UTXO_LIMIT,
1372
            0,
1373
            max_tx_size,
4✔
1374
            get_satoshis_per_byte(&self.config),
4✔
1375
            &mut utxos,
4✔
1376
            signer,
4✔
1377
            false,
1378
        );
1379

1380
        increment_btc_ops_sent_counter();
4✔
1381

1382
        info!(
4✔
1383
            "Miner node: submitting stack-stx op - {}",
1384
            public_key.to_hex()
4✔
1385
        );
1386

1387
        Ok(tx)
4✔
1388
    }
4✔
1389

1390
    fn magic_bytes(&self) -> Vec<u8> {
9,539✔
1391
        #[cfg(test)]
1392
        {
1393
            if let Some(set_bytes) = *TEST_MAGIC_BYTES
9,539✔
1394
                .lock()
9,539✔
1395
                .expect("FATAL: test magic bytes mutex poisoned")
9,539✔
1396
            {
1397
                return set_bytes.to_vec();
1✔
1398
            }
9,538✔
1399
        }
1400
        self.config.burnchain.magic_bytes.as_bytes().to_vec()
9,538✔
1401
    }
9,539✔
1402

1403
    #[allow(clippy::too_many_arguments)]
1404
    fn send_block_commit_operation(
10,577✔
1405
        &mut self,
10,577✔
1406
        epoch_id: StacksEpochId,
10,577✔
1407
        payload: LeaderBlockCommitOp,
10,577✔
1408
        signer: &mut BurnchainOpSigner,
10,577✔
1409
        utxos_to_include: Option<UTXOSet>,
10,577✔
1410
        utxos_to_exclude: Option<UTXOSet>,
10,577✔
1411
        previous_fees: Option<LeaderBlockCommitFees>,
10,577✔
1412
        previous_txids: &[Txid],
10,577✔
1413
    ) -> Result<Transaction, BurnchainControllerError> {
10,577✔
1414
        let _ = self.sortdb_mut();
10,577✔
1415
        let burn_chain_tip = self
10,577✔
1416
            .burnchain_db
10,577✔
1417
            .as_ref()
10,577✔
1418
            .ok_or(BurnchainControllerError::BurnchainError)?
10,577✔
1419
            .get_canonical_chain_tip()
10,577✔
1420
            .map_err(|_| BurnchainControllerError::BurnchainError)?;
10,577✔
1421
        let estimated_fees = match previous_fees {
10,577✔
1422
            Some(fees) => fees.fees_from_previous_tx(&payload, &self.config),
572✔
1423
            None => LeaderBlockCommitFees::estimated_fees_from_payload(&payload, &self.config),
10,005✔
1424
        };
1425

1426
        self.send_block_commit_operation_at_burnchain_height(
10,577✔
1427
            epoch_id,
10,577✔
1428
            payload,
10,577✔
1429
            signer,
10,577✔
1430
            utxos_to_include,
10,577✔
1431
            utxos_to_exclude,
10,577✔
1432
            estimated_fees,
10,577✔
1433
            previous_txids,
10,577✔
1434
            burn_chain_tip.block_height,
10,577✔
1435
        )
1436
    }
10,577✔
1437

1438
    #[allow(clippy::too_many_arguments)]
1439
    fn send_block_commit_operation_at_burnchain_height(
10,578✔
1440
        &mut self,
10,578✔
1441
        epoch_id: StacksEpochId,
10,578✔
1442
        payload: LeaderBlockCommitOp,
10,578✔
1443
        signer: &mut BurnchainOpSigner,
10,578✔
1444
        utxos_to_include: Option<UTXOSet>,
10,578✔
1445
        utxos_to_exclude: Option<UTXOSet>,
10,578✔
1446
        mut estimated_fees: LeaderBlockCommitFees,
10,578✔
1447
        previous_txids: &[Txid],
10,578✔
1448
        burnchain_block_height: u64,
10,578✔
1449
    ) -> Result<Transaction, BurnchainControllerError> {
10,578✔
1450
        let public_key = signer.get_public_key();
10,578✔
1451
        let (mut tx, mut utxos) = self.prepare_tx(
10,578✔
1452
            epoch_id,
10,578✔
1453
            &public_key,
10,578✔
1454
            estimated_fees.estimated_amount_required(),
10,578✔
1455
            utxos_to_include,
10,578✔
1456
            utxos_to_exclude,
10,578✔
1457
            burnchain_block_height,
10,578✔
1458
        )?;
1,038✔
1459

1460
        // Serialize the payload
1461
        let op_bytes = {
9,540✔
1462
            let mut buffer = vec![];
9,540✔
1463
            let mut magic_bytes = self.magic_bytes();
9,540✔
1464
            buffer.append(&mut magic_bytes);
9,540✔
1465
            payload
9,540✔
1466
                .consensus_serialize(&mut buffer)
9,540✔
1467
                .expect("FATAL: invalid operation");
9,540✔
1468
            buffer
9,540✔
1469
        };
1470

1471
        let consensus_output = TxOut {
9,540✔
1472
            value: estimated_fees.sunset_fee,
9,540✔
1473
            script_pubkey: Builder::new()
9,540✔
1474
                .push_opcode(opcodes::All::OP_RETURN)
9,540✔
1475
                .push_slice(&op_bytes)
9,540✔
1476
                .into_script(),
9,540✔
1477
        };
9,540✔
1478

1479
        tx.output = vec![consensus_output];
9,540✔
1480

1481
        for commit_to in payload.commit_outs.iter() {
16,747✔
1482
            tx.output
16,747✔
1483
                .push(commit_to.to_bitcoin_tx_out(estimated_fees.amount_per_output()));
16,747✔
1484
        }
16,747✔
1485

1486
        let fee_rate = estimated_fees.fee_rate;
9,540✔
1487
        self.finalize_tx(
9,540✔
1488
            epoch_id,
9,540✔
1489
            &mut tx,
9,540✔
1490
            estimated_fees.total_spent_in_outputs(),
9,540✔
1491
            estimated_fees.spent_in_attempts,
9,540✔
1492
            estimated_fees.min_tx_size(),
9,540✔
1493
            fee_rate,
9,540✔
1494
            &mut utxos,
9,540✔
1495
            signer,
9,540✔
1496
            true, // block commit op requires change output to exist
1497
        );
1498
        debug!("Transaction relying on UTXOs: {utxos:?}");
9,540✔
1499

1500
        let serialized_tx = serialize(&tx).expect("BUG: failed to serialize to a vec");
9,540✔
1501
        let tx_size = serialized_tx.len() as u64;
9,540✔
1502
        estimated_fees.register_replacement(tx_size);
9,540✔
1503

1504
        let txid = Txid::from_bitcoin_tx_hash(&tx.txid());
9,540✔
1505
        let mut txids = previous_txids.to_vec();
9,540✔
1506
        txids.push(txid.clone());
9,540✔
1507
        let ongoing_block_commit = OngoingBlockCommit {
9,540✔
1508
            payload,
9,540✔
1509
            utxos,
9,540✔
1510
            fees: estimated_fees,
9,540✔
1511
            txids,
9,540✔
1512
        };
9,540✔
1513

1514
        info!(
9,540✔
1515
            "Miner node: submitting leader_block_commit (txid: {}, rbf: {}, total spent: {}, size: {}, fee_rate: {fee_rate})",
1516
            txid.to_hex(),
9,539✔
1517
            ongoing_block_commit.fees.is_rbf_enabled,
1518
            ongoing_block_commit.fees.total_spent(),
9,539✔
1519
            ongoing_block_commit.fees.final_size
1520
        );
1521

1522
        self.ongoing_block_commit = Some(ongoing_block_commit);
9,540✔
1523

1524
        increment_btc_ops_sent_counter();
9,540✔
1525

1526
        Ok(tx)
9,540✔
1527
    }
10,578✔
1528

1529
    fn build_leader_block_commit_tx(
24,954✔
1530
        &mut self,
24,954✔
1531
        epoch_id: StacksEpochId,
24,954✔
1532
        payload: LeaderBlockCommitOp,
24,954✔
1533
        signer: &mut BurnchainOpSigner,
24,954✔
1534
    ) -> Result<Transaction, BurnchainControllerError> {
24,954✔
1535
        // Are we currently tracking an operation?
1536
        if self.ongoing_block_commit.is_none() {
24,954✔
1537
            // Good to go, let's build the transaction and send it.
1538
            let res =
1,537✔
1539
                self.send_block_commit_operation(epoch_id, payload, signer, None, None, None, &[]);
1,537✔
1540
            return res;
1,537✔
1541
        }
23,417✔
1542

1543
        let ongoing_op = self.ongoing_block_commit.take().unwrap();
23,417✔
1544

1545
        let _ = self.sortdb_mut();
23,417✔
1546
        let burnchain_db = self.burnchain_db.as_ref().expect("BurnchainDB not opened");
23,417✔
1547

1548
        for txid in ongoing_op.txids.iter() {
23,946✔
1549
            // check if ongoing_op is in the burnchain_db *or* has been confirmed via the bitcoin RPC
1550
            let mined_op = burnchain_db.find_burnchain_op(&self.indexer, txid);
23,946✔
1551
            let ongoing_tx_confirmed = mined_op.is_some() || self.is_transaction_confirmed(txid);
23,946✔
1552

1553
            test_debug!("Ongoing Tx confirmed: {ongoing_tx_confirmed} - TXID: {txid}");
23,946✔
1554
            if ongoing_tx_confirmed {
23,946✔
1555
                if ongoing_op.payload == payload {
9,709✔
1556
                    info!("Abort attempt to re-submit confirmed LeaderBlockCommit");
1,245✔
1557
                    self.ongoing_block_commit = Some(ongoing_op);
1,245✔
1558
                    return Err(BurnchainControllerError::IdenticalOperation);
1,245✔
1559
                }
8,464✔
1560

1561
                debug!("Was able to retrieve confirmation of ongoing burnchain TXID - {txid}");
8,464✔
1562
                let res = self.send_block_commit_operation(
8,464✔
1563
                    epoch_id,
8,464✔
1564
                    payload,
8,464✔
1565
                    signer,
8,464✔
1566
                    None,
8,464✔
1567
                    None,
8,464✔
1568
                    None,
8,464✔
1569
                    &[],
8,464✔
1570
                );
1571
                return res;
8,464✔
1572
            } else {
1573
                debug!("Was unable to retrieve ongoing TXID - {txid}");
14,237✔
1574
            };
1575
        }
1576

1577
        // Did a re-org occur since we fetched our UTXOs, or are the UTXOs so stale that they should be abandoned?
1578
        let mut traversal_depth = 0;
13,708✔
1579
        let mut burn_chain_tip = burnchain_db
13,708✔
1580
            .get_canonical_chain_tip()
13,708✔
1581
            .map_err(|_| BurnchainControllerError::BurnchainError)?;
13,708✔
1582
        let mut found_last_mined_at = false;
13,708✔
1583
        while traversal_depth < UTXO_CACHE_STALENESS_LIMIT {
13,801✔
1584
            if burn_chain_tip.block_hash == ongoing_op.utxos.bhh {
13,797✔
1585
                found_last_mined_at = true;
13,704✔
1586
                break;
13,704✔
1587
            }
93✔
1588

1589
            let parent = BurnchainDB::get_burnchain_block(
93✔
1590
                burnchain_db.conn(),
93✔
1591
                &burn_chain_tip.parent_block_hash,
93✔
1592
            )
1593
            .map_err(|_| BurnchainControllerError::BurnchainError)?;
93✔
1594

1595
            burn_chain_tip = parent.header;
93✔
1596
            traversal_depth += 1;
93✔
1597
        }
1598

1599
        if !found_last_mined_at {
13,708✔
1600
            info!(
4✔
1601
                "Possible presence of fork or stale UTXO cache, invalidating cached set of UTXOs.";
1602
                "cached_burn_block_hash" => %ongoing_op.utxos.bhh,
1603
            );
1604
            let res =
4✔
1605
                self.send_block_commit_operation(epoch_id, payload, signer, None, None, None, &[]);
4✔
1606
            return res;
4✔
1607
        }
13,704✔
1608

1609
        // Stop as soon as the fee_rate is ${self.config.burnchain.max_rbf} percent higher, stop RBF
1610
        if ongoing_op.fees.fee_rate
13,704✔
1611
            > (get_satoshis_per_byte(&self.config) * get_max_rbf(&self.config) / 100)
13,704✔
1612
        {
1613
            warn!(
×
1614
                "RBF'd block commits reached {}% satoshi per byte fee rate, not resubmitting",
1615
                get_max_rbf(&self.config)
×
1616
            );
1617
            self.ongoing_block_commit = Some(ongoing_op);
×
1618
            return Err(BurnchainControllerError::MaxFeeRateExceeded);
×
1619
        }
13,704✔
1620

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

1625
        // Let's start by early returning (1)
1626
        if payload == ongoing_op.payload {
13,704✔
1627
            info!("Abort attempt to re-submit identical LeaderBlockCommit");
13,132✔
1628
            self.ongoing_block_commit = Some(ongoing_op);
13,132✔
1629
            return Err(BurnchainControllerError::IdenticalOperation);
13,132✔
1630
        }
572✔
1631

1632
        // If we reach this point, we are attempting to RBF the ongoing operation (2)
1633
        info!(
572✔
1634
            "Attempt to replace by fee an outdated leader block commit";
1635
            "ongoing_txids" => ?ongoing_op.txids
1636
        );
1637
        let res = self.send_block_commit_operation(
572✔
1638
            epoch_id,
572✔
1639
            payload,
572✔
1640
            signer,
572✔
1641
            Some(ongoing_op.utxos.clone()),
572✔
1642
            None,
572✔
1643
            Some(ongoing_op.fees.clone()),
572✔
1644
            &ongoing_op.txids,
572✔
1645
        );
1646

1647
        if res.is_err() {
572✔
1648
            self.ongoing_block_commit = Some(ongoing_op);
×
1649
        }
572✔
1650

1651
        res
572✔
1652
    }
24,954✔
1653

1654
    pub(crate) fn get_miner_address(
37,252✔
1655
        &self,
37,252✔
1656
        epoch_id: StacksEpochId,
37,252✔
1657
        public_key: &Secp256k1PublicKey,
37,252✔
1658
    ) -> BitcoinAddress {
37,252✔
1659
        let (_, network_id) = self.config.burnchain.get_bitcoin_network();
37,252✔
1660

1661
        if self.config.miner.segwit && epoch_id >= StacksEpochId::Epoch21 {
37,252✔
1662
            let hash160 = Hash160::from_data(&public_key.to_bytes_compressed());
1✔
1663
            BitcoinAddress::from_bytes_segwit_p2wpkh(network_id, &hash160.0)
1✔
1664
                .expect("Public key incorrect")
1✔
1665
        } else {
1666
            let hash160 = Hash160::from_data(&public_key.to_bytes());
37,251✔
1667
            BitcoinAddress::from_bytes_legacy(
37,251✔
1668
                network_id,
37,251✔
1669
                LegacyBitcoinAddressType::PublicKeyHash,
37,251✔
1670
                &hash160.0,
37,251✔
1671
            )
1672
            .expect("Public key incorrect")
37,251✔
1673
        }
1674
    }
37,252✔
1675

1676
    // TODO: add tests from mutation testing results #4865
1677
    #[cfg_attr(test, mutants::skip)]
1678
    fn prepare_tx(
10,896✔
1679
        &mut self,
10,896✔
1680
        epoch_id: StacksEpochId,
10,896✔
1681
        public_key: &Secp256k1PublicKey,
10,896✔
1682
        total_required: u64,
10,896✔
1683
        utxos_to_include: Option<UTXOSet>,
10,896✔
1684
        utxos_to_exclude: Option<UTXOSet>,
10,896✔
1685
        block_height: u64,
10,896✔
1686
    ) -> Result<(Transaction, UTXOSet), BurnchainControllerError> {
10,896✔
1687
        let utxos = if let Some(utxos) = utxos_to_include {
10,896✔
1688
            // in RBF, you have to consume the same UTXOs
1689
            utxos
573✔
1690
        } else {
1691
            // if mock mining, do not even bother requesting UTXOs
1692
            if self.config.node.mock_mining {
10,323✔
1693
                return Err(BurnchainControllerError::NoUTXOs);
1,038✔
1694
            }
9,285✔
1695

1696
            // Fetch some UTXOs
1697
            let addr = self.get_miner_address(epoch_id, public_key);
9,285✔
1698
            match self.get_utxos(
9,285✔
1699
                epoch_id,
9,285✔
1700
                public_key,
9,285✔
1701
                total_required,
9,285✔
1702
                utxos_to_exclude,
9,285✔
1703
                block_height,
9,285✔
1704
            ) {
9,285✔
1705
                Some(utxos) => utxos,
9,282✔
1706
                None => {
1707
                    warn!(
3✔
1708
                        "No UTXOs for {} ({addr}) in epoch {epoch_id}",
1709
                        &public_key.to_hex(),
2✔
1710
                    );
1711
                    return Err(BurnchainControllerError::NoUTXOs);
3✔
1712
                }
1713
            }
1714
        };
1715

1716
        // Prepare a backbone for the tx
1717
        let transaction = Transaction {
9,855✔
1718
            input: vec![],
9,855✔
1719
            output: vec![],
9,855✔
1720
            version: 1,
9,855✔
1721
            lock_time: 0,
9,855✔
1722
        };
9,855✔
1723

1724
        Ok((transaction, utxos))
9,855✔
1725
    }
10,896✔
1726

1727
    #[allow(clippy::too_many_arguments)]
1728
    fn finalize_tx(
9,856✔
1729
        &mut self,
9,856✔
1730
        epoch_id: StacksEpochId,
9,856✔
1731
        tx: &mut Transaction,
9,856✔
1732
        spent_in_outputs: u64,
9,856✔
1733
        spent_in_rbf: u64,
9,856✔
1734
        min_tx_size: u64,
9,856✔
1735
        fee_rate: u64,
9,856✔
1736
        utxos_set: &mut UTXOSet,
9,856✔
1737
        signer: &mut BurnchainOpSigner,
9,856✔
1738
        force_change_output: bool,
9,856✔
1739
    ) {
9,856✔
1740
        // spend UTXOs in order by confirmations.  Spend the least-confirmed UTXO first, and in the
1741
        // event of a tie, spend the smallest-value UTXO first.
1742
        utxos_set.utxos.sort_by(|u1, u2| {
7,299,648✔
1743
            if u1.confirmations != u2.confirmations {
7,299,632✔
1744
                u1.confirmations.cmp(&u2.confirmations)
7,299,631✔
1745
            } else {
1746
                // for block-commits, the smaller value is likely the UTXO-chained value, so
1747
                // continue to prioritize it as the first spend in order to avoid breaking the
1748
                // miner commit chain.
1749
                u1.amount.cmp(&u2.amount)
1✔
1750
            }
1751
        });
7,299,632✔
1752

1753
        let tx_size = {
9,856✔
1754
            // We will be calling 2 times serialize_tx, the first time with an estimated size,
1755
            // Second time with the actual size, computed thanks to the 1st attempt.
1756
            let estimated_rbf = if spent_in_rbf == 0 {
9,856✔
1757
                0
9,284✔
1758
            } else {
1759
                spent_in_rbf + min_tx_size // we're spending 1 sat / byte in RBF
572✔
1760
            };
1761
            let mut tx_cloned = tx.clone();
9,856✔
1762
            let mut utxos_cloned = utxos_set.clone();
9,856✔
1763
            self.serialize_tx(
9,856✔
1764
                epoch_id,
9,856✔
1765
                &mut tx_cloned,
9,856✔
1766
                spent_in_outputs + min_tx_size * fee_rate + estimated_rbf,
9,856✔
1767
                &mut utxos_cloned,
9,856✔
1768
                signer,
9,856✔
1769
                force_change_output,
9,856✔
1770
            );
1771
            let serialized_tx = serialize(&tx_cloned).expect("BUG: failed to serialize to a vec");
9,856✔
1772
            cmp::max(min_tx_size, serialized_tx.len() as u64)
9,856✔
1773
        };
1774

1775
        let rbf_fee = if spent_in_rbf == 0 {
9,856✔
1776
            0
9,284✔
1777
        } else {
1778
            spent_in_rbf + tx_size // we're spending 1 sat / byte in RBF
572✔
1779
        };
1780
        self.serialize_tx(
9,856✔
1781
            epoch_id,
9,856✔
1782
            tx,
9,856✔
1783
            spent_in_outputs + tx_size * fee_rate + rbf_fee,
9,856✔
1784
            utxos_set,
9,856✔
1785
            signer,
9,856✔
1786
            force_change_output,
9,856✔
1787
        );
1788
        signer.dispose();
9,856✔
1789
    }
9,856✔
1790

1791
    /// Sign and serialize a tx, consuming the UTXOs in utxo_set and spending total_to_spend
1792
    /// satoshis.  Uses the key in signer.
1793
    /// If self.config.miner.segwit is true, the transaction's change address will be a p2wpkh
1794
    /// output. Otherwise, it will be a p2pkh output.
1795
    fn serialize_tx(
19,713✔
1796
        &mut self,
19,713✔
1797
        epoch_id: StacksEpochId,
19,713✔
1798
        tx: &mut Transaction,
19,713✔
1799
        tx_cost: u64,
19,713✔
1800
        utxos_set: &mut UTXOSet,
19,713✔
1801
        signer: &mut BurnchainOpSigner,
19,713✔
1802
        force_change_output: bool,
19,713✔
1803
    ) -> bool {
19,713✔
1804
        let mut public_key = signer.get_public_key();
19,713✔
1805

1806
        let total_target = if force_change_output {
19,713✔
1807
            tx_cost + DUST_UTXO_LIMIT
19,659✔
1808
        } else {
1809
            tx_cost
54✔
1810
        };
1811

1812
        // select UTXOs until we have enough to cover the cost
1813
        let mut total_consumed = 0;
19,713✔
1814
        let mut available_utxos = vec![];
19,713✔
1815
        available_utxos.append(&mut utxos_set.utxos);
19,713✔
1816
        for utxo in available_utxos.into_iter() {
19,716✔
1817
            total_consumed += utxo.amount;
19,716✔
1818
            utxos_set.utxos.push(utxo);
19,716✔
1819

1820
            if total_consumed >= total_target {
19,716✔
1821
                break;
19,713✔
1822
            }
3✔
1823
        }
1824

1825
        if total_consumed < total_target {
19,713✔
1826
            warn!("Consumed total {total_consumed} is less than intended spend: {total_target}");
×
1827
            return false;
×
1828
        }
19,713✔
1829

1830
        // Append the change output
1831
        let value = total_consumed - tx_cost;
19,713✔
1832
        debug!(
19,713✔
1833
            "Payments value: {value:?}, total_consumed: {total_consumed:?}, total_spent: {total_target:?}"
1834
        );
1835
        if value >= DUST_UTXO_LIMIT {
19,713✔
1836
            let change_output = if self.config.miner.segwit && epoch_id >= StacksEpochId::Epoch21 {
19,704✔
1837
                // p2wpkh
1838
                public_key.set_compressed(true);
×
1839
                let change_address_hash = Hash160::from_data(&public_key.to_bytes());
×
1840
                SegwitBitcoinAddress::to_p2wpkh_tx_out(&change_address_hash.0, value)
×
1841
            } else {
1842
                // p2pkh
1843
                let change_address_hash = Hash160::from_data(&public_key.to_bytes());
19,704✔
1844
                LegacyBitcoinAddress::to_p2pkh_tx_out(&change_address_hash, value)
19,704✔
1845
            };
1846
            tx.output.push(change_output);
19,704✔
1847
        } else {
1848
            // Instead of leaving that change to the BTC miner, we could / should bump the sortition fee
1849
            debug!("Not enough change to clear dust limit. Not adding change address.");
9✔
1850
        }
1851

1852
        for utxo in utxos_set.utxos.iter() {
19,716✔
1853
            let input = TxIn {
19,716✔
1854
                previous_output: OutPoint {
19,716✔
1855
                    txid: utxo.txid.clone(),
19,716✔
1856
                    vout: utxo.vout,
19,716✔
1857
                },
19,716✔
1858
                script_sig: Script::new(),
19,716✔
1859
                sequence: 0xFFFFFFFD, // allow RBF
19,716✔
1860
                witness: vec![],
19,716✔
1861
            };
19,716✔
1862
            tx.input.push(input);
19,716✔
1863
        }
19,716✔
1864
        for (i, utxo) in utxos_set.utxos.iter().enumerate() {
19,716✔
1865
            let script_pub_key = utxo.script_pub_key.clone();
19,716✔
1866
            let sig_hash_all = 0x01;
19,716✔
1867

1868
            let (sig_hash, is_segwit) = if script_pub_key.as_bytes().len() == 22
19,716✔
1869
                && script_pub_key.as_bytes()[0..2] == [0x00, 0x14]
×
1870
            {
1871
                // p2wpkh
1872
                (
×
1873
                    tx.segwit_signature_hash(i, &script_pub_key, utxo.amount, sig_hash_all),
×
1874
                    true,
×
1875
                )
×
1876
            } else {
1877
                // p2pkh
1878
                (tx.signature_hash(i, &script_pub_key, sig_hash_all), false)
19,716✔
1879
            };
1880

1881
            let sig1_der = {
19,716✔
1882
                let message = signer
19,716✔
1883
                    .sign_message(sig_hash.as_bytes())
19,716✔
1884
                    .expect("Unable to sign message");
19,716✔
1885
                message
19,716✔
1886
                    .to_secp256k1_recoverable()
19,716✔
1887
                    .expect("Unable to get recoverable signature")
19,716✔
1888
                    .to_standard()
19,716✔
1889
                    .serialize_der()
19,716✔
1890
            };
1891

1892
            if is_segwit {
19,716✔
1893
                // segwit
×
1894
                public_key.set_compressed(true);
×
1895
                tx.input[i].script_sig = Script::from(vec![]);
×
1896
                tx.input[i].witness = vec![
×
1897
                    [&*sig1_der, &[sig_hash_all as u8][..]].concat().to_vec(),
×
1898
                    public_key.to_bytes(),
×
1899
                ];
×
1900
            } else {
19,716✔
1901
                // legacy scriptSig
19,716✔
1902
                tx.input[i].script_sig = Builder::new()
19,716✔
1903
                    .push_slice(&[&*sig1_der, &[sig_hash_all as u8][..]].concat())
19,716✔
1904
                    .push_slice(&public_key.to_bytes())
19,716✔
1905
                    .into_script();
19,716✔
1906
                tx.input[i].witness.clear();
19,716✔
1907
            }
19,716✔
1908
        }
1909
        true
19,713✔
1910
    }
19,713✔
1911

1912
    /// Broadcast a signed raw [`Transaction`] to the underlying Bitcoin node.
1913
    ///
1914
    /// The transaction is submitted with following parameters:
1915
    /// - `max_fee_rate = 0.0` (uncapped, accept any fee rate),
1916
    /// - `max_burn_amount = 1_000_000` (in sats).
1917
    ///
1918
    /// # Arguments
1919
    /// * `transaction` - A fully signed raw [`Transaction`] to broadcast.
1920
    ///
1921
    /// # Returns
1922
    /// On success, returns the [`Txid`] of the broadcasted transaction.
1923
    pub fn send_transaction(&self, tx: &Transaction) -> Result<Txid, BurnchainControllerError> {
9,845✔
1924
        debug!(
9,845✔
1925
            "Sending raw transaction: {}",
1926
            serialize_hex(tx).unwrap_or("SERIALIZATION FAILED".to_string())
×
1927
        );
1928

1929
        const UNCAPPED_FEE: f64 = 0.0;
1930
        const MAX_BURN_AMOUNT: u64 = 1_000_000;
1931
        self.get_rpc_client()
9,845✔
1932
            .send_raw_transaction(tx, Some(UNCAPPED_FEE), Some(MAX_BURN_AMOUNT))
9,845✔
1933
            .map(|txid| {
9,845✔
1934
                debug!("Transaction {txid} sent successfully");
9,842✔
1935
                txid
9,842✔
1936
            })
9,842✔
1937
            .map_err(|e| {
9,845✔
1938
                error!("Bitcoin RPC error: transaction submission failed - {e:?}");
3✔
1939
                BurnchainControllerError::TransactionSubmissionFailed(format!("{e:?}"))
3✔
1940
            })
3✔
1941
    }
9,845✔
1942

1943
    /// wait until the ChainsCoordinator has processed sortitions up to
1944
    /// height_to_wait
1945
    pub fn wait_for_sortitions(
476,678✔
1946
        &self,
476,678✔
1947
        coord_comms: CoordinatorChannels,
476,678✔
1948
        height_to_wait: u64,
476,678✔
1949
    ) -> Result<BurnchainTip, BurnchainControllerError> {
476,678✔
1950
        let mut debug_ctr = 0;
476,678✔
1951
        loop {
1952
            let canonical_sortition_tip =
484,027✔
1953
                SortitionDB::get_canonical_burn_chain_tip(self.sortdb_ref().conn()).unwrap();
484,027✔
1954

1955
            if debug_ctr % 10 == 0 {
484,027✔
1956
                debug!(
476,707✔
1957
                    "Waiting until canonical sortition height reaches {height_to_wait} (currently {})",
1958
                    canonical_sortition_tip.block_height
1959
                );
1960
            }
7,320✔
1961
            debug_ctr += 1;
484,027✔
1962

1963
            if canonical_sortition_tip.block_height >= height_to_wait {
484,027✔
1964
                let (_, state_transition) = self
476,677✔
1965
                    .sortdb_ref()
476,677✔
1966
                    .get_sortition_result(&canonical_sortition_tip.sortition_id)
476,677✔
1967
                    .expect("Sortition DB error.")
476,677✔
1968
                    .expect("BUG: no data for the canonical chain tip");
476,677✔
1969

1970
                return Ok(BurnchainTip {
476,677✔
1971
                    block_snapshot: canonical_sortition_tip,
476,677✔
1972
                    received_at: Instant::now(),
476,677✔
1973
                    state_transition,
476,677✔
1974
                });
476,677✔
1975
            }
7,350✔
1976

1977
            if !self.should_keep_running() {
7,350✔
1978
                return Err(BurnchainControllerError::CoordinatorClosed);
1✔
1979
            }
7,349✔
1980

1981
            // help the chains coordinator along
1982
            coord_comms.announce_new_burn_block();
7,349✔
1983
            coord_comms.announce_new_stacks_block();
7,349✔
1984

1985
            // yield some time
1986
            sleep_ms(1000);
7,349✔
1987
        }
1988
    }
476,678✔
1989

1990
    /// Instruct a regtest Bitcoin node to build the next block.
1991
    pub fn build_next_block(&self, num_blocks: u64) {
9,380✔
1992
        debug!("Generate {num_blocks} block(s)");
9,380✔
1993
        let public_key_bytes = match &self.config.burnchain.local_mining_public_key {
9,380✔
1994
            Some(public_key) => hex_bytes(public_key).expect("Invalid byte sequence"),
9,380✔
1995
            None => panic!("Unable to make new block, mining public key"),
×
1996
        };
1997

1998
        // NOTE: miner address is whatever the configured segwit setting is
1999
        let public_key = Secp256k1PublicKey::from_slice(&public_key_bytes)
9,380✔
2000
            .expect("FATAL: invalid public key bytes");
9,380✔
2001
        let address = self.get_miner_address(StacksEpochId::Epoch21, &public_key);
9,380✔
2002

2003
        let result = self
9,380✔
2004
            .get_rpc_client()
9,380✔
2005
            .generate_to_address(num_blocks, &address);
9,380✔
2006
        /*
2007
            Temporary: not using `BitcoinRpcClientResultExt::ok_or_log_panic` (test code related),
2008
            because we need this logic available outside `#[cfg(test)]` due to Helium network.
2009

2010
            After the Helium cleanup (https://github.com/stacks-network/stacks-core/issues/6408),
2011
            we can:
2012
              - move `build_next_block` behind `#[cfg(test)]`
2013
              - simplify this match by using `ok_or_log_panic`.
2014
        */
2015
        match result {
9,380✔
2016
            Ok(_) => {}
9,380✔
2017
            Err(e) => {
×
2018
                error!("Bitcoin RPC failure: error generating block {e:?}");
×
2019
                panic!();
×
2020
            }
2021
        }
2022
    }
9,380✔
2023

2024
    /// Instruct a regtest Bitcoin node to build an empty block.
2025
    #[cfg(test)]
2026
    pub fn build_empty_block(&self) {
4✔
2027
        info!("Generate empty block");
4✔
2028
        let public_key_bytes = match &self.config.burnchain.local_mining_public_key {
4✔
2029
            Some(public_key) => hex_bytes(public_key).expect("Invalid byte sequence"),
4✔
2030
            None => panic!("Unable to make new block, mining public key"),
×
2031
        };
2032

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

2038
        self.get_rpc_client()
4✔
2039
            .generate_block(&address, &[])
4✔
2040
            .ok_or_log_panic("generating block")
4✔
2041
    }
4✔
2042

2043
    /// Invalidate a block given its hash as a [`BurnchainHeaderHash`].
2044
    #[cfg(test)]
2045
    pub fn invalidate_block(&self, block: &BurnchainHeaderHash) {
30✔
2046
        info!("Invalidating block {block}");
30✔
2047
        self.get_rpc_client()
30✔
2048
            .invalidate_block(block)
30✔
2049
            .ok_or_log_panic("invalidate block")
30✔
2050
    }
30✔
2051

2052
    /// Retrieve the hash (as a [`BurnchainHeaderHash`]) of the block at the given height.
2053
    #[cfg(test)]
2054
    pub fn get_block_hash(&self, height: u64) -> BurnchainHeaderHash {
33✔
2055
        self.get_rpc_client()
33✔
2056
            .get_block_hash(height)
33✔
2057
            .unwrap_or_log_panic("retrieve block")
33✔
2058
    }
33✔
2059

2060
    #[cfg(test)]
2061
    pub fn get_mining_pubkey(&self) -> Option<String> {
5✔
2062
        self.config.burnchain.local_mining_public_key.clone()
5✔
2063
    }
5✔
2064

2065
    #[cfg(test)]
2066
    pub fn set_mining_pubkey(&mut self, pubkey: String) -> Option<String> {
×
2067
        let old_key = self.config.burnchain.local_mining_public_key.take();
×
2068
        self.config.burnchain.local_mining_public_key = Some(pubkey);
×
2069
        old_key
×
2070
    }
×
2071

2072
    #[cfg(test)]
2073
    pub fn set_use_segwit(&mut self, segwit: bool) {
×
2074
        self.config.miner.segwit = segwit;
×
2075
    }
×
2076

2077
    // TODO: add tests from mutation testing results #4866
2078
    #[cfg_attr(test, mutants::skip)]
2079
    fn make_operation_tx(
25,256✔
2080
        &mut self,
25,256✔
2081
        epoch_id: StacksEpochId,
25,256✔
2082
        operation: BlockstackOperationType,
25,256✔
2083
        op_signer: &mut BurnchainOpSigner,
25,256✔
2084
    ) -> Result<Transaction, BurnchainControllerError> {
25,256✔
2085
        match operation {
25,256✔
2086
            BlockstackOperationType::LeaderBlockCommit(payload) => {
24,945✔
2087
                self.build_leader_block_commit_tx(epoch_id, payload, op_signer)
24,945✔
2088
            }
2089
            BlockstackOperationType::LeaderKeyRegister(payload) => {
287✔
2090
                self.build_leader_key_register_tx(epoch_id, payload, op_signer)
287✔
2091
            }
2092
            BlockstackOperationType::PreStx(payload) => {
13✔
2093
                self.build_pre_stacks_tx(epoch_id, payload, op_signer)
13✔
2094
            }
2095
            BlockstackOperationType::TransferStx(payload) => {
3✔
2096
                self.build_transfer_stacks_tx(epoch_id, payload, op_signer, None)
3✔
2097
            }
2098
            BlockstackOperationType::StackStx(_payload) => {
4✔
2099
                self.build_stack_stx_tx(epoch_id, _payload, op_signer, None)
4✔
2100
            }
2101
            BlockstackOperationType::DelegateStx(payload) => {
2✔
2102
                self.build_delegate_stacks_tx(epoch_id, payload, op_signer, None)
2✔
2103
            }
2104
            BlockstackOperationType::VoteForAggregateKey(payload) => {
2✔
2105
                self.build_vote_for_aggregate_key_tx(epoch_id, payload, op_signer, None)
2✔
2106
            }
2107
        }
2108
    }
25,256✔
2109

2110
    /// Retrieves a raw [`Transaction`] by its [`Txid`]
2111
    #[cfg(test)]
2112
    pub fn get_raw_transaction(&self, txid: &Txid) -> Transaction {
62✔
2113
        self.get_rpc_client()
62✔
2114
            .get_raw_transaction(txid)
62✔
2115
            .unwrap_or_log_panic("retrieve raw tx")
62✔
2116
    }
62✔
2117

2118
    /// Build, sign, and broadcast a regular Bitcoin payment from the
2119
    /// miner address to `recipient`.
2120
    ///
2121
    /// Internally this is the same UTXO-selection + signing flow used
2122
    /// by `build_leader_block_commit_tx` / `build_leader_key_register_tx`
2123
    /// — `prepare_tx` picks miner UTXOs, then `finalize_tx` adds a
2124
    /// change output, fills inputs, and signs each one with the keychain
2125
    /// `BurnchainOpSigner` the caller supplies. The resulting tx is
2126
    /// broadcast via `sendrawtransaction`; the next regtest block (e.g.
2127
    /// `build_next_block(1)`) confirms it.
2128
    ///
2129
    /// Intended for tests that need the bondholder / a third party to
2130
    /// receive BTC on regtest without relying on bitcoind's wallet to
2131
    /// sign for the miner (the wallet is created with
2132
    /// `disable_private_keys=true`, so it can't).
2133
    #[cfg(test)]
2134
    pub fn send_btc(
2✔
2135
        &mut self,
2✔
2136
        epoch_id: StacksEpochId,
2✔
2137
        op_signer: &mut BurnchainOpSigner,
2✔
2138
        recipient: &BitcoinAddress,
2✔
2139
        amount: u64,
2✔
2140
    ) -> Result<Txid, BurnchainControllerError> {
2✔
2141
        let public_key = op_signer.get_public_key();
2✔
2142

2143
        let fee_rate = get_satoshis_per_byte(&self.config);
2✔
2144
        // Rough upper-bound for a 1-input, 2-output P2PKH/P2WPKH tx. The
2145
        // `finalize_tx` flow re-serializes to compute the real size; this
2146
        // is just for UTXO selection / change accounting.
2147
        let min_tx_size: u64 = 250;
2✔
2148
        let total_required = amount + min_tx_size * fee_rate;
2✔
2149

2150
        let (mut tx, mut utxos) =
2✔
2151
            self.prepare_tx(epoch_id, &public_key, total_required, None, None, 0)?;
2✔
2152

2153
        let recipient_output = match recipient {
2✔
UNCOV
2154
            BitcoinAddress::Legacy(legacy) => match legacy.addrtype {
×
2155
                LegacyBitcoinAddressType::PublicKeyHash => {
2156
                    LegacyBitcoinAddress::to_p2pkh_tx_out(&legacy.bytes, amount)
×
2157
                }
2158
                LegacyBitcoinAddressType::ScriptHash => {
UNCOV
2159
                    LegacyBitcoinAddress::to_p2sh_tx_out(&legacy.bytes, amount)
×
2160
                }
2161
            },
2162
            BitcoinAddress::Segwit(segwit) => match segwit {
2✔
2163
                SegwitBitcoinAddress::P2WPKH(_, bytes) => {
2✔
2164
                    SegwitBitcoinAddress::to_p2wpkh_tx_out(bytes, amount)
2✔
2165
                }
UNCOV
2166
                SegwitBitcoinAddress::P2WSH(_, bytes) => {
×
UNCOV
2167
                    SegwitBitcoinAddress::to_p2wsh_tx_out(bytes, amount)
×
2168
                }
UNCOV
2169
                SegwitBitcoinAddress::P2TR(_, bytes) => {
×
UNCOV
2170
                    SegwitBitcoinAddress::to_p2tr_tx_out(bytes, amount)
×
2171
                }
2172
            },
2173
        };
2174
        tx.output = vec![recipient_output];
2✔
2175

2176
        self.finalize_tx(
2✔
2177
            epoch_id,
2✔
2178
            &mut tx,
2✔
2179
            amount,
2✔
2180
            0,
2181
            min_tx_size,
2✔
2182
            fee_rate,
2✔
2183
            &mut utxos,
2✔
2184
            op_signer,
2✔
2185
            true,
2186
        );
2187

2188
        info!(
2✔
2189
            "Test send_btc: paying {amount} sats to {recipient}";
2190
            "fee_rate" => fee_rate,
2✔
2191
        );
2192

2193
        self.send_transaction(&tx)
2✔
2194
    }
2✔
2195

2196
    /// Produce `num_blocks` regtest bitcoin blocks, sending the bitcoin coinbase rewards
2197
    ///  to the bitcoin single sig addresses corresponding to `pks` in a round robin fashion.
2198
    #[cfg(test)]
2199
    pub fn bootstrap_chain_to_pks(&self, num_blocks: u64, pks: &[Secp256k1PublicKey]) {
280✔
2200
        info!("Ensuring the test wallet is loaded, creating it if needed");
280✔
2201
        if let Err(e) = self.ensure_test_wallet_loaded() {
280✔
NEW
2202
            error!("Error ensuring wallet is loaded: {e:?}");
×
2203
        }
280✔
2204

2205
        for pk in pks {
321✔
2206
            debug!("Import public key '{}'", &pk.to_hex());
321✔
2207
            if let Err(e) = self.import_public_key(pk) {
321✔
UNCOV
2208
                warn!("Error when importing pubkey: {e:?}");
×
2209
            }
321✔
2210
        }
2211

2212
        if pks.len() == 1 {
280✔
2213
            // if we only have one pubkey, just generate all the blocks at once
2214
            let address = self.get_miner_address(StacksEpochId::Epoch21, &pks[0]);
239✔
2215
            debug!(
239✔
2216
                "Generate to address '{address}' for public key '{}'",
UNCOV
2217
                &pks[0].to_hex()
×
2218
            );
2219
            self.get_rpc_client()
239✔
2220
                .generate_to_address(num_blocks, &address)
239✔
2221
                .ok_or_log_panic("generating block");
239✔
2222
            return;
239✔
2223
        }
41✔
2224

2225
        // otherwise, round robin generate blocks
2226
        let num_blocks = num_blocks as usize;
41✔
2227
        for i in 0..num_blocks {
7,861✔
2228
            let pk = &pks[i % pks.len()];
7,861✔
2229
            let address = self.get_miner_address(StacksEpochId::Epoch21, pk);
7,861✔
2230
            if i < pks.len() {
7,861✔
2231
                debug!(
82✔
2232
                    "Generate to address '{}' for public key '{}'",
UNCOV
2233
                    address.to_string(),
×
UNCOV
2234
                    &pk.to_hex(),
×
2235
                );
2236
            }
7,779✔
2237
            self.get_rpc_client()
7,861✔
2238
                .generate_to_address(1, &address)
7,861✔
2239
                .ok_or_log_panic("generating block");
7,861✔
2240
        }
2241
    }
280✔
2242

2243
    /// Checks whether a transaction has been confirmed by the burnchain
2244
    ///
2245
    /// # Arguments
2246
    ///
2247
    /// * `txid` - The transaction ID to check (in big-endian order)
2248
    ///
2249
    /// # Returns
2250
    ///
2251
    /// * `true` if the transaction is confirmed (has at least one confirmation).
2252
    /// * `false` if the transaction is unconfirmed or could not be found.
2253
    pub fn is_transaction_confirmed(&self, txid: &Txid) -> bool {
15,522✔
2254
        match self
15,522✔
2255
            .get_rpc_client()
15,522✔
2256
            .get_transaction(self.get_wallet_name(), txid)
15,522✔
2257
        {
2258
            Ok(info) => info.confirmations > 0,
14,983✔
2259
            Err(e) => {
539✔
2260
                error!("Bitcoin RPC failure: checking tx confirmation {e:?}");
539✔
2261
                false
539✔
2262
            }
2263
        }
2264
    }
15,522✔
2265

2266
    /// Returns the configured wallet name used for wallet RPC routing.
2267
    fn get_wallet_name(&self) -> &str {
27,015✔
2268
        &self.config.burnchain.wallet_name
27,015✔
2269
    }
27,015✔
2270

2271
    /// Imports a public key into configured wallet by registering its
2272
    /// corresponding addresses as descriptors.
2273
    ///
2274
    /// This computes both **legacy (P2PKH)** and, if the miner is configured
2275
    /// with `segwit` enabled, also **SegWit (P2WPKH)** addresses, then imports
2276
    /// the related descriptors into the wallet.
2277
    pub fn import_public_key(
432✔
2278
        &self,
432✔
2279
        public_key: &Secp256k1PublicKey,
432✔
2280
    ) -> BitcoinRegtestControllerResult<()> {
432✔
2281
        let pkh = Hash160::from_data(&public_key.to_bytes())
432✔
2282
            .to_bytes()
432✔
2283
            .to_vec();
432✔
2284
        let (_, network_id) = self.config.burnchain.get_bitcoin_network();
432✔
2285

2286
        // import both the legacy and segwit variants of this public key
2287
        let mut addresses = vec![BitcoinAddress::from_bytes_legacy(
432✔
2288
            network_id,
432✔
2289
            LegacyBitcoinAddressType::PublicKeyHash,
432✔
2290
            &pkh,
432✔
2291
        )
2292
        .map_err(BitcoinRegtestControllerError::InvalidPublicKey)?];
432✔
2293

2294
        if self.config.miner.segwit {
432✔
2295
            addresses.push(
1✔
2296
                BitcoinAddress::from_bytes_segwit_p2wpkh(network_id, &pkh)
1✔
2297
                    .map_err(BitcoinRegtestControllerError::InvalidPublicKey)?,
1✔
2298
            );
2299
        }
431✔
2300

2301
        for address in addresses.into_iter() {
433✔
2302
            debug!(
433✔
2303
                "Import address {address} for public key {}",
UNCOV
2304
                public_key.to_hex()
×
2305
            );
2306

2307
            let descriptor = format!("addr({address})");
433✔
2308
            let info = self.get_rpc_client().get_descriptor_info(&descriptor)?;
433✔
2309

2310
            let descr_req = ImportDescriptorsRequest {
433✔
2311
                descriptor: format!("addr({address})#{}", info.checksum),
433✔
2312
                timestamp: Timestamp::Time(0),
433✔
2313
                internal: Some(true),
433✔
2314
            };
433✔
2315

2316
            let results = self
433✔
2317
                .get_rpc_client()
433✔
2318
                .import_descriptors(self.get_wallet_name(), &[&descr_req])?;
433✔
2319
            // the RPC reports per-descriptor failures in the response body,
2320
            // e.g. when the target wallet has private keys enabled
2321
            for result in results {
433✔
2322
                if !result.success {
433✔
2323
                    return Err(BitcoinRegtestControllerError::ImportDescriptors(
2324
                        result.error.map_or_else(
1✔
NEW
2325
                            || format!("importing addr({address}) failed with no error message"),
×
2326
                            |e| format!("importing addr({address}) failed: {}", e.message),
1✔
2327
                        ),
2328
                    ));
2329
                }
432✔
2330
            }
2331
        }
2332
        Ok(())
431✔
2333
    }
432✔
2334

2335
    /// Returns a copy of the given public key adjusted to the current epoch rules.
2336
    ///
2337
    /// In particular:
2338
    /// - For epochs **before** [`StacksEpochId::Epoch21`], the public key is returned
2339
    ///   unchanged.
2340
    /// - Starting with [`StacksEpochId::Epoch21`], if **SegWit** is enabled in the miner
2341
    ///   configuration, the key is forced into compressed form.
2342
    ///
2343
    /// # Arguments
2344
    /// * `epoch_id` — The epoch identifier to check against protocol upgrade rules.
2345
    /// * `public_key` — The original public key to adjust.
2346
    ///
2347
    /// # Returns
2348
    /// A [`Secp256k1PublicKey`] that is either the same as the input or compressed,
2349
    /// depending on the epoch and miner configuration.
2350
    fn to_epoch_aware_pubkey(
9,925✔
2351
        &self,
9,925✔
2352
        epoch_id: StacksEpochId,
9,925✔
2353
        public_key: &Secp256k1PublicKey,
9,925✔
2354
    ) -> Secp256k1PublicKey {
9,925✔
2355
        let mut reviewed = public_key.clone();
9,925✔
2356
        if self.config.miner.segwit && epoch_id >= StacksEpochId::Epoch21 {
9,925✔
2357
            reviewed.set_compressed(true);
1✔
2358
        }
9,924✔
2359
        return reviewed;
9,925✔
2360
    }
9,925✔
2361

2362
    /// Retrieves the set of UTXOs for a given address at a specific block height.
2363
    ///
2364
    /// This method queries all unspent outputs belonging to the provided address:
2365
    /// 1. Using a confirmation window of `0..=9_999_999` for the RPC call.
2366
    /// 2. Filtering out UTXOs that:
2367
    ///    - Are present in the optional exclusion set (matched by transaction ID).
2368
    ///    - Have an amount below the specified `minimum_sum_amount`.
2369
    ///
2370
    /// Note: The `block_height` is only used to retrieve the corresponding block hash
2371
    /// and does not affect which UTXOs are included in the result.
2372
    ///
2373
    /// # Arguments
2374
    /// - `address`: The Bitcoin address whose UTXOs should be retrieved.
2375
    /// - `include_unsafe`: Whether to include unsafe UTXOs.
2376
    /// - `minimum_sum_amount`: Minimum amount (in satoshis) that a UTXO must have to be included in the final set.
2377
    /// - `utxos_to_exclude`: Optional set of UTXOs to exclude from the final result.
2378
    /// - `block_height`: The block height at which to resolve the block hash used in the result.
2379
    ///
2380
    /// # Returns
2381
    /// A [`UTXOSet`] containing the filtered UTXOs and the block hash corresponding to `block_height`.
2382
    fn retrieve_utxo_set(
9,951✔
2383
        &self,
9,951✔
2384
        address: &BitcoinAddress,
9,951✔
2385
        include_unsafe: bool,
9,951✔
2386
        minimum_sum_amount: u64,
9,951✔
2387
        utxos_to_exclude: &Option<UTXOSet>,
9,951✔
2388
        block_height: u64,
9,951✔
2389
    ) -> BitcoinRpcClientResult<UTXOSet> {
9,951✔
2390
        let bhh = self.get_rpc_client().get_block_hash(block_height)?;
9,951✔
2391

2392
        const MIN_CONFIRMATIONS: u64 = 0;
2393
        const MAX_CONFIRMATIONS: u64 = 9_999_999;
2394
        let unspents = self.get_rpc_client().list_unspent(
9,950✔
2395
            self.get_wallet_name(),
9,950✔
2396
            Some(MIN_CONFIRMATIONS),
9,950✔
2397
            Some(MAX_CONFIRMATIONS),
9,950✔
2398
            Some(&[address]),
9,950✔
2399
            Some(include_unsafe),
9,950✔
2400
            Some(minimum_sum_amount),
9,950✔
2401
            self.config.burnchain.max_unspent_utxos.clone(),
9,950✔
UNCOV
2402
        )?;
×
2403

2404
        let txids_to_exclude = utxos_to_exclude.as_ref().map_or_else(HashSet::new, |set| {
9,950✔
2405
            set.utxos
4✔
2406
                .iter()
4✔
2407
                .map(|utxo| Txid::from_bitcoin_tx_hash(&utxo.txid))
92✔
2408
                .collect()
4✔
2409
        });
4✔
2410

2411
        let utxos = unspents
9,950✔
2412
            .into_iter()
9,950✔
2413
            .filter(|each| !txids_to_exclude.contains(&each.txid))
1,058,032✔
2414
            .filter(|each| each.amount >= minimum_sum_amount)
1,057,941✔
2415
            .map(|each| UTXO {
9,950✔
2416
                txid: Txid::to_bitcoin_tx_hash(&each.txid),
1,057,915✔
2417
                vout: each.vout,
1,057,915✔
2418
                script_pub_key: each.script_pub_key,
1,057,915✔
2419
                amount: each.amount,
1,057,915✔
2420
                confirmations: each.confirmations,
1,057,915✔
2421
            })
1,057,915✔
2422
            .collect::<Vec<_>>();
9,950✔
2423
        Ok(UTXOSet { bhh, utxos })
9,950✔
2424
    }
9,951✔
2425
}
2426

2427
impl BurnchainController for BitcoinRegtestController {
2428
    fn sortdb_ref(&self) -> &SortitionDB {
2,447,405✔
2429
        self.db
2,447,405✔
2430
            .as_ref()
2,447,405✔
2431
            .expect("BUG: did not instantiate the burn DB")
2,447,405✔
2432
    }
2,447,405✔
2433

2434
    fn sortdb_mut(&mut self) -> &mut SortitionDB {
583,672✔
2435
        let burnchain = self.get_burnchain();
583,672✔
2436

2437
        let (db, burnchain_db) = burnchain.open_db(true).unwrap();
583,672✔
2438
        self.db = Some(db);
583,672✔
2439
        self.burnchain_db = Some(burnchain_db);
583,672✔
2440

2441
        match self.db {
583,672✔
2442
            Some(ref mut sortdb) => sortdb,
583,672✔
UNCOV
2443
            None => unreachable!(),
×
2444
        }
2445
    }
583,672✔
2446

UNCOV
2447
    fn get_chain_tip(&self) -> BurnchainTip {
×
UNCOV
2448
        match &self.chain_tip {
×
UNCOV
2449
            Some(chain_tip) => chain_tip.clone(),
×
2450
            None => {
UNCOV
2451
                unreachable!();
×
2452
            }
2453
        }
UNCOV
2454
    }
×
2455

2456
    fn get_headers_height(&self) -> u64 {
476,989✔
2457
        let (_, network_id) = self.config.burnchain.get_bitcoin_network();
476,989✔
2458
        let spv_client = SpvClient::new(
476,989✔
2459
            &self.config.get_spv_headers_file_path(),
476,989✔
2460
            0,
2461
            None,
476,989✔
2462
            network_id,
476,989✔
2463
            false,
2464
            false,
2465
        )
2466
        .expect("Unable to open burnchain headers DB");
476,989✔
2467
        spv_client
476,989✔
2468
            .get_headers_height()
476,989✔
2469
            .expect("Unable to query number of burnchain headers")
476,989✔
2470
    }
476,989✔
2471

2472
    fn connect_dbs(&mut self) -> Result<(), BurnchainControllerError> {
567✔
2473
        let burnchain = self.get_burnchain();
567✔
2474
        burnchain.connect_db(
567✔
2475
            true,
2476
            &self.indexer.get_first_block_header_hash()?,
567✔
2477
            self.indexer.get_first_block_header_timestamp()?,
567✔
2478
            self.indexer.get_stacks_epochs(),
567✔
UNCOV
2479
        )?;
×
2480
        Ok(())
567✔
2481
    }
567✔
2482

2483
    fn get_stacks_epochs(&self) -> EpochList {
560✔
2484
        self.indexer.get_stacks_epochs()
560✔
2485
    }
560✔
2486

2487
    fn start(
560✔
2488
        &mut self,
560✔
2489
        target_block_height_opt: Option<u64>,
560✔
2490
    ) -> Result<(BurnchainTip, u64), BurnchainControllerError> {
560✔
2491
        // if no target block height is given, just fetch the first burnchain block.
2492
        self.receive_blocks(false, target_block_height_opt.map_or_else(|| Some(1), Some))
560✔
2493
    }
560✔
2494

2495
    fn sync(
475,920✔
2496
        &mut self,
475,920✔
2497
        target_block_height_opt: Option<u64>,
475,920✔
2498
    ) -> Result<(BurnchainTip, u64), BurnchainControllerError> {
475,920✔
2499
        let (burnchain_tip, burnchain_height) = if self.config.burnchain.mode == "helium" {
475,920✔
2500
            // Helium: this node is responsible for mining new burnchain blocks
UNCOV
2501
            self.build_next_block(1);
×
UNCOV
2502
            self.receive_blocks(true, None)?
×
2503
        } else {
2504
            // Neon: this node is waiting on a block to be produced
2505
            self.receive_blocks(true, target_block_height_opt)?
475,920✔
2506
        };
2507

2508
        // Evaluate process_exit_at_block_height setting
2509
        if let Some(cap) = self.config.burnchain.process_exit_at_block_height {
475,858✔
2510
            if burnchain_tip.block_snapshot.block_height >= cap {
38✔
UNCOV
2511
                info!("Node succesfully reached the end of the ongoing {cap} blocks epoch!");
×
UNCOV
2512
                info!("This process will automatically terminate in 30s, restart your node for participating in the next epoch.");
×
UNCOV
2513
                sleep_ms(30000);
×
UNCOV
2514
                std::process::exit(0);
×
2515
            }
38✔
2516
        }
475,820✔
2517
        Ok((burnchain_tip, burnchain_height))
475,858✔
2518
    }
475,920✔
2519

2520
    /// Build and send a burnchain operation transaction.
2521
    /// Returns the [`Txid`] on success, [`BurnchainControllerError`] otherwise.
2522
    /// On [`BitcoinRegtestController::send_transaction`] failure for block commits,
2523
    /// clears `ongoing_block_commit` so the commit can be resubmitted.
2524
    fn submit_operation(
25,253✔
2525
        &mut self,
25,253✔
2526
        epoch_id: StacksEpochId,
25,253✔
2527
        operation: BlockstackOperationType,
25,253✔
2528
        op_signer: &mut BurnchainOpSigner,
25,253✔
2529
    ) -> Result<Txid, BurnchainControllerError> {
25,253✔
2530
        let is_block_commit = matches!(operation, BlockstackOperationType::LeaderBlockCommit(_));
25,253✔
2531
        let transaction = self.make_operation_tx(epoch_id, operation, op_signer)?;
25,253✔
2532
        self.send_transaction(&transaction).inspect_err(|_| {
9,840✔
2533
            if is_block_commit {
3✔
2534
                self.ongoing_block_commit = None;
3✔
2535
            }
3✔
2536
        })
3✔
2537
    }
25,253✔
2538

2539
    #[cfg(test)]
2540
    fn bootstrap_chain(&self, num_blocks: u64) {
133✔
2541
        let Some(ref local_mining_pubkey) = &self.config.burnchain.local_mining_public_key else {
133✔
2542
            warn!("No local mining pubkey while bootstrapping bitcoin regtest, will not generate bitcoin blocks");
1✔
2543
            return;
1✔
2544
        };
2545

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

2549
        if self.config.miner.segwit {
132✔
UNCOV
2550
            local_mining_pubkey.set_compressed(true);
×
2551
        }
132✔
2552

2553
        self.bootstrap_chain_to_pks(num_blocks, &[local_mining_pubkey])
132✔
2554
    }
133✔
2555
}
2556

2557
#[derive(Debug, Clone)]
2558
pub struct UTXOSet {
2559
    bhh: BurnchainHeaderHash,
2560
    utxos: Vec<UTXO>,
2561
}
2562

2563
impl UTXOSet {
2564
    pub fn is_empty(&self) -> bool {
9,865✔
2565
        self.utxos.len() == 0
9,865✔
2566
    }
9,865✔
2567

2568
    pub fn total_available(&self) -> u64 {
9,830✔
2569
        self.utxos.iter().map(|o| o.amount).sum()
9,830✔
2570
    }
9,830✔
2571

2572
    pub fn num_utxos(&self) -> usize {
6✔
2573
        self.utxos.len()
6✔
2574
    }
6✔
2575
}
2576

2577
#[derive(Clone, Debug, PartialEq)]
2578
pub struct UTXO {
2579
    pub txid: Sha256dHash,
2580
    pub vout: u32,
2581
    pub script_pub_key: Script,
2582
    pub amount: u64,
2583
    pub confirmations: u32,
2584
}
2585

2586
#[cfg(test)]
2587
mod tests {
2588
    use std::env::{self, temp_dir};
2589
    use std::fs::File;
2590
    use std::io::Write;
2591
    use std::panic::{self, AssertUnwindSafe};
2592

2593
    use stacks::burnchains::BurnchainSigner;
2594
    use stacks::config::DEFAULT_SATS_PER_VB;
2595
    use stacks_common::deps_common::bitcoin::blockdata::script::Builder;
2596
    use stacks_common::types::chainstate::{BlockHeaderHash, StacksAddress, VRFSeed};
2597
    use stacks_common::util::hash::to_hex;
2598
    use stacks_common::util::secp256k1::Secp256k1PrivateKey;
2599

2600
    use super::*;
2601
    use crate::burnchains::bitcoin::core_controller::BitcoinCoreController;
2602
    use crate::burnchains::bitcoin_regtest_controller::tests::utils::{
2603
        create_follower_config, create_miner_config, to_address_legacy,
2604
    };
2605
    use crate::Keychain;
2606

2607
    mod utils {
2608
        use std::net::TcpListener;
2609

2610
        use stacks::burnchains::MagicBytes;
2611
        use stacks::chainstate::burn::ConsensusHash;
2612
        use stacks::util::vrf::{VRFPrivateKey, VRFPublicKey};
2613

2614
        use super::*;
2615
        use crate::burnchains::bitcoin::core_controller::BURNCHAIN_CONFIG_PEER_PORT_DISABLED;
2616
        use crate::util::get_epoch_time_nanos;
2617

2618
        pub fn create_miner_config() -> Config {
41✔
2619
            let mut config = Config::default();
41✔
2620
            config.node.miner = true;
41✔
2621
            config.burnchain.wallet_name = "test-miner".to_string();
41✔
2622
            config.burnchain.magic_bytes = "T3".as_bytes().into();
41✔
2623
            config.burnchain.username = Some(String::from("user"));
41✔
2624
            config.burnchain.password = Some(String::from("12345"));
41✔
2625
            // overriding default "0.0.0.0" because doesn't play nicely on Windows.
2626
            config.burnchain.peer_host = String::from("127.0.0.1");
41✔
2627
            // avoiding peer port biding to reduce the number of ports to bind to.
2628
            config.burnchain.peer_port = BURNCHAIN_CONFIG_PEER_PORT_DISABLED;
41✔
2629

2630
            //Ask the OS for a free port. Not guaranteed to stay free,
2631
            //after TcpListner is dropped, but good enough for testing
2632
            //and starting bitcoind right after config is created
2633
            let tmp_listener =
41✔
2634
                TcpListener::bind("127.0.0.1:0").expect("Failed to bind to get a free port");
41✔
2635
            let port = tmp_listener.local_addr().unwrap().port();
41✔
2636

2637
            config.burnchain.rpc_port = port;
41✔
2638

2639
            let now = get_epoch_time_nanos();
41✔
2640
            let dir = format!("/tmp/regtest-ctrl-{port}-{now}");
41✔
2641
            config.node.working_dir = dir;
41✔
2642

2643
            config
41✔
2644
        }
41✔
2645

2646
        pub fn create_keychain() -> Keychain {
16✔
2647
            create_keychain_with_seed(1)
16✔
2648
        }
16✔
2649

2650
        pub fn create_keychain_with_seed(value: u8) -> Keychain {
35✔
2651
            let seed = vec![value; 4];
35✔
2652
            let keychain = Keychain::default(seed);
35✔
2653
            keychain
35✔
2654
        }
35✔
2655

2656
        pub fn create_miner1_pubkey() -> Secp256k1PublicKey {
17✔
2657
            create_keychain_with_seed(1).get_pub_key()
17✔
2658
        }
17✔
2659

2660
        pub fn create_miner2_pubkey() -> Secp256k1PublicKey {
2✔
2661
            create_keychain_with_seed(2).get_pub_key()
2✔
2662
        }
2✔
2663

2664
        pub fn to_address_legacy(pub_key: &Secp256k1PublicKey) -> BitcoinAddress {
6✔
2665
            let hash160 = Hash160::from_data(&pub_key.to_bytes());
6✔
2666
            BitcoinAddress::from_bytes_legacy(
6✔
2667
                BitcoinNetworkType::Regtest,
6✔
2668
                LegacyBitcoinAddressType::PublicKeyHash,
6✔
2669
                &hash160.0,
6✔
2670
            )
2671
            .expect("Public key incorrect")
6✔
2672
        }
6✔
2673

2674
        pub fn to_address_segwit_p2wpkh(pub_key: &Secp256k1PublicKey) -> BitcoinAddress {
1✔
2675
            // pub_key.to_byte_compressed() equivalent to pub_key.set_compressed(true) + pub_key.to_bytes()
2676
            let hash160 = Hash160::from_data(&pub_key.to_bytes_compressed());
1✔
2677
            BitcoinAddress::from_bytes_segwit_p2wpkh(BitcoinNetworkType::Regtest, &hash160.0)
1✔
2678
                .expect("Public key incorrect")
1✔
2679
        }
1✔
2680

2681
        pub fn mine_tx(btc_controller: &BitcoinRegtestController, tx: &Transaction) {
2✔
2682
            btc_controller
2✔
2683
                .send_transaction(tx)
2✔
2684
                .expect("Tx should be sent to the burnchain!");
2✔
2685
            btc_controller.build_next_block(1); // Now tx is confirmed
2✔
2686
        }
2✔
2687

2688
        pub fn create_templated_commit_op() -> LeaderBlockCommitOp {
8✔
2689
            LeaderBlockCommitOp {
8✔
2690
                block_header_hash: BlockHeaderHash::from_hex(
8✔
2691
                    "e88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32af",
8✔
2692
                )
8✔
2693
                .unwrap(),
8✔
2694
                new_seed: VRFSeed::from_hex(
8✔
2695
                    "d5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375",
8✔
2696
                )
8✔
2697
                .unwrap(),
8✔
2698
                parent_block_ptr: 2211, // 0x000008a3
8✔
2699
                parent_vtxindex: 1,     // 0x0001
8✔
2700
                key_block_ptr: 1432,    // 0x00000598
8✔
2701
                key_vtxindex: 1,        // 0x0001
8✔
2702
                memo: vec![11],         // 0x5a >> 3
8✔
2703

8✔
2704
                burn_fee: 110_000, //relevant for fee calculation when sending the tx
8✔
2705
                input: (Txid([0x00; 32]), 0),
8✔
2706
                burn_parent_modulus: 2, // 0x5a & 0b111
8✔
2707

8✔
2708
                apparent_sender: BurnchainSigner("mgbpit8FvkVJ9kuXY8QSM5P7eibnhcEMBk".to_string()),
8✔
2709
                commit_outs: vec![
8✔
2710
                    PoxAddress::Standard(StacksAddress::burn_address(false), None),
8✔
2711
                    PoxAddress::Standard(StacksAddress::burn_address(false), None),
8✔
2712
                ],
8✔
2713

8✔
2714
                treatment: vec![],
8✔
2715
                sunset_burn: 5_500, //relevant for fee calculation when sending the tx
8✔
2716

8✔
2717
                txid: Txid([0x00; 32]),
8✔
2718
                vtxindex: 0,
8✔
2719
                block_height: 2212,
8✔
2720
                burn_header_hash: BurnchainHeaderHash([0x01; 32]),
8✔
2721
            }
8✔
2722
        }
8✔
2723

2724
        pub fn txout_opreturn<T: StacksMessageCodec>(
5✔
2725
            op: &T,
5✔
2726
            magic: &MagicBytes,
5✔
2727
            value: u64,
5✔
2728
        ) -> TxOut {
5✔
2729
            let op_bytes = {
5✔
2730
                let mut buffer = vec![];
5✔
2731
                let mut magic_bytes = magic.as_bytes().to_vec();
5✔
2732
                buffer.append(&mut magic_bytes);
5✔
2733
                op.consensus_serialize(&mut buffer)
5✔
2734
                    .expect("FATAL: invalid operation");
5✔
2735
                buffer
5✔
2736
            };
2737

2738
            TxOut {
5✔
2739
                value,
5✔
2740
                script_pubkey: Builder::new()
5✔
2741
                    .push_opcode(opcodes::All::OP_RETURN)
5✔
2742
                    .push_slice(&op_bytes)
5✔
2743
                    .into_script(),
5✔
2744
            }
5✔
2745
        }
5✔
2746

2747
        pub fn txout_opdup_commit_to(addr: &PoxAddress, amount: u64) -> TxOut {
6✔
2748
            addr.to_bitcoin_tx_out(amount)
6✔
2749
        }
6✔
2750

2751
        pub fn txout_opdup_change_legacy(signer: &mut BurnchainOpSigner, amount: u64) -> TxOut {
5✔
2752
            let public_key = signer.get_public_key();
5✔
2753
            let change_address_hash = Hash160::from_data(&public_key.to_bytes());
5✔
2754
            LegacyBitcoinAddress::to_p2pkh_tx_out(&change_address_hash, amount)
5✔
2755
        }
5✔
2756

2757
        pub fn txin_at_index(
5✔
2758
            complete_tx: &Transaction,
5✔
2759
            signer: &BurnchainOpSigner,
5✔
2760
            utxos: &[UTXO],
5✔
2761
            index: usize,
5✔
2762
        ) -> TxIn {
5✔
2763
            //Refresh op signer
2764
            let mut signer = signer.undisposed();
5✔
2765
            let mut public_key = signer.get_public_key();
5✔
2766

2767
            let mut tx = Transaction {
5✔
2768
                version: complete_tx.version,
5✔
2769
                lock_time: complete_tx.lock_time,
5✔
2770
                input: vec![],
5✔
2771
                output: complete_tx.output.clone(),
5✔
2772
            };
5✔
2773

2774
            for utxo in utxos.iter() {
5✔
2775
                let input = TxIn {
5✔
2776
                    previous_output: OutPoint {
5✔
2777
                        txid: utxo.txid.clone(),
5✔
2778
                        vout: utxo.vout,
5✔
2779
                    },
5✔
2780
                    script_sig: Script::new(),
5✔
2781
                    sequence: 0xFFFFFFFD, // allow RBF
5✔
2782
                    witness: vec![],
5✔
2783
                };
5✔
2784
                tx.input.push(input);
5✔
2785
            }
5✔
2786

2787
            for (i, utxo) in utxos.iter().enumerate() {
5✔
2788
                let script_pub_key = utxo.script_pub_key.clone();
5✔
2789
                let sig_hash_all = 0x01;
5✔
2790

2791
                let (sig_hash, is_segwit) = if script_pub_key.as_bytes().len() == 22
5✔
UNCOV
2792
                    && script_pub_key.as_bytes()[0..2] == [0x00, 0x14]
×
2793
                {
2794
                    // p2wpkh
UNCOV
2795
                    (
×
UNCOV
2796
                        tx.segwit_signature_hash(i, &script_pub_key, utxo.amount, sig_hash_all),
×
UNCOV
2797
                        true,
×
UNCOV
2798
                    )
×
2799
                } else {
2800
                    // p2pkh
2801
                    (tx.signature_hash(i, &script_pub_key, sig_hash_all), false)
5✔
2802
                };
2803

2804
                let sig1_der = {
5✔
2805
                    let message = signer
5✔
2806
                        .sign_message(sig_hash.as_bytes())
5✔
2807
                        .expect("Unable to sign message");
5✔
2808
                    message
5✔
2809
                        .to_secp256k1_recoverable()
5✔
2810
                        .expect("Unable to get recoverable signature")
5✔
2811
                        .to_standard()
5✔
2812
                        .serialize_der()
5✔
2813
                };
2814

2815
                if is_segwit {
5✔
UNCOV
2816
                    // segwit
×
UNCOV
2817
                    public_key.set_compressed(true);
×
UNCOV
2818
                    tx.input[i].script_sig = Script::from(vec![]);
×
UNCOV
2819
                    tx.input[i].witness = vec![
×
UNCOV
2820
                        [&*sig1_der, &[sig_hash_all as u8][..]].concat().to_vec(),
×
UNCOV
2821
                        public_key.to_bytes(),
×
UNCOV
2822
                    ];
×
2823
                } else {
5✔
2824
                    // legacy scriptSig
5✔
2825
                    tx.input[i].script_sig = Builder::new()
5✔
2826
                        .push_slice(&[&*sig1_der, &[sig_hash_all as u8][..]].concat())
5✔
2827
                        .push_slice(&public_key.to_bytes())
5✔
2828
                        .into_script();
5✔
2829
                    tx.input[i].witness.clear();
5✔
2830
                }
5✔
2831
            }
2832

2833
            tx.input[index].clone()
5✔
2834
        }
5✔
2835

2836
        pub fn create_templated_leader_key_op() -> LeaderKeyRegisterOp {
4✔
2837
            LeaderKeyRegisterOp {
4✔
2838
                consensus_hash: ConsensusHash([0u8; 20]),
4✔
2839
                public_key: VRFPublicKey::from_private(
4✔
2840
                    &VRFPrivateKey::from_bytes(&[0u8; 32]).unwrap(),
4✔
2841
                ),
4✔
2842
                memo: vec![],
4✔
2843
                txid: Txid([3u8; 32]),
4✔
2844
                vtxindex: 0,
4✔
2845
                block_height: 1,
4✔
2846
                burn_header_hash: BurnchainHeaderHash([9u8; 32]),
4✔
2847
            }
4✔
2848
        }
4✔
2849

2850
        pub fn create_templated_pre_stx_op() -> PreStxOp {
4✔
2851
            PreStxOp {
4✔
2852
                output: StacksAddress::p2pkh_from_hash(false, Hash160::from_data(&[2u8; 20])),
4✔
2853
                txid: Txid([0u8; 32]),
4✔
2854
                vtxindex: 0,
4✔
2855
                block_height: 0,
4✔
2856
                burn_header_hash: BurnchainHeaderHash([0u8; 32]),
4✔
2857
            }
4✔
2858
        }
4✔
2859

2860
        pub fn create_follower_config() -> Config {
2✔
2861
            let mut config = Config::default();
2✔
2862
            config.node.miner = false;
2✔
2863
            config.burnchain.magic_bytes = "T3".as_bytes().into();
2✔
2864
            config.burnchain.username = None;
2✔
2865
            config.burnchain.password = None;
2✔
2866
            config.burnchain.peer_host = String::from("127.0.0.1");
2✔
2867
            config.burnchain.peer_port = 8333;
2✔
2868
            config.node.working_dir = format!("/tmp/follower");
2✔
2869
            config
2✔
2870
        }
2✔
2871
    }
2872

2873
    #[test]
2874
    fn test_get_satoshis_per_byte() {
1✔
2875
        let dir = temp_dir();
1✔
2876
        let file_path = dir.as_path().join("config.toml");
1✔
2877

2878
        let mut config = Config::default();
1✔
2879

2880
        let satoshis_per_byte = get_satoshis_per_byte(&config);
1✔
2881
        assert_eq!(satoshis_per_byte, DEFAULT_SATS_PER_VB);
1✔
2882

2883
        let mut file = File::create(&file_path).unwrap();
1✔
2884
        writeln!(file, "[burnchain]").unwrap();
1✔
2885
        writeln!(file, "satoshis_per_byte = 51").unwrap();
1✔
2886
        config.config_path = Some(file_path.to_str().unwrap().to_string());
1✔
2887

2888
        assert_eq!(get_satoshis_per_byte(&config), 51);
1✔
2889
    }
1✔
2890

2891
    /// Verify that we can build a valid Bitcoin transaction with multiple UTXOs.
2892
    /// Taken from production data.
2893
    /// Tests `serialize_tx()` and `send_block_commit_operation_at_burnchain_height()`
2894
    #[test]
2895
    fn test_multiple_inputs() {
1✔
2896
        let spend_utxos = vec![
1✔
2897
            UTXO {
1✔
2898
                txid: Sha256dHash::from_hex(
1✔
2899
                    "d3eafb3aba3cec925473550ed2e4d00bcb0d00744bb3212e4a8e72878909daee",
1✔
2900
                )
1✔
2901
                .unwrap(),
1✔
2902
                vout: 3,
1✔
2903
                script_pub_key: Builder::from(
1✔
2904
                    hex_bytes("76a9141dc27eba0247f8cc9575e7d45e50a0bc7e72427d88ac").unwrap(),
1✔
2905
                )
1✔
2906
                .into_script(),
1✔
2907
                amount: 42051,
1✔
2908
                confirmations: 1421,
1✔
2909
            },
1✔
2910
            UTXO {
1✔
2911
                txid: Sha256dHash::from_hex(
1✔
2912
                    "01132f2d4a98cc715624e033214c8d841098a1ee15b30188ab89589a320b3b24",
1✔
2913
                )
1✔
2914
                .unwrap(),
1✔
2915
                vout: 0,
1✔
2916
                script_pub_key: Builder::from(
1✔
2917
                    hex_bytes("76a9141dc27eba0247f8cc9575e7d45e50a0bc7e72427d88ac").unwrap(),
1✔
2918
                )
1✔
2919
                .into_script(),
1✔
2920
                amount: 326456,
1✔
2921
                confirmations: 1421,
1✔
2922
            },
1✔
2923
        ];
2924

2925
        // test serialize_tx()
2926
        let config = utils::create_miner_config();
1✔
2927

2928
        let mut btc_controller = BitcoinRegtestController::new(config, None);
1✔
2929
        let mut utxo_set = UTXOSet {
1✔
2930
            bhh: BurnchainHeaderHash([0x01; 32]),
1✔
2931
            utxos: spend_utxos.clone(),
1✔
2932
        };
1✔
2933
        let mut transaction = Transaction {
1✔
2934
            input: vec![],
1✔
2935
            output: vec![
1✔
2936
                TxOut {
1✔
2937
                    value: 0,
1✔
2938
                    script_pubkey: Builder::from(hex_bytes("6a4c5054335be88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32afd5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375000008a300010000059800015a").unwrap()).into_script(),
1✔
2939
                },
1✔
2940
                TxOut {
1✔
2941
                    value: 10000,
1✔
2942
                    script_pubkey: Builder::from(hex_bytes("76a914000000000000000000000000000000000000000088ac").unwrap()).into_script(),
1✔
2943
                },
1✔
2944
                TxOut {
1✔
2945
                    value: 10000,
1✔
2946
                    script_pubkey: Builder::from(hex_bytes("76a914000000000000000000000000000000000000000088ac").unwrap()).into_script(),
1✔
2947
                },
1✔
2948
            ],
1✔
2949
            version: 1,
1✔
2950
            lock_time: 0,
1✔
2951
        };
1✔
2952

2953
        let mut signer = BurnchainOpSigner::new(
1✔
2954
            Secp256k1PrivateKey::from_hex(
1✔
2955
                "9e446f6b0c6a96cf2190e54bcd5a8569c3e386f091605499464389b8d4e0bfc201",
1✔
2956
            )
2957
            .unwrap(),
1✔
2958
        );
2959
        assert!(btc_controller.serialize_tx(
1✔
2960
            StacksEpochId::Epoch25,
1✔
2961
            &mut transaction,
1✔
2962
            44950,
2963
            &mut utxo_set,
1✔
2964
            &mut signer,
1✔
2965
            true
2966
        ));
2967
        assert_eq!(transaction.output[3].value, 323557);
1✔
2968

2969
        // test send_block_commit_operation_at_burn_height()
2970
        let utxo_set = UTXOSet {
1✔
2971
            bhh: BurnchainHeaderHash([0x01; 32]),
1✔
2972
            utxos: spend_utxos,
1✔
2973
        };
1✔
2974

2975
        let commit_op = LeaderBlockCommitOp {
1✔
2976
            block_header_hash: BlockHeaderHash::from_hex(
1✔
2977
                "e88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32af",
1✔
2978
            )
1✔
2979
            .unwrap(),
1✔
2980
            new_seed: VRFSeed::from_hex(
1✔
2981
                "d5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375",
1✔
2982
            )
1✔
2983
            .unwrap(),
1✔
2984
            parent_block_ptr: 2211, // 0x000008a3
1✔
2985
            parent_vtxindex: 1,     // 0x0001
1✔
2986
            key_block_ptr: 1432,    // 0x00000598
1✔
2987
            key_vtxindex: 1,        // 0x0001
1✔
2988
            memo: vec![11],         // 0x5a >> 3
1✔
2989

1✔
2990
            burn_fee: 0,
1✔
2991
            input: (Txid([0x00; 32]), 0),
1✔
2992
            burn_parent_modulus: 2, // 0x5a & 0b111
1✔
2993

1✔
2994
            apparent_sender: BurnchainSigner("mgbpit8FvkVJ9kuXY8QSM5P7eibnhcEMBk".to_string()),
1✔
2995
            commit_outs: vec![
1✔
2996
                PoxAddress::Standard(StacksAddress::burn_address(false), None),
1✔
2997
                PoxAddress::Standard(StacksAddress::burn_address(false), None),
1✔
2998
            ],
1✔
2999

1✔
3000
            treatment: vec![],
1✔
3001
            sunset_burn: 0,
1✔
3002

1✔
3003
            txid: Txid([0x00; 32]),
1✔
3004
            vtxindex: 0,
1✔
3005
            block_height: 2212,
1✔
3006
            burn_header_hash: BurnchainHeaderHash([0x01; 32]),
1✔
3007
        };
1✔
3008

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

3011
        let leader_fees = LeaderBlockCommitFees {
1✔
3012
            sunset_fee: 0,
1✔
3013
            fee_rate: 50,
1✔
3014
            sortition_fee: 20000,
1✔
3015
            outputs_len: 2,
1✔
3016
            default_tx_size: 380,
1✔
3017
            spent_in_attempts: 0,
1✔
3018
            is_rbf_enabled: false,
1✔
3019
            final_size: 498,
1✔
3020
        };
1✔
3021

3022
        assert_eq!(leader_fees.amount_per_output(), 10000);
1✔
3023
        assert_eq!(leader_fees.total_spent(), 44900);
1✔
3024

3025
        let block_commit = btc_controller
1✔
3026
            .send_block_commit_operation_at_burnchain_height(
1✔
3027
                StacksEpochId::Epoch30,
1✔
3028
                commit_op,
1✔
3029
                &mut signer,
1✔
3030
                Some(utxo_set),
1✔
3031
                None,
1✔
3032
                leader_fees,
1✔
3033
                &[],
1✔
3034
                2212,
3035
            )
3036
            .unwrap();
1✔
3037

3038
        debug!("send_block_commit_operation:\n{block_commit:#?}");
1✔
3039
        assert_eq!(block_commit.output[3].value, 323507);
1✔
3040
        assert_eq!(serialize_hex(&block_commit).unwrap(), "0100000002eeda098987728e4a2e21b34b74000dcb0bd0e4d20e55735492ec3cba3afbead3030000006a4730440220558286e20e10ce31537f0625dae5cc62fac7961b9d2cf272c990de96323d7e2502202255adbea3d2e0509b80c5d8a3a4fe6397a87bcf18da1852740d5267d89a0cb20121035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7cfdffffff243b0b329a5889ab8801b315eea19810848d4c2133e0245671cc984a2d2f1301000000006a47304402206d9f8de107f9e1eb15aafac66c2bb34331a7523260b30e18779257e367048d34022013c7dabb32a5c281aa00d405e2ccbd00f34f03a65b2336553a4acd6c52c251ef0121035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7cfdffffff040000000000000000536a4c5054335be88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32afd5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375000008a300010000059800015a10270000000000001976a914000000000000000000000000000000000000000088ac10270000000000001976a914000000000000000000000000000000000000000088acb3ef0400000000001976a9141dc27eba0247f8cc9575e7d45e50a0bc7e72427d88ac00000000");
1✔
3041
    }
1✔
3042

3043
    #[test]
3044
    fn test_to_epoch_aware_pubkey() {
1✔
3045
        let mut config = utils::create_miner_config();
1✔
3046
        let pubkey = utils::create_miner1_pubkey();
1✔
3047

3048
        config.miner.segwit = false;
1✔
3049
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3050

3051
        let reviewed = btc_controller.to_epoch_aware_pubkey(StacksEpochId::Epoch20, &pubkey);
1✔
3052
        assert_eq!(
1✔
3053
            false,
3054
            reviewed.compressed(),
1✔
3055
            "Segwit disabled with Epoch < 2.1: not compressed"
3056
        );
3057
        let reviewed = btc_controller.to_epoch_aware_pubkey(StacksEpochId::Epoch21, &pubkey);
1✔
3058
        assert_eq!(
1✔
3059
            false,
3060
            reviewed.compressed(),
1✔
3061
            "Segwit disabled with Epoch >= 2.1: not compressed"
3062
        );
3063

3064
        config.miner.segwit = true;
1✔
3065
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3066

3067
        let reviewed = btc_controller.to_epoch_aware_pubkey(StacksEpochId::Epoch20, &pubkey);
1✔
3068
        assert_eq!(
1✔
3069
            false,
3070
            reviewed.compressed(),
1✔
3071
            "Segwit enabled with Epoch < 2.1: not compressed"
3072
        );
3073
        let reviewed = btc_controller.to_epoch_aware_pubkey(StacksEpochId::Epoch21, &pubkey);
1✔
3074
        assert_eq!(
1✔
3075
            true,
3076
            reviewed.compressed(),
1✔
3077
            "Segwit enabled with Epoch >= 2.1: compressed"
3078
        );
3079
    }
1✔
3080

3081
    #[test]
3082
    fn test_get_miner_address() {
1✔
3083
        let mut config = utils::create_miner_config();
1✔
3084
        let pub_key = utils::create_miner1_pubkey();
1✔
3085

3086
        config.miner.segwit = false;
1✔
3087
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3088

3089
        let expected = utils::to_address_legacy(&pub_key);
1✔
3090
        let address = btc_controller.get_miner_address(StacksEpochId::Epoch20, &pub_key);
1✔
3091
        assert_eq!(
1✔
3092
            expected, address,
3093
            "Segwit disabled with Epoch < 2.1: legacy addr"
3094
        );
3095

3096
        let expected = utils::to_address_legacy(&pub_key);
1✔
3097
        let address = btc_controller.get_miner_address(StacksEpochId::Epoch21, &pub_key);
1✔
3098
        assert_eq!(
1✔
3099
            expected, address,
3100
            "Segwit disabled with Epoch >= 2.1: legacy addr"
3101
        );
3102

3103
        config.miner.segwit = true;
1✔
3104
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3105

3106
        let expected = utils::to_address_legacy(&pub_key);
1✔
3107
        let address = btc_controller.get_miner_address(StacksEpochId::Epoch20, &pub_key);
1✔
3108
        assert_eq!(
1✔
3109
            expected, address,
3110
            "Segwit enabled with Epoch < 2.1: legacy addr"
3111
        );
3112

3113
        let expected = utils::to_address_segwit_p2wpkh(&pub_key);
1✔
3114
        let address = btc_controller.get_miner_address(StacksEpochId::Epoch21, &pub_key);
1✔
3115
        assert_eq!(
1✔
3116
            expected, address,
3117
            "Segwit enabled with Epoch >= 2.1: segwit addr"
3118
        );
3119
    }
1✔
3120

3121
    #[test]
3122
    fn test_instantiate_with_burnchain_on_follower_node_ok() {
1✔
3123
        let config = create_follower_config();
1✔
3124

3125
        let btc_controller = BitcoinRegtestController::with_burnchain(config, None, None, None);
1✔
3126

3127
        let result = panic::catch_unwind(AssertUnwindSafe(|| {
1✔
3128
            _ = btc_controller.get_rpc_client();
1✔
3129
        }));
1✔
3130
        assert!(
1✔
3131
            result.is_err(),
1✔
3132
            "Invoking any Bitcoin RPC related method should panic."
3133
        );
3134
    }
1✔
3135

3136
    #[test]
3137
    fn test_instantiate_with_burnchain_on_miner_node_ok() {
1✔
3138
        let config = create_miner_config();
1✔
3139

3140
        let btc_controller = BitcoinRegtestController::with_burnchain(config, None, None, None);
1✔
3141

3142
        let _ = btc_controller.get_rpc_client();
1✔
3143
        assert!(true, "Invoking any Bitcoin RPC related method should work.");
1✔
3144
    }
1✔
3145

3146
    #[test]
3147
    fn test_instantiate_with_burnchain_on_miner_node_failure() {
1✔
3148
        let mut config = create_miner_config();
1✔
3149
        config.burnchain.username = None;
1✔
3150
        config.burnchain.password = None;
1✔
3151

3152
        let result = panic::catch_unwind(|| {
1✔
3153
            _ = BitcoinRegtestController::with_burnchain(config, None, None, None);
1✔
3154
        });
1✔
3155
        assert!(
1✔
3156
            result.is_err(),
1✔
3157
            "Bitcoin RPC credentials are mandatory for miner node."
3158
        );
3159
    }
1✔
3160

3161
    #[test]
3162
    fn test_instantiate_new_dummy_on_follower_node_ok() {
1✔
3163
        let config = create_follower_config();
1✔
3164

3165
        let btc_controller = BitcoinRegtestController::new_dummy(config);
1✔
3166

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

3176
    #[test]
3177
    fn test_instantiate_new_dummy_on_miner_node_ok() {
1✔
3178
        let config = create_miner_config();
1✔
3179

3180
        let btc_controller = BitcoinRegtestController::new_dummy(config);
1✔
3181

3182
        let _ = btc_controller.get_rpc_client();
1✔
3183
        assert!(true, "Invoking any Bitcoin RPC related method should work.");
1✔
3184
    }
1✔
3185

3186
    #[test]
3187
    fn test_instantiate_new_dummy_on_miner_node_failure() {
1✔
3188
        let mut config = create_miner_config();
1✔
3189
        config.burnchain.username = None;
1✔
3190
        config.burnchain.password = None;
1✔
3191

3192
        let result = panic::catch_unwind(|| {
1✔
3193
            _ = BitcoinRegtestController::new_dummy(config);
1✔
3194
        });
1✔
3195
        assert!(
1✔
3196
            result.is_err(),
1✔
3197
            "Bitcoin RPC credentials are mandatory for miner node."
3198
        );
3199
    }
1✔
3200

3201
    #[test]
3202
    fn test_ensure_wallet_loaded_rejects_empty_name() {
1✔
3203
        let mut config = utils::create_miner_config();
1✔
3204
        config.burnchain.wallet_name.clear();
1✔
3205

3206
        let btc_controller = BitcoinRegtestController::new(config, None);
1✔
3207
        assert!(matches!(
1✔
3208
            btc_controller.ensure_wallet_loaded(),
1✔
3209
            Err(BitcoinRegtestControllerError::MissingWalletName)
3210
        ));
3211
    }
1✔
3212

3213
    #[test]
3214
    #[ignore]
3215
    fn test_ensure_wallet_loaded_reloads_unloaded_wallet() {
1✔
3216
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3217
            return;
×
3218
        }
1✔
3219

3220
        let config = utils::create_miner_config();
1✔
3221
        let wallet_name = config.burnchain.wallet_name.clone();
1✔
3222

3223
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3224
        btcd_controller
1✔
3225
            .start_bitcoind()
1✔
3226
            .expect("bitcoind should be started!");
1✔
3227

3228
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3229
        btc_controller
1✔
3230
            .ensure_test_wallet_loaded()
1✔
3231
            .expect("Test wallet should be created!");
1✔
3232

3233
        // simulate a bitcoind restart, after which no wallet is loaded
3234
        btc_controller
1✔
3235
            .get_rpc_client()
1✔
3236
            .unload_wallet(&wallet_name)
1✔
3237
            .expect("Wallet should be unloaded!");
1✔
3238
        assert_eq!(0, btc_controller.list_wallets().unwrap().len());
1✔
3239

3240
        btc_controller
1✔
3241
            .ensure_wallet_loaded()
1✔
3242
            .expect("Wallet should be loaded from disk, not re-created!");
1✔
3243

3244
        let wallets = btc_controller.list_wallets().unwrap();
1✔
3245
        assert_eq!(vec![wallet_name], wallets);
1✔
3246
    }
1✔
3247

3248
    #[test]
3249
    #[ignore]
3250
    fn test_wallet_rpcs_route_to_configured_wallet() {
1✔
3251
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
NEW
3252
            return;
×
3253
        }
1✔
3254

3255
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3256

3257
        let config = utils::create_miner_config();
1✔
3258

3259
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3260
        btcd_controller
1✔
3261
            .start_bitcoind()
1✔
3262
            .expect("bitcoind should be started!");
1✔
3263

3264
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3265
        btc_controller
1✔
3266
            .ensure_test_wallet_loaded()
1✔
3267
            .expect("Test wallet should be created!");
1✔
3268

3269
        // A second loaded wallet breaks node-level routing. The miner's RPCs
3270
        // must continue to use its configured wallet.
3271
        btc_controller
1✔
3272
            .get_rpc_client()
1✔
3273
            .create_wallet("other_wallet", Some(true), None)
1✔
3274
            .expect("other_wallet should be created!");
1✔
3275

3276
        btc_controller
1✔
3277
            .import_public_key(&miner_pubkey)
1✔
3278
            .expect("Import should succeed via explicit routing to the configured wallet!");
1✔
3279
    }
1✔
3280

3281
    #[test]
3282
    #[ignore]
3283
    fn test_import_public_key_fails_on_wallet_with_private_keys() {
1✔
3284
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3285
            return;
×
3286
        }
1✔
3287

3288
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3289

3290
        let mut config = utils::create_miner_config();
1✔
3291
        config.burnchain.wallet_name = "keyed".to_string();
1✔
3292

3293
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3294
        btcd_controller
1✔
3295
            .start_bitcoind()
1✔
3296
            .expect("bitcoind should be started!");
1✔
3297

3298
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3299
        btc_controller
1✔
3300
            .get_rpc_client()
1✔
3301
            .create_wallet("keyed", Some(false), None)
1✔
3302
            .expect("keyed wallet should be created!");
1✔
3303

3304
        btc_controller
1✔
3305
            .ensure_wallet_loaded()
1✔
3306
            .expect("Configured wallet should already be loaded!");
1✔
3307

3308
        let result = btc_controller.import_public_key(&miner_pubkey);
1✔
3309
        assert!(
1✔
NEW
3310
            matches!(
×
3311
                result,
1✔
3312
                Err(BitcoinRegtestControllerError::ImportDescriptors(_))
3313
            ),
3314
            "Watch-only import into a wallet with private keys should fail, got: {result:?}"
3315
        );
3316
    }
1✔
3317

3318
    #[test]
3319
    #[ignore]
3320
    fn test_ensure_wallet_loaded_fails_if_wallet_missing() {
1✔
3321
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
NEW
3322
            return;
×
3323
        }
1✔
3324

3325
        let mut config = utils::create_miner_config();
1✔
3326
        config.burnchain.wallet_name = String::from("mywallet");
1✔
3327

3328
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3329
        btcd_controller
1✔
3330
            .start_bitcoind()
1✔
3331
            .expect("bitcoind should be started!");
1✔
3332

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

3335
        assert!(matches!(
1✔
3336
            btc_controller.ensure_wallet_loaded(),
1✔
3337
            Err(BitcoinRegtestControllerError::WalletNotFound(ref name)) if name == "mywallet"
1✔
3338
        ));
3339
        assert!(btc_controller.list_wallets().unwrap().is_empty());
1✔
3340
    }
1✔
3341

3342
    #[test]
3343
    #[ignore]
3344
    fn test_retrieve_utxo_set_with_all_utxos() {
1✔
3345
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3346
            return;
×
3347
        }
1✔
3348

3349
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3350

3351
        let mut config = utils::create_miner_config();
1✔
3352
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3353

3354
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3355
        btcd_controller
1✔
3356
            .start_bitcoind()
1✔
3357
            .expect("Failed starting bitcoind");
1✔
3358

3359
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3360
        btc_controller.bootstrap_chain(150); //produces 50 spendable utxos
1✔
3361

3362
        let address = to_address_legacy(&miner_pubkey);
1✔
3363
        let utxo_set = btc_controller
1✔
3364
            .retrieve_utxo_set(&address, false, 0, &None, 0)
1✔
3365
            .expect("Failed to get utxos");
1✔
3366
        assert_eq!(btc_controller.get_block_hash(0), utxo_set.bhh);
1✔
3367
        assert_eq!(50, utxo_set.num_utxos());
1✔
3368
    }
1✔
3369

3370
    #[test]
3371
    #[ignore]
3372
    fn test_retrive_utxo_set_excluding_some_utxo() {
1✔
3373
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3374
            return;
×
3375
        }
1✔
3376

3377
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3378

3379
        let mut config = utils::create_miner_config();
1✔
3380
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3381

3382
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3383
        btcd_controller
1✔
3384
            .start_bitcoind()
1✔
3385
            .expect("Failed starting bitcoind");
1✔
3386

3387
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3388
        btc_controller.bootstrap_chain(150); //produces 50 spendable utxos
1✔
3389

3390
        let address = to_address_legacy(&miner_pubkey);
1✔
3391
        let mut all_utxos = btc_controller
1✔
3392
            .retrieve_utxo_set(&address, false, 0, &None, 0)
1✔
3393
            .expect("Failed to get utxos (50)");
1✔
3394

3395
        let filtered_utxos = btc_controller
1✔
3396
            .retrieve_utxo_set(&address, false, 0, &Some(all_utxos.clone()), 0)
1✔
3397
            .expect("Failed to get utxos");
1✔
3398
        assert_eq!(0, filtered_utxos.num_utxos(), "all utxos filtered out!");
1✔
3399

3400
        all_utxos.utxos.drain(0..10);
1✔
3401
        let filtered_utxos = btc_controller
1✔
3402
            .retrieve_utxo_set(&address, false, 0, &Some(all_utxos), 0)
1✔
3403
            .expect("Failed to get utxos");
1✔
3404
        assert_eq!(10, filtered_utxos.num_utxos(), "40 utxos filtered out!");
1✔
3405
    }
1✔
3406

3407
    #[test]
3408
    #[ignore]
3409
    fn test_list_unspent_with_max_utxos_config() {
1✔
3410
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3411
            return;
×
3412
        }
1✔
3413

3414
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3415

3416
        let mut config = utils::create_miner_config();
1✔
3417
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3418
        config.burnchain.max_unspent_utxos = Some(10);
1✔
3419

3420
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3421
        btcd_controller
1✔
3422
            .start_bitcoind()
1✔
3423
            .expect("Failed starting bitcoind");
1✔
3424

3425
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3426
        btc_controller.bootstrap_chain(150); //produces 50 spendable utxos
1✔
3427

3428
        let address = to_address_legacy(&miner_pubkey);
1✔
3429
        let utxos = btc_controller
1✔
3430
            .retrieve_utxo_set(&address, false, 1, &None, 0)
1✔
3431
            .expect("Failed to get utxos");
1✔
3432
        assert_eq!(10, utxos.num_utxos());
1✔
3433
    }
1✔
3434

3435
    #[test]
3436
    #[ignore]
3437
    fn test_get_all_utxos_with_confirmation() {
1✔
3438
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3439
            return;
×
3440
        }
1✔
3441

3442
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3443

3444
        let mut config = utils::create_miner_config();
1✔
3445
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3446

3447
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3448
        btcd_controller
1✔
3449
            .start_bitcoind()
1✔
3450
            .expect("bitcoind should be started!");
1✔
3451

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

3454
        btc_controller.bootstrap_chain(100);
1✔
3455
        let utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
3456
        assert_eq!(0, utxos.len());
1✔
3457

3458
        btc_controller.build_next_block(1);
1✔
3459
        let utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
3460
        assert_eq!(1, utxos.len());
1✔
3461
        assert_eq!(101, utxos[0].confirmations);
1✔
3462
        assert_eq!(5_000_000_000, utxos[0].amount);
1✔
3463

3464
        btc_controller.build_next_block(1);
1✔
3465
        let mut utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
3466
        utxos.sort_by(|a, b| b.confirmations.cmp(&a.confirmations));
1✔
3467

3468
        assert_eq!(2, utxos.len());
1✔
3469
        assert_eq!(102, utxos[0].confirmations);
1✔
3470
        assert_eq!(5_000_000_000, utxos[0].amount);
1✔
3471
        assert_eq!(101, utxos[1].confirmations);
1✔
3472
        assert_eq!(5_000_000_000, utxos[1].amount);
1✔
3473
    }
1✔
3474

3475
    #[test]
3476
    #[ignore]
3477
    fn test_get_all_utxos_for_other_pubkey() {
1✔
3478
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3479
            return;
×
3480
        }
1✔
3481

3482
        let miner1_pubkey = utils::create_miner1_pubkey();
1✔
3483
        let miner2_pubkey = utils::create_miner2_pubkey();
1✔
3484

3485
        let mut config = utils::create_miner_config();
1✔
3486
        config.burnchain.local_mining_public_key = Some(miner1_pubkey.to_hex());
1✔
3487

3488
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3489
        btcd_controller
1✔
3490
            .start_bitcoind()
1✔
3491
            .expect("bitcoind should be started!");
1✔
3492

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

3497
        config.burnchain.local_mining_public_key = Some(miner2_pubkey.to_hex());
1✔
3498
        config.burnchain.wallet_name = "miner2_wallet".to_string();
1✔
3499
        let miner2_btc_controller = BitcoinRegtestController::new(config, None);
1✔
3500
        miner2_btc_controller.bootstrap_chain(102); // two utxo for other_pubkeys related address
1✔
3501

3502
        let utxos = miner1_btc_controller.get_all_utxos(&miner1_pubkey);
1✔
3503
        assert_eq!(1, utxos.len(), "miner1 see its own utxos");
1✔
3504

3505
        let utxos = miner1_btc_controller.get_all_utxos(&miner2_pubkey);
1✔
3506
        assert_eq!(2, utxos.len(), "miner1 see miner2 utxos");
1✔
3507

3508
        let utxos = miner2_btc_controller.get_all_utxos(&miner2_pubkey);
1✔
3509
        assert_eq!(2, utxos.len(), "miner2 see its own utxos");
1✔
3510

3511
        let utxos = miner2_btc_controller.get_all_utxos(&miner1_pubkey);
1✔
3512
        assert_eq!(1, utxos.len(), "miner2 see miner1 own utxos");
1✔
3513
    }
1✔
3514

3515
    #[test]
3516
    #[ignore]
3517
    fn test_get_utxos_ok_with_confirmation() {
1✔
3518
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3519
            return;
×
3520
        }
1✔
3521

3522
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3523

3524
        let mut config = utils::create_miner_config();
1✔
3525
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3526

3527
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3528
        btcd_controller
1✔
3529
            .start_bitcoind()
1✔
3530
            .expect("bitcoind should be started!");
1✔
3531

3532
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3533
        btc_controller.bootstrap_chain(101);
1✔
3534

3535
        let utxos_opt =
1✔
3536
            btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 1, None, 101);
1✔
3537
        let uxto_set = utxos_opt.expect("Shouldn't be None at height 101!");
1✔
3538

3539
        assert_eq!(btc_controller.get_block_hash(101), uxto_set.bhh);
1✔
3540
        assert_eq!(1, uxto_set.num_utxos());
1✔
3541
        assert_eq!(5_000_000_000, uxto_set.total_available());
1✔
3542
        let utxos = uxto_set.utxos;
1✔
3543
        assert_eq!(101, utxos[0].confirmations);
1✔
3544
        assert_eq!(5_000_000_000, utxos[0].amount);
1✔
3545

3546
        btc_controller.build_next_block(1);
1✔
3547

3548
        let utxos_opt =
1✔
3549
            btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 1, None, 102);
1✔
3550
        let uxto_set = utxos_opt.expect("Shouldn't be None at height 102!");
1✔
3551

3552
        assert_eq!(btc_controller.get_block_hash(102), uxto_set.bhh);
1✔
3553
        assert_eq!(2, uxto_set.num_utxos());
1✔
3554
        assert_eq!(10_000_000_000, uxto_set.total_available());
1✔
3555
        let mut utxos = uxto_set.utxos;
1✔
3556
        utxos.sort_by(|a, b| b.confirmations.cmp(&a.confirmations));
1✔
3557
        assert_eq!(102, utxos[0].confirmations);
1✔
3558
        assert_eq!(5_000_000_000, utxos[0].amount);
1✔
3559
        assert_eq!(101, utxos[1].confirmations);
1✔
3560
        assert_eq!(5_000_000_000, utxos[1].amount);
1✔
3561
    }
1✔
3562

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

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

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

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

3580
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3581
        btc_controller.bootstrap_chain(101); // one utxo exists
1✔
3582

3583
        let too_much_required = 10_000_000_000;
1✔
3584
        let utxos = btc_controller.get_utxos(
1✔
3585
            StacksEpochId::Epoch31,
1✔
3586
            &miner_pubkey,
1✔
3587
            too_much_required,
1✔
3588
            None,
1✔
3589
            0,
3590
        );
3591
        assert!(utxos.is_none(), "None because too much required");
1✔
3592
    }
1✔
3593

3594
    #[test]
3595
    #[ignore]
3596
    fn test_get_utxos_none_due_to_filter_pubkey() {
1✔
3597
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3598
            return;
×
3599
        }
1✔
3600

3601
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3602

3603
        let mut config = utils::create_miner_config();
1✔
3604
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3605

3606
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3607
        btcd_controller
1✔
3608
            .start_bitcoind()
1✔
3609
            .expect("bitcoind should be started!");
1✔
3610

3611
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3612
        btc_controller.bootstrap_chain(101); // one utxo exists
1✔
3613

3614
        let other_pubkey = utils::create_miner2_pubkey();
1✔
3615
        let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &other_pubkey, 1, None, 0);
1✔
3616
        assert!(
1✔
3617
            utxos.is_none(),
1✔
3618
            "None because utxos for other pubkey don't exist"
3619
        );
3620
    }
1✔
3621

3622
    #[test]
3623
    #[ignore]
3624
    fn test_get_utxos_none_due_to_filter_utxo_exclusion() {
1✔
3625
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3626
            return;
×
3627
        }
1✔
3628

3629
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3630

3631
        let mut config = utils::create_miner_config();
1✔
3632
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3633

3634
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3635
        btcd_controller
1✔
3636
            .start_bitcoind()
1✔
3637
            .expect("bitcoind should be started!");
1✔
3638

3639
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3640
        btc_controller.bootstrap_chain(101); // one utxo exists
1✔
3641

3642
        let existent_utxo = btc_controller
1✔
3643
            .get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 0, None, 0)
1✔
3644
            .expect("utxo set should exist");
1✔
3645
        let utxos = btc_controller.get_utxos(
1✔
3646
            StacksEpochId::Epoch31,
1✔
3647
            &miner_pubkey,
1✔
3648
            0,
3649
            Some(existent_utxo),
1✔
3650
            0,
3651
        );
3652
        assert!(
1✔
3653
            utxos.is_none(),
1✔
3654
            "None because filtering exclude existent utxo set"
3655
        );
3656
    }
1✔
3657

3658
    #[test]
3659
    #[ignore]
3660
    fn test_tx_confirmed_from_utxo_ok() {
1✔
3661
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3662
            return;
×
3663
        }
1✔
3664

3665
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3666

3667
        let mut config = utils::create_miner_config();
1✔
3668
        config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3669

3670
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3671
        btcd_controller
1✔
3672
            .start_bitcoind()
1✔
3673
            .expect("bitcoind should be started!");
1✔
3674

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

3677
        btc_controller.bootstrap_chain(101);
1✔
3678
        let utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
3679
        assert_eq!(1, utxos.len(), "One UTXO should be confirmed!");
1✔
3680

3681
        let txid = Txid::from_bitcoin_tx_hash(&utxos[0].txid);
1✔
3682
        assert!(
1✔
3683
            btc_controller.is_transaction_confirmed(&txid),
1✔
3684
            "UTXO tx should be confirmed!"
3685
        );
3686
    }
1✔
3687

3688
    #[test]
3689
    #[ignore]
3690
    fn test_import_public_key_ok() {
1✔
3691
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3692
            return;
×
3693
        }
1✔
3694

3695
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3696

3697
        let config = utils::create_miner_config();
1✔
3698

3699
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3700
        btcd_controller
1✔
3701
            .start_bitcoind()
1✔
3702
            .expect("bitcoind should be started!");
1✔
3703

3704
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3705
        btc_controller
1✔
3706
            .ensure_test_wallet_loaded()
1✔
3707
            .expect("Test wallet should be created!");
1✔
3708

3709
        let result = btc_controller.import_public_key(&miner_pubkey);
1✔
3710
        assert!(
1✔
3711
            result.is_ok(),
1✔
3712
            "Should be ok, got err instead: {:?}",
UNCOV
3713
            result.unwrap_err()
×
3714
        );
3715
    }
1✔
3716

3717
    #[test]
3718
    #[ignore]
3719
    fn test_import_public_key_twice_ok() {
1✔
3720
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3721
            return;
×
3722
        }
1✔
3723

3724
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3725

3726
        let config = utils::create_miner_config();
1✔
3727

3728
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3729
        btcd_controller
1✔
3730
            .start_bitcoind()
1✔
3731
            .expect("bitcoind should be started!");
1✔
3732

3733
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3734
        btc_controller
1✔
3735
            .ensure_test_wallet_loaded()
1✔
3736
            .expect("Test wallet should be created!");
1✔
3737

3738
        btc_controller
1✔
3739
            .import_public_key(&miner_pubkey)
1✔
3740
            .expect("Import should be ok: first time!");
1✔
3741

3742
        //ok, but it is basically a no-op
3743
        let result = btc_controller.import_public_key(&miner_pubkey);
1✔
3744
        assert!(
1✔
3745
            result.is_ok(),
1✔
3746
            "Should be ok, got err instead: {:?}",
UNCOV
3747
            result.unwrap_err()
×
3748
        );
3749
    }
1✔
3750

3751
    #[test]
3752
    #[ignore]
3753
    fn test_import_public_key_segwit_ok() {
1✔
3754
        if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3755
            return;
×
3756
        }
1✔
3757

3758
        let miner_pubkey = utils::create_miner1_pubkey();
1✔
3759

3760
        let mut config = utils::create_miner_config();
1✔
3761
        config.miner.segwit = true;
1✔
3762

3763
        let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3764
        btcd_controller
1✔
3765
            .start_bitcoind()
1✔
3766
            .expect("bitcoind should be started!");
1✔
3767

3768
        let btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3769
        btc_controller
1✔
3770
            .ensure_test_wallet_loaded()
1✔
3771
            .expect("Test wallet should be created!");
1✔
3772

3773
        let result = btc_controller.import_public_key(&miner_pubkey);
1✔
3774
        assert!(
1✔
3775
            result.is_ok(),
1✔
3776
            "Should be ok, got err instead: {:?}",
UNCOV
3777
            result.unwrap_err()
×
3778
        );
3779
    }
1✔
3780

3781
    /// Tests related to Leader Block Commit operation
3782
    mod leader_commit_op {
3783
        use super::*;
3784

3785
        #[test]
3786
        #[ignore]
3787
        fn test_build_leader_block_commit_tx_ok_with_new_commit_op() {
1✔
3788
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3789
                return;
×
3790
            }
1✔
3791

3792
            let keychain = utils::create_keychain();
1✔
3793
            let miner_pubkey = keychain.get_pub_key();
1✔
3794
            let mut op_signer = keychain.generate_op_signer();
1✔
3795

3796
            let mut config = utils::create_miner_config();
1✔
3797
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3798
            config.burnchain.pox_reward_length = Some(11);
1✔
3799

3800
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3801
            btcd_controller
1✔
3802
                .start_bitcoind()
1✔
3803
                .expect("bitcoind should be started!");
1✔
3804

3805
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
3806
            btc_controller
1✔
3807
                .connect_dbs()
1✔
3808
                .expect("Dbs initialization required!");
1✔
3809
            btc_controller.bootstrap_chain(101); // now, one utxo exists
1✔
3810

3811
            let mut commit_op = utils::create_templated_commit_op();
1✔
3812
            commit_op.sunset_burn = 5_500;
1✔
3813
            commit_op.burn_fee = 110_000;
1✔
3814

3815
            let tx = btc_controller
1✔
3816
                .build_leader_block_commit_tx(
1✔
3817
                    StacksEpochId::Epoch31,
1✔
3818
                    commit_op.clone(),
1✔
3819
                    &mut op_signer,
1✔
3820
                )
3821
                .expect("Build leader block commit should work");
1✔
3822

3823
            assert!(op_signer.is_disposed());
1✔
3824

3825
            assert_eq!(1, tx.version);
1✔
3826
            assert_eq!(0, tx.lock_time);
1✔
3827
            assert_eq!(1, tx.input.len());
1✔
3828
            assert_eq!(4, tx.output.len());
1✔
3829

3830
            // utxos list contains the only existing utxo
3831
            let used_utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
3832
            let input_0 = utils::txin_at_index(&tx, &op_signer, &used_utxos, 0);
1✔
3833
            assert_eq!(input_0, tx.input[0]);
1✔
3834

3835
            let op_return = utils::txout_opreturn(&commit_op, &config.burnchain.magic_bytes, 5_500);
1✔
3836
            let op_commit_1 = utils::txout_opdup_commit_to(&commit_op.commit_outs[0], 55_000);
1✔
3837
            let op_commit_2 = utils::txout_opdup_commit_to(&commit_op.commit_outs[1], 55_000);
1✔
3838
            let op_change = utils::txout_opdup_change_legacy(&mut op_signer, 4_999_865_300);
1✔
3839
            assert_eq!(op_return, tx.output[0]);
1✔
3840
            assert_eq!(op_commit_1, tx.output[1]);
1✔
3841
            assert_eq!(op_commit_2, tx.output[2]);
1✔
3842
            assert_eq!(op_change, tx.output[3]);
1✔
3843
        }
1✔
3844

3845
        #[test]
3846
        #[ignore]
3847
        fn test_build_leader_block_commit_tx_fails_resub_same_commit_op_while_prev_not_confirmed() {
1✔
3848
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3849
                return;
×
3850
            }
1✔
3851

3852
            let keychain = utils::create_keychain();
1✔
3853
            let miner_pubkey = keychain.get_pub_key();
1✔
3854
            let mut op_signer = keychain.generate_op_signer();
1✔
3855

3856
            let mut config = utils::create_miner_config();
1✔
3857
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3858
            config.burnchain.pox_reward_length = Some(11);
1✔
3859

3860
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
3861
            btcd_controller
1✔
3862
                .start_bitcoind()
1✔
3863
                .expect("bitcoind should be started!");
1✔
3864

3865
            let mut btc_controller = BitcoinRegtestController::new(config, None);
1✔
3866
            btc_controller
1✔
3867
                .connect_dbs()
1✔
3868
                .expect("Dbs initialization required!");
1✔
3869
            btc_controller.bootstrap_chain(101); // now, one utxo exists
1✔
3870

3871
            let commit_op = utils::create_templated_commit_op();
1✔
3872

3873
            let _first_tx_ok = btc_controller
1✔
3874
                .build_leader_block_commit_tx(
1✔
3875
                    StacksEpochId::Epoch31,
1✔
3876
                    commit_op.clone(),
1✔
3877
                    &mut op_signer,
1✔
3878
                )
3879
                .expect("At first, building leader block commit should work");
1✔
3880

3881
            // re-submitting same commit while previous it is not confirmed by the burnchain
3882
            let resubmit = btc_controller.build_leader_block_commit_tx(
1✔
3883
                StacksEpochId::Epoch31,
1✔
3884
                commit_op,
1✔
3885
                &mut op_signer,
1✔
3886
            );
3887

3888
            assert!(resubmit.is_err());
1✔
3889
            assert_eq!(
1✔
3890
                BurnchainControllerError::IdenticalOperation,
3891
                resubmit.unwrap_err()
1✔
3892
            );
3893
        }
1✔
3894

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

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

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

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

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

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

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

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

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

3940
            assert!(resubmit.is_err());
1✔
3941
            assert_eq!(
1✔
3942
                BurnchainControllerError::IdenticalOperation,
3943
                resubmit.unwrap_err()
1✔
3944
            );
3945
        }
1✔
3946

3947
        #[test]
3948
        #[ignore]
3949
        fn test_build_leader_block_commit_tx_ok_while_prev_is_confirmed() {
1✔
3950
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
3951
                return;
×
3952
            }
1✔
3953

3954
            let keychain = utils::create_keychain();
1✔
3955
            let miner_pubkey = keychain.get_pub_key();
1✔
3956
            let mut op_signer = keychain.generate_op_signer();
1✔
3957

3958
            let mut config = utils::create_miner_config();
1✔
3959
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
3960
            config.burnchain.pox_reward_length = Some(11);
1✔
3961

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

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

3973
            let mut commit_op = utils::create_templated_commit_op();
1✔
3974
            commit_op.sunset_burn = 5_500;
1✔
3975
            commit_op.burn_fee = 110_000;
1✔
3976

3977
            let first_tx_ok = btc_controller
1✔
3978
                .build_leader_block_commit_tx(
1✔
3979
                    StacksEpochId::Epoch31,
1✔
3980
                    commit_op.clone(),
1✔
3981
                    &mut op_signer,
1✔
3982
                )
3983
                .expect("At first, building leader block commit should work");
1✔
3984

3985
            let first_txid = first_tx_ok.txid();
1✔
3986

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

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

3995
            let new_tx = btc_controller
1✔
3996
                .build_leader_block_commit_tx(
1✔
3997
                    StacksEpochId::Epoch31,
1✔
3998
                    commit_op.clone(),
1✔
3999
                    &mut signer,
1✔
4000
                )
4001
                .expect("Commit tx should be created!");
1✔
4002

4003
            assert!(op_signer.is_disposed());
1✔
4004

4005
            assert_eq!(1, new_tx.version);
1✔
4006
            assert_eq!(0, new_tx.lock_time);
1✔
4007
            assert_eq!(1, new_tx.input.len());
1✔
4008
            assert_eq!(4, new_tx.output.len());
1✔
4009

4010
            // utxos list contains the sole utxo used by prev commit operation
4011
            // because has enough amount to cover the new commit
4012
            let used_utxos: Vec<UTXO> = btc_controller
1✔
4013
                .get_all_utxos(&miner_pubkey)
1✔
4014
                .into_iter()
1✔
4015
                .filter(|utxo| utxo.txid == first_txid)
2✔
4016
                .collect();
1✔
4017

4018
            let input_0 = utils::txin_at_index(&new_tx, &op_signer, &used_utxos, 0);
1✔
4019
            assert_eq!(input_0, new_tx.input[0]);
1✔
4020

4021
            let op_return = utils::txout_opreturn(&commit_op, &config.burnchain.magic_bytes, 5_500);
1✔
4022
            let op_commit_1 = utils::txout_opdup_commit_to(&commit_op.commit_outs[0], 55_005);
1✔
4023
            let op_commit_2 = utils::txout_opdup_commit_to(&commit_op.commit_outs[1], 55_005);
1✔
4024
            let op_change = utils::txout_opdup_change_legacy(&mut signer, 4_999_730_590);
1✔
4025
            assert_eq!(op_return, new_tx.output[0]);
1✔
4026
            assert_eq!(op_commit_1, new_tx.output[1]);
1✔
4027
            assert_eq!(op_commit_2, new_tx.output[2]);
1✔
4028
            assert_eq!(op_change, new_tx.output[3]);
1✔
4029
        }
1✔
4030

4031
        #[test]
4032
        #[ignore]
4033
        fn test_build_leader_block_commit_tx_ok_rbf_while_prev_not_confirmed() {
1✔
4034
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
4035
                return;
×
4036
            }
1✔
4037

4038
            let keychain = utils::create_keychain();
1✔
4039
            let miner_pubkey = keychain.get_pub_key();
1✔
4040
            let mut op_signer = keychain.generate_op_signer();
1✔
4041

4042
            let mut config = utils::create_miner_config();
1✔
4043
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4044
            config.burnchain.pox_reward_length = Some(11);
1✔
4045

4046
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4047
            btcd_controller
1✔
4048
                .start_bitcoind()
1✔
4049
                .expect("bitcoind should be started!");
1✔
4050

4051
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4052
            btc_controller
1✔
4053
                .connect_dbs()
1✔
4054
                .expect("Dbs initialization required!");
1✔
4055
            btc_controller.bootstrap_chain(101); // Now, one utxo exists
1✔
4056

4057
            let mut commit_op = utils::create_templated_commit_op();
1✔
4058
            commit_op.sunset_burn = 5_500;
1✔
4059
            commit_op.burn_fee = 110_000;
1✔
4060

4061
            let _first_tx_ok = btc_controller
1✔
4062
                .build_leader_block_commit_tx(
1✔
4063
                    StacksEpochId::Epoch31,
1✔
4064
                    commit_op.clone(),
1✔
4065
                    &mut op_signer,
1✔
4066
                )
4067
                .expect("At first, building leader block commit should work");
1✔
4068

4069
            //re-gen signer othewise fails because it will be disposed during previous commit tx.
4070
            let mut signer = keychain.generate_op_signer();
1✔
4071
            //small change to the commit op payload
4072
            commit_op.burn_fee += 10;
1✔
4073

4074
            let rbf_tx = btc_controller
1✔
4075
                .build_leader_block_commit_tx(
1✔
4076
                    StacksEpochId::Epoch31,
1✔
4077
                    commit_op.clone(),
1✔
4078
                    &mut signer,
1✔
4079
                )
4080
                .expect("Commit tx should be rbf-ed");
1✔
4081

4082
            assert!(op_signer.is_disposed());
1✔
4083

4084
            assert_eq!(1, rbf_tx.version);
1✔
4085
            assert_eq!(0, rbf_tx.lock_time);
1✔
4086
            assert_eq!(1, rbf_tx.input.len());
1✔
4087
            assert_eq!(4, rbf_tx.output.len());
1✔
4088

4089
            // utxos list contains the only existing utxo
4090
            let used_utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
4091

4092
            let input_0 = utils::txin_at_index(&rbf_tx, &op_signer, &used_utxos, 0);
1✔
4093
            assert_eq!(input_0, rbf_tx.input[0]);
1✔
4094

4095
            let op_return = utils::txout_opreturn(&commit_op, &config.burnchain.magic_bytes, 5_500);
1✔
4096
            let op_commit_1 = utils::txout_opdup_commit_to(&commit_op.commit_outs[0], 55_005);
1✔
4097
            let op_commit_2 = utils::txout_opdup_commit_to(&commit_op.commit_outs[1], 55_005);
1✔
4098
            let op_change = utils::txout_opdup_change_legacy(&mut signer, 4_999_862_985);
1✔
4099
            assert_eq!(op_return, rbf_tx.output[0]);
1✔
4100
            assert_eq!(op_commit_1, rbf_tx.output[1]);
1✔
4101
            assert_eq!(op_commit_2, rbf_tx.output[2]);
1✔
4102
            assert_eq!(op_change, rbf_tx.output[3]);
1✔
4103
        }
1✔
4104

4105
        #[test]
4106
        #[ignore]
4107
        fn test_make_operation_leader_block_commit_tx_ok() {
1✔
4108
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
4109
                return;
×
4110
            }
1✔
4111

4112
            let keychain = utils::create_keychain();
1✔
4113
            let miner_pubkey = keychain.get_pub_key();
1✔
4114
            let mut op_signer = keychain.generate_op_signer();
1✔
4115

4116
            let mut config = utils::create_miner_config();
1✔
4117
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4118
            config.burnchain.pox_reward_length = Some(11);
1✔
4119

4120
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4121
            btcd_controller
1✔
4122
                .start_bitcoind()
1✔
4123
                .expect("bitcoind should be started!");
1✔
4124

4125
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4126
            btc_controller
1✔
4127
                .connect_dbs()
1✔
4128
                .expect("Dbs initialization required!");
1✔
4129
            btc_controller.bootstrap_chain(101); // now, one utxo exists
1✔
4130

4131
            let mut commit_op = utils::create_templated_commit_op();
1✔
4132
            commit_op.sunset_burn = 5_500;
1✔
4133
            commit_op.burn_fee = 110_000;
1✔
4134

4135
            let tx = btc_controller
1✔
4136
                .make_operation_tx(
1✔
4137
                    StacksEpochId::Epoch31,
1✔
4138
                    BlockstackOperationType::LeaderBlockCommit(commit_op),
1✔
4139
                    &mut op_signer,
1✔
4140
                )
4141
                .expect("Make op should work");
1✔
4142

4143
            assert!(op_signer.is_disposed());
1✔
4144

4145
            assert_eq!(
1✔
4146
                "1a74106bd760117892fbd90fca11646b4de46f99fd2b065c9e0706cfdcea0336",
4147
                tx.txid().to_string()
1✔
4148
            );
4149
        }
1✔
4150

4151
        #[test]
4152
        #[ignore]
4153
        fn test_submit_leader_block_commit_tx_ok() {
1✔
4154
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
4155
                return;
×
4156
            }
1✔
4157

4158
            let keychain = utils::create_keychain();
1✔
4159
            let miner_pubkey = keychain.get_pub_key();
1✔
4160
            let mut op_signer = keychain.generate_op_signer();
1✔
4161

4162
            let mut config = utils::create_miner_config();
1✔
4163
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4164
            config.burnchain.pox_reward_length = Some(11);
1✔
4165

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

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

4177
            let mut commit_op = utils::create_templated_commit_op();
1✔
4178
            commit_op.sunset_burn = 5_500;
1✔
4179
            commit_op.burn_fee = 110_000;
1✔
4180

4181
            let tx_id = btc_controller
1✔
4182
                .submit_operation(
1✔
4183
                    StacksEpochId::Epoch31,
1✔
4184
                    BlockstackOperationType::LeaderBlockCommit(commit_op),
1✔
4185
                    &mut op_signer,
1✔
4186
                )
4187
                .expect("Submit op should work");
1✔
4188

4189
            assert!(op_signer.is_disposed());
1✔
4190

4191
            assert_eq!(
1✔
4192
                "1a74106bd760117892fbd90fca11646b4de46f99fd2b065c9e0706cfdcea0336",
4193
                tx_id.to_hex()
1✔
4194
            );
4195
        }
1✔
4196

4197
        #[test]
4198
        #[ignore]
4199
        fn test_submit_operation_block_commit_clears_ongoing_on_send_failure() {
1✔
4200
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
4201
                return;
×
4202
            }
1✔
4203

4204
            let keychain = utils::create_keychain();
1✔
4205
            let miner_pubkey = keychain.get_pub_key();
1✔
4206
            let mut op_signer = keychain.generate_op_signer();
1✔
4207

4208
            let mut config = utils::create_miner_config();
1✔
4209
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4210
            config.burnchain.pox_reward_length = Some(11);
1✔
4211

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

4217
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4218
            btc_controller
1✔
4219
                .connect_dbs()
1✔
4220
                .expect("Dbs initialization required!");
1✔
4221
            btc_controller.bootstrap_chain(101);
1✔
4222

4223
            let mut commit_op = utils::create_templated_commit_op();
1✔
4224
            commit_op.sunset_burn = 5_500;
1✔
4225
            commit_op.burn_fee = 110_000;
1✔
4226

4227
            // First submit succeeds and sets ongoing_block_commit
4228
            btc_controller
1✔
4229
                .submit_operation(
1✔
4230
                    StacksEpochId::Epoch31,
1✔
4231
                    BlockstackOperationType::LeaderBlockCommit(commit_op.clone()),
1✔
4232
                    &mut op_signer,
1✔
4233
                )
4234
                .expect("First submit should succeed");
1✔
4235

4236
            assert!(
1✔
4237
                btc_controller.get_ongoing_commit().is_some(),
1✔
4238
                "ongoing_block_commit should be set after successful submit"
4239
            );
4240

4241
            // Corrupt the cached UTXOs so the RBF transaction references
4242
            // non-existent inputs, causing send_transaction to be rejected
4243
            // by bitcoind immediately.
4244
            let mut ongoing = btc_controller.get_ongoing_commit().unwrap();
1✔
4245
            for utxo in ongoing.utxos.utxos.iter_mut() {
1✔
4246
                utxo.txid = Sha256dHash::default();
1✔
4247
            }
1✔
4248
            btc_controller.set_ongoing_commit(Some(ongoing));
1✔
4249

4250
            // Second submit with different payload triggers the RBF path.
4251
            // make_operation_tx builds the tx using the corrupted UTXOs,
4252
            // then send_transaction fails because the inputs don't exist.
4253
            let mut op_signer = keychain.generate_op_signer();
1✔
4254
            commit_op.burn_fee += 10;
1✔
4255

4256
            let err = btc_controller
1✔
4257
                .submit_operation(
1✔
4258
                    StacksEpochId::Epoch31,
1✔
4259
                    BlockstackOperationType::LeaderBlockCommit(commit_op),
1✔
4260
                    &mut op_signer,
1✔
4261
                )
4262
                .unwrap_err();
1✔
4263

4264
            assert!(
1✔
UNCOV
4265
                matches!(
×
4266
                    err,
1✔
4267
                    BurnchainControllerError::TransactionSubmissionFailed(_)
4268
                ),
4269
                "Error should be TransactionSubmissionFailed, but was {err}"
4270
            );
4271
            assert!(
1✔
4272
                btc_controller.get_ongoing_commit().is_none(),
1✔
4273
                "ongoing_block_commit should be cleared after send failure"
4274
            );
4275
        }
1✔
4276
    }
4277

4278
    /// Tests related to Leader Key Register operation
4279
    mod leader_key_op {
4280
        use super::*;
4281

4282
        #[test]
4283
        #[ignore]
4284
        fn test_build_leader_key_tx_ok() {
1✔
4285
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
4286
                return;
×
4287
            }
1✔
4288

4289
            let keychain = utils::create_keychain();
1✔
4290
            let miner_pubkey = keychain.get_pub_key();
1✔
4291
            let mut op_signer = keychain.generate_op_signer();
1✔
4292

4293
            let mut config = utils::create_miner_config();
1✔
4294
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4295

4296
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4297
            btcd_controller
1✔
4298
                .start_bitcoind()
1✔
4299
                .expect("bitcoind should be started!");
1✔
4300

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

4304
            let leader_key_op = utils::create_templated_leader_key_op();
1✔
4305

4306
            let tx = btc_controller
1✔
4307
                .build_leader_key_register_tx(
1✔
4308
                    StacksEpochId::Epoch31,
1✔
4309
                    leader_key_op.clone(),
1✔
4310
                    &mut op_signer,
1✔
4311
                )
4312
                .expect("Build leader key should work");
1✔
4313

4314
            assert!(op_signer.is_disposed());
1✔
4315

4316
            assert_eq!(1, tx.version);
1✔
4317
            assert_eq!(0, tx.lock_time);
1✔
4318
            assert_eq!(1, tx.input.len());
1✔
4319
            assert_eq!(2, tx.output.len());
1✔
4320

4321
            // utxos list contains the only existing utxo
4322
            let used_utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
4323
            let input_0 = utils::txin_at_index(&tx, &op_signer, &used_utxos, 0);
1✔
4324
            assert_eq!(input_0, tx.input[0]);
1✔
4325

4326
            let op_return = utils::txout_opreturn(&leader_key_op, &config.burnchain.magic_bytes, 0);
1✔
4327
            let op_change = utils::txout_opdup_change_legacy(&mut op_signer, 4_999_980_000);
1✔
4328
            assert_eq!(op_return, tx.output[0]);
1✔
4329
            assert_eq!(op_change, tx.output[1]);
1✔
4330
        }
1✔
4331

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

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

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

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

4351
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4352
            btc_controller.bootstrap_chain(100); // no utxos exist
1✔
4353

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

4356
            let error = btc_controller
1✔
4357
                .build_leader_key_register_tx(
1✔
4358
                    StacksEpochId::Epoch31,
1✔
4359
                    leader_key_op.clone(),
1✔
4360
                    &mut op_signer,
1✔
4361
                )
4362
                .expect_err("Leader key build should fail!");
1✔
4363

4364
            assert!(!op_signer.is_disposed());
1✔
4365
            assert_eq!(BurnchainControllerError::NoUTXOs, error);
1✔
4366
        }
1✔
4367

4368
        #[test]
4369
        #[ignore]
4370
        fn test_make_operation_leader_key_register_tx_ok() {
1✔
4371
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
4372
                return;
×
4373
            }
1✔
4374

4375
            let keychain = utils::create_keychain();
1✔
4376
            let miner_pubkey = keychain.get_pub_key();
1✔
4377
            let mut op_signer = keychain.generate_op_signer();
1✔
4378

4379
            let mut config = utils::create_miner_config();
1✔
4380
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4381
            config.burnchain.pox_reward_length = Some(11);
1✔
4382

4383
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4384
            btcd_controller
1✔
4385
                .start_bitcoind()
1✔
4386
                .expect("bitcoind should be started!");
1✔
4387

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

4391
            let leader_key_op = utils::create_templated_leader_key_op();
1✔
4392

4393
            let tx = btc_controller
1✔
4394
                .make_operation_tx(
1✔
4395
                    StacksEpochId::Epoch31,
1✔
4396
                    BlockstackOperationType::LeaderKeyRegister(leader_key_op),
1✔
4397
                    &mut op_signer,
1✔
4398
                )
4399
                .expect("Make op should work");
1✔
4400

4401
            assert!(op_signer.is_disposed());
1✔
4402

4403
            assert_eq!(
1✔
4404
                "4ecd7ba71bebd1aaed49dd63747ee424473f1c571bb9a576361607a669191024",
4405
                tx.txid().to_string()
1✔
4406
            );
4407
        }
1✔
4408

4409
        #[test]
4410
        #[ignore]
4411
        fn test_submit_operation_leader_key_register_tx_ok() {
1✔
4412
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
4413
                return;
×
4414
            }
1✔
4415

4416
            let keychain = utils::create_keychain();
1✔
4417
            let miner_pubkey = keychain.get_pub_key();
1✔
4418
            let mut op_signer = keychain.generate_op_signer();
1✔
4419

4420
            let mut config = utils::create_miner_config();
1✔
4421
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4422

4423
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4424
            btcd_controller
1✔
4425
                .start_bitcoind()
1✔
4426
                .expect("bitcoind should be started!");
1✔
4427

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

4431
            let leader_key_op = utils::create_templated_leader_key_op();
1✔
4432

4433
            let tx_id = btc_controller
1✔
4434
                .submit_operation(
1✔
4435
                    StacksEpochId::Epoch31,
1✔
4436
                    BlockstackOperationType::LeaderKeyRegister(leader_key_op),
1✔
4437
                    &mut op_signer,
1✔
4438
                )
4439
                .expect("Submit op should work");
1✔
4440

4441
            assert!(op_signer.is_disposed());
1✔
4442

4443
            assert_eq!(
1✔
4444
                "4ecd7ba71bebd1aaed49dd63747ee424473f1c571bb9a576361607a669191024",
4445
                tx_id.to_hex()
1✔
4446
            );
4447
        }
1✔
4448
    }
4449

4450
    /// Tests related to Pre Stacks operation
4451
    mod pre_stx_op {
4452
        use super::*;
4453

4454
        #[test]
4455
        #[ignore]
4456
        fn test_build_pre_stx_tx_ok() {
1✔
4457
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
4458
                return;
×
4459
            }
1✔
4460

4461
            let keychain = utils::create_keychain();
1✔
4462
            let miner_pubkey = keychain.get_pub_key();
1✔
4463
            let mut op_signer = keychain.generate_op_signer();
1✔
4464

4465
            let mut config = utils::create_miner_config();
1✔
4466
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4467

4468
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4469
            btcd_controller
1✔
4470
                .start_bitcoind()
1✔
4471
                .expect("bitcoind should be started!");
1✔
4472

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

4476
            let mut pre_stx_op = utils::create_templated_pre_stx_op();
1✔
4477
            pre_stx_op.output = keychain.get_address(false);
1✔
4478

4479
            let tx = btc_controller
1✔
4480
                .build_pre_stacks_tx(StacksEpochId::Epoch31, pre_stx_op.clone(), &mut op_signer)
1✔
4481
                .expect("Build leader key should work");
1✔
4482

4483
            assert!(op_signer.is_disposed());
1✔
4484

4485
            assert_eq!(1, tx.version);
1✔
4486
            assert_eq!(0, tx.lock_time);
1✔
4487
            assert_eq!(1, tx.input.len());
1✔
4488
            assert_eq!(3, tx.output.len());
1✔
4489

4490
            // utxos list contains the only existing utxo
4491
            let used_utxos = btc_controller.get_all_utxos(&miner_pubkey);
1✔
4492
            let input_0 = utils::txin_at_index(&tx, &op_signer, &used_utxos, 0);
1✔
4493
            assert_eq!(input_0, tx.input[0]);
1✔
4494

4495
            let op_return = utils::txout_opreturn(&pre_stx_op, &config.burnchain.magic_bytes, 0);
1✔
4496
            let op_change = utils::txout_opdup_change_legacy(&mut op_signer, 24_500);
1✔
4497
            assert_eq!(op_return, tx.output[0]);
1✔
4498
            assert_eq!(op_change, tx.output[1]);
1✔
4499
        }
1✔
4500

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

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

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

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

4520
            let mut btc_controller = BitcoinRegtestController::new(config.clone(), None);
1✔
4521
            btc_controller.bootstrap_chain(100); // no utxo exists
1✔
4522

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

4526
            let error = btc_controller
1✔
4527
                .build_pre_stacks_tx(StacksEpochId::Epoch31, pre_stx_op.clone(), &mut op_signer)
1✔
4528
                .expect_err("Leader key build should fail!");
1✔
4529

4530
            assert!(!op_signer.is_disposed());
1✔
4531
            assert_eq!(BurnchainControllerError::NoUTXOs, error);
1✔
4532
        }
1✔
4533

4534
        #[test]
4535
        #[ignore]
4536
        fn test_make_operation_pre_stx_tx_ok() {
1✔
4537
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
4538
                return;
×
4539
            }
1✔
4540

4541
            let keychain = utils::create_keychain();
1✔
4542
            let miner_pubkey = keychain.get_pub_key();
1✔
4543
            let mut op_signer = keychain.generate_op_signer();
1✔
4544

4545
            let mut config = utils::create_miner_config();
1✔
4546
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4547

4548
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4549
            btcd_controller
1✔
4550
                .start_bitcoind()
1✔
4551
                .expect("bitcoind should be started!");
1✔
4552

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

4556
            let mut pre_stx_op = utils::create_templated_pre_stx_op();
1✔
4557
            pre_stx_op.output = keychain.get_address(false);
1✔
4558

4559
            let tx = btc_controller
1✔
4560
                .make_operation_tx(
1✔
4561
                    StacksEpochId::Epoch31,
1✔
4562
                    BlockstackOperationType::PreStx(pre_stx_op),
1✔
4563
                    &mut op_signer,
1✔
4564
                )
4565
                .expect("Make op should work");
1✔
4566

4567
            assert!(op_signer.is_disposed());
1✔
4568

4569
            assert_eq!(
1✔
4570
                "2d061c42c6f13a62fd9d80dc9fdcd19bdb4f9e4a07f786e42530c64c52ed9d1d",
4571
                tx.txid().to_string()
1✔
4572
            );
4573
        }
1✔
4574

4575
        #[test]
4576
        #[ignore]
4577
        fn test_submit_operation_pre_stx_tx_ok() {
1✔
4578
            if env::var("BITCOIND_TEST") != Ok("1".into()) {
1✔
UNCOV
4579
                return;
×
4580
            }
1✔
4581

4582
            let keychain = utils::create_keychain();
1✔
4583
            let miner_pubkey = keychain.get_pub_key();
1✔
4584
            let mut op_signer = keychain.generate_op_signer();
1✔
4585

4586
            let mut config = utils::create_miner_config();
1✔
4587
            config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex());
1✔
4588

4589
            let mut btcd_controller = BitcoinCoreController::from_stx_config(&config);
1✔
4590
            btcd_controller
1✔
4591
                .start_bitcoind()
1✔
4592
                .expect("bitcoind should be started!");
1✔
4593

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

4597
            let mut pre_stx_op = utils::create_templated_pre_stx_op();
1✔
4598
            pre_stx_op.output = keychain.get_address(false);
1✔
4599

4600
            let tx_id = btc_controller
1✔
4601
                .submit_operation(
1✔
4602
                    StacksEpochId::Epoch31,
1✔
4603
                    BlockstackOperationType::PreStx(pre_stx_op),
1✔
4604
                    &mut op_signer,
1✔
4605
                )
4606
                .expect("submit op should work");
1✔
4607

4608
            assert!(op_signer.is_disposed());
1✔
4609

4610
            assert_eq!(
1✔
4611
                "2d061c42c6f13a62fd9d80dc9fdcd19bdb4f9e4a07f786e42530c64c52ed9d1d",
4612
                tx_id.to_hex()
1✔
4613
            );
4614
        }
1✔
4615
    }
4616
}
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