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

Neptune-Crypto / neptune-core / 15261546953

26 May 2025 04:35PM UTC coverage: 71.839% (+0.1%) from 71.731%
15261546953

push

github

jan-ferdinand
ci: Fix coverage workflow

Due to a regression (?) in the rustc compiler, the code coverage
workflow has been failing since 2025-05-20. Temporarily use a version of
nightly that is known to work, until the underlying issue has been
resolved.

20196 of 28113 relevant lines covered (71.84%)

405411.65 hits per line

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

76.0
/src/mine_loop.rs
1
pub(crate) mod composer_parameters;
2
use std::cmp::max;
3
use std::sync::Arc;
4
use std::time::Duration;
5

6
use anyhow::bail;
7
use anyhow::Result;
8
use block_header::BlockHeader;
9
use block_header::BlockHeaderField;
10
use composer_parameters::ComposerParameters;
11
use futures::channel::oneshot;
12
use num_traits::CheckedSub;
13
use num_traits::Zero;
14
use primitive_witness::PrimitiveWitness;
15
use rand::rngs::StdRng;
16
use rand::Rng;
17
use rand::SeedableRng;
18
use rayon::iter::ParallelIterator;
19
use rayon::ThreadPoolBuilder;
20
use tasm_lib::prelude::Tip5;
21
use tasm_lib::triton_vm::prelude::BFieldCodec;
22
use tokio::select;
23
use tokio::sync::mpsc;
24
use tokio::task::JoinHandle;
25
use tokio::time;
26
use tokio::time::sleep;
27
use tracing::*;
28
use twenty_first::math::digest::Digest;
29

30
use crate::api::export::TxInputList;
31
use crate::api::tx_initiation::builder::transaction_builder::TransactionBuilder;
32
use crate::api::tx_initiation::builder::transaction_proof_builder::TransactionProofBuilder;
33
use crate::api::tx_initiation::builder::triton_vm_proof_job_options_builder::TritonVmProofJobOptionsBuilder;
34
use crate::api::tx_initiation::error::CreateProofError;
35
use crate::config_models::network::Network;
36
use crate::job_queue::errors::JobHandleError;
37
use crate::models::blockchain::block::block_height::BlockHeight;
38
use crate::models::blockchain::block::block_kernel::BlockKernel;
39
use crate::models::blockchain::block::block_kernel::BlockKernelField;
40
use crate::models::blockchain::block::difficulty_control::difficulty_control;
41
use crate::models::blockchain::block::*;
42
use crate::models::blockchain::transaction::transaction_proof::TransactionProofType;
43
use crate::models::blockchain::transaction::*;
44
use crate::models::blockchain::type_scripts::native_currency_amount::NativeCurrencyAmount;
45
use crate::models::channel::*;
46
use crate::models::proof_abstractions::mast_hash::MastHash;
47
use crate::models::proof_abstractions::tasm::program::TritonVmProofJobOptions;
48
use crate::models::proof_abstractions::timestamp::Timestamp;
49
use crate::models::shared::MAX_NUM_TXS_TO_MERGE;
50
use crate::models::shared::SIZE_20MB_IN_BYTES;
51
use crate::models::state::mining_status::MiningStatus;
52
use crate::models::state::transaction_details::TransactionDetails;
53
use crate::models::state::wallet::address::hash_lock_key::HashLockKey;
54
use crate::models::state::wallet::expected_utxo::ExpectedUtxo;
55
use crate::models::state::wallet::transaction_output::TxOutput;
56
use crate::models::state::wallet::transaction_output::TxOutputList;
57
use crate::models::state::GlobalStateLock;
58
use crate::prelude::twenty_first;
59
use crate::triton_vm_job_queue::vm_job_queue;
60
use crate::triton_vm_job_queue::TritonVmJobPriority;
61
use crate::triton_vm_job_queue::TritonVmJobQueue;
62
use crate::COMPOSITION_FAILED_EXIT_CODE;
63

64
/// Information related to the resources to be used for guessing.
65
#[derive(Debug, Clone, Copy)]
66
pub(crate) struct GuessingConfiguration {
67
    pub(crate) sleepy_guessing: bool,
68
    pub(crate) num_guesser_threads: Option<usize>,
69
}
70

71
async fn compose_block(
4✔
72
    latest_block: Block,
4✔
73
    global_state_lock: GlobalStateLock,
4✔
74
    sender: oneshot::Sender<(Block, Vec<ExpectedUtxo>)>,
4✔
75
    cancel_compose_rx: tokio::sync::watch::Receiver<()>,
4✔
76
    now: Timestamp,
4✔
77
) -> Result<()> {
4✔
78
    let timestamp = max(
4✔
79
        now,
4✔
80
        latest_block.header().timestamp + global_state_lock.cli().network.minimum_block_time(),
4✔
81
    );
82

83
    let mut job_options = global_state_lock
4✔
84
        .cli()
4✔
85
        .proof_job_options(TritonVmJobPriority::High);
4✔
86
    job_options.cancel_job_rx = Some(cancel_compose_rx);
4✔
87

88
    let (transaction, composer_utxos) = create_block_transaction(
4✔
89
        &latest_block,
4✔
90
        &global_state_lock,
4✔
91
        timestamp,
4✔
92
        job_options.clone(),
4✔
93
    )
4✔
94
    .await?;
4✔
95

96
    let compose_result = Block::compose(
2✔
97
        &latest_block,
2✔
98
        transaction,
2✔
99
        timestamp,
2✔
100
        vm_job_queue(),
2✔
101
        job_options,
2✔
102
    )
2✔
103
    .await;
2✔
104

105
    let proposal = match compose_result {
2✔
106
        Ok(template) => template,
2✔
107
        Err(e) => bail!("Miner failed to generate block template. {}", e.to_string()),
×
108
    };
109

110
    // Please clap.
111
    match sender.send((proposal, composer_utxos)) {
2✔
112
        Ok(_) => Ok(()),
2✔
113
        Err(_) => bail!("Composer task failed to send to miner master"),
×
114
    }
115
}
4✔
116

117
/// Attempt to mine a valid block for the network.
118
pub(crate) async fn guess_nonce(
1✔
119
    network: Network,
1✔
120
    block: Block,
1✔
121
    previous_block_header: BlockHeader,
1✔
122
    sender: oneshot::Sender<NewBlockFound>,
1✔
123
    guesser_key: HashLockKey,
1✔
124
    guessing_configuration: GuessingConfiguration,
1✔
125
) {
1✔
126
    // We wrap mining loop with spawn_blocking() because it is a
127
    // very lengthy and CPU intensive task, which should execute
128
    // on its own thread(s).
129
    //
130
    // Instead of spawn_blocking(), we could start a native OS
131
    // thread which avoids using one from tokio's threadpool
132
    // but that doesn't seem a concern for neptune-core.
133
    // Also we would need to use a oneshot channel to avoid
134
    // blocking while joining the thread.
135
    // see: https://ryhl.io/blog/async-what-is-blocking/
136
    //
137
    // note: there is no async code inside the mining loop.
138
    tokio::task::spawn_blocking(move || {
1✔
139
        guess_worker(
1✔
140
            network,
1✔
141
            block,
1✔
142
            previous_block_header,
1✔
143
            sender,
1✔
144
            guesser_key,
1✔
145
            guessing_configuration,
1✔
146
            Timestamp::now(),
1✔
147
            #[cfg(test)]
148
            None,
1✔
149
        )
150
    })
1✔
151
    .await
1✔
152
    .unwrap()
1✔
153
}
1✔
154

155
/// Return MAST nodes from which the block header MAST hash is calculated,
156
/// given a variable nonce.
157
fn precalculate_header_ap(
41✔
158
    block_header_template: &BlockHeader,
41✔
159
) -> [Digest; BlockHeader::MAST_HEIGHT] {
41✔
160
    let header_mt = block_header_template.merkle_tree();
41✔
161

162
    header_mt
41✔
163
        .authentication_structure(&[BlockHeaderField::Nonce as usize])
41✔
164
        .unwrap()
41✔
165
        .try_into()
41✔
166
        .unwrap()
41✔
167
}
41✔
168

169
/// Return MAST nodes from which the block kernel MAST hash is calculated,
170
/// given a variable header.
171
fn precalculate_kernel_ap(block_kernel: &BlockKernel) -> [Digest; BlockKernel::MAST_HEIGHT] {
41✔
172
    let block_mt = block_kernel.merkle_tree();
41✔
173

174
    block_mt
41✔
175
        .authentication_structure(&[BlockKernelField::Header as usize])
41✔
176
        .unwrap()
41✔
177
        .try_into()
41✔
178
        .unwrap()
41✔
179
}
41✔
180

181
/// Return MAST nodes from which the block hash is calculated, given a
182
/// variable block header with a variable block nonce.
183
///
184
/// Returns those MAST nodes that can be precalculated prior to PoW-guessing.
185
/// This vastly reduces the amount of hashing needed for each PoW-guess.
186
pub(crate) fn precalculate_block_auth_paths(
41✔
187
    block_template: &Block,
41✔
188
) -> (
41✔
189
    [Digest; BlockKernel::MAST_HEIGHT],
41✔
190
    [Digest; BlockHeader::MAST_HEIGHT],
41✔
191
) {
41✔
192
    let header_ap = precalculate_header_ap(block_template.header());
41✔
193
    let kernel_ap = precalculate_kernel_ap(&block_template.kernel);
41✔
194

195
    (kernel_ap, header_ap)
41✔
196
}
41✔
197

198
/// Guess the nonce in parallel until success.
199
#[cfg_attr(test, expect(clippy::too_many_arguments))]
200
fn guess_worker(
23✔
201
    network: Network,
23✔
202
    mut block: Block,
23✔
203
    previous_block_header: BlockHeader,
23✔
204
    sender: oneshot::Sender<NewBlockFound>,
23✔
205
    guesser_key: HashLockKey,
23✔
206
    guessing_configuration: GuessingConfiguration,
23✔
207
    now: Timestamp,
23✔
208
    #[cfg(test)] target_block_interval: Option<Timestamp>,
23✔
209
) {
23✔
210
    #[cfg(test)]
211
    let target_block_interval = target_block_interval.unwrap_or(network.target_block_interval());
23✔
212
    #[cfg(not(test))]
213
    let target_block_interval = network.target_block_interval();
×
214

215
    let GuessingConfiguration {
216
        sleepy_guessing,
23✔
217
        num_guesser_threads,
23✔
218
    } = guessing_configuration;
23✔
219

220
    // Following code must match the rules in `[Block::has_proof_of_work]`.
221

222
    // a difficulty reset (to min difficulty) occurs on testnet(s)
223
    // when the elapsed time between two blocks is greater than a
224
    // max interval, defined by the network.  It never occurs for
225
    // mainnet.
226
    let should_reset_difficulty =
23✔
227
        Block::should_reset_difficulty(network, now, previous_block_header.timestamp);
23✔
228

229
    info!(
23✔
230
        "prev block height: {}, prev block time: {}, now: {}",
×
231
        previous_block_header.height, previous_block_header.timestamp, now
232
    );
233

234
    let prev_difficulty = previous_block_header.difficulty;
23✔
235
    let new_difficulty = if should_reset_difficulty {
23✔
236
        let new_difficulty = network.genesis_difficulty();
4✔
237
        info!(
4✔
238
            "resetting difficulty to genesis: {}. {} seconds elapsed since previous block",
×
239
            new_difficulty,
240
            (now - previous_block_header.timestamp).to_millis() / 1000
×
241
        );
242
        new_difficulty
4✔
243
    } else {
244
        difficulty_control(
19✔
245
            now,
19✔
246
            previous_block_header.timestamp,
19✔
247
            previous_block_header.difficulty,
19✔
248
            target_block_interval,
19✔
249
            previous_block_header.height,
19✔
250
        )
251
    };
252

253
    let threshold = prev_difficulty.target();
23✔
254
    let threads_to_use = num_guesser_threads.unwrap_or_else(rayon::current_num_threads);
23✔
255
    info!(
23✔
256
        "Guessing with {} threads on block {} with {} outputs and difficulty {}. Target: {}",
×
257
        threads_to_use,
258
        block.header().height,
×
259
        block.body().transaction_kernel.outputs.len(),
×
260
        previous_block_header.difficulty,
261
        threshold.to_hex()
×
262
    );
263

264
    // note: this article discusses rayon strategies for mining.
265
    // https://www.innoq.com/en/blog/2018/06/blockchain-mining-embarrassingly-parallel/
266
    //
267
    // note: number of rayon threads can be set with env var RAYON_NUM_THREADS
268
    // see:  https://docs.rs/rayon/latest/rayon/fn.max_num_threads.html
269
    block.set_header_timestamp_and_difficulty(now, new_difficulty);
23✔
270

271
    block.set_header_guesser_digest(guesser_key.after_image());
23✔
272

273
    let (kernel_auth_path, header_auth_path) = precalculate_block_auth_paths(&block);
23✔
274

275
    let pool = ThreadPoolBuilder::new()
23✔
276
        .num_threads(threads_to_use)
23✔
277
        .build()
23✔
278
        .unwrap();
23✔
279
    let guess_result = pool.install(|| {
23✔
280
        rayon::iter::repeat(0)
23✔
281
            .map_init(rand::rng, |rng, _i| {
17,830,600✔
282
                guess_nonce_iteration(
17,830,600✔
283
                    kernel_auth_path,
17,830,600✔
284
                    threshold,
17,830,600✔
285
                    sleepy_guessing,
17,830,600✔
286
                    rng,
17,830,600✔
287
                    header_auth_path,
17,830,600✔
288
                    &sender,
17,830,600✔
289
                )
290
            })
17,830,600✔
291
            .find_any(|r| !r.block_not_found())
17,830,533✔
292
            .unwrap()
23✔
293
    });
23✔
294

295
    let nonce = match guess_result {
23✔
296
        GuessNonceResult::Cancelled => {
297
            info!("Restarting guessing task",);
×
298
            return;
×
299
        }
300
        GuessNonceResult::NonceFound { nonce } => nonce,
23✔
301
        GuessNonceResult::BlockNotFound => unreachable!(),
×
302
    };
303

304
    info!("Found valid block with nonce ({nonce}).");
23✔
305

306
    block.set_header_nonce(nonce);
23✔
307

308
    let timestamp = block.header().timestamp;
23✔
309
    let timestamp_standard = timestamp.standard_format();
23✔
310
    let elapsed_human = (timestamp - previous_block_header.timestamp).format_human_duration();
23✔
311
    let hash = block.hash();
23✔
312
    let hex = hash.to_hex();
23✔
313
    let height = block.kernel.header.height;
23✔
314
    let num_inputs = block.body().transaction_kernel.inputs.len();
23✔
315
    let num_outputs = block.body().transaction_kernel.outputs.len();
23✔
316
    info!(
23✔
317
        r#"Newly mined block details:
×
318
              Height: {height}
×
319
                Time: {timestamp_standard} ({timestamp})
×
320
Since previous block: {elapsed_human}
×
321
        Digest (Hex): {hex}
×
322
        Digest (Raw): {hash}
×
323
Difficulty threshold: {threshold}
×
324
          Difficulty: {prev_difficulty}
×
325
           #inputs  : {num_inputs}
×
326
           #outputs : {num_outputs}
×
327
"#
×
328
    );
329

330
    let new_block_found = NewBlockFound {
23✔
331
        block: Box::new(block),
23✔
332
    };
23✔
333

334
    sender
23✔
335
        .send(new_block_found)
23✔
336
        .unwrap_or_else(|_| warn!("Receiver in mining loop closed prematurely"))
23✔
337
}
23✔
338

339
enum GuessNonceResult {
340
    NonceFound { nonce: Digest },
341
    BlockNotFound,
342
    Cancelled,
343
}
344
impl GuessNonceResult {
345
    fn block_not_found(&self) -> bool {
17,830,533✔
346
        matches!(self, Self::BlockNotFound)
17,830,533✔
347
    }
17,830,533✔
348
}
349

350
/// Return the block-kernel MAST hash given a variable nonce, holding all other
351
/// fields constant.
352
///
353
/// Calculates the block hash in as few Tip5 invocations as possible.
354
/// This function is required for benchmarks, but is not part of the public API.
355
#[inline(always)]
356
#[doc(hidden)]
357
pub fn fast_kernel_mast_hash(
17,834,465✔
358
    kernel_auth_path: [Digest; BlockKernel::MAST_HEIGHT],
17,834,465✔
359
    header_auth_path: [Digest; BlockHeader::MAST_HEIGHT],
17,834,465✔
360
    nonce: Digest,
17,834,465✔
361
) -> Digest {
17,834,465✔
362
    let header_mast_hash = Tip5::hash_pair(Tip5::hash_varlen(&nonce.encode()), header_auth_path[0]);
17,834,465✔
363
    let header_mast_hash = Tip5::hash_pair(header_mast_hash, header_auth_path[1]);
17,834,465✔
364
    let header_mast_hash = Tip5::hash_pair(header_auth_path[2], header_mast_hash);
17,834,465✔
365

366
    Tip5::hash_pair(
17,834,465✔
367
        Tip5::hash_pair(
17,834,465✔
368
            Tip5::hash_varlen(&header_mast_hash.encode()),
17,834,465✔
369
            kernel_auth_path[0],
17,834,465✔
370
        ),
371
        kernel_auth_path[1],
17,834,465✔
372
    )
373
}
17,834,465✔
374

375
/// Run a single iteration of the mining loop.
376
#[inline]
377
fn guess_nonce_iteration(
17,830,600✔
378
    kernel_auth_path: [Digest; BlockKernel::MAST_HEIGHT],
17,830,600✔
379
    threshold: Digest,
17,830,600✔
380
    sleepy_guessing: bool,
17,830,600✔
381
    rng: &mut rand::rngs::ThreadRng,
17,830,600✔
382
    bh_auth_path: [Digest; BlockHeader::MAST_HEIGHT],
17,830,600✔
383
    sender: &oneshot::Sender<NewBlockFound>,
17,830,600✔
384
) -> GuessNonceResult {
17,830,600✔
385
    if sleepy_guessing {
17,830,600✔
386
        std::thread::sleep(Duration::from_millis(100));
×
387
    }
17,830,600✔
388

389
    // Modify the nonce in the block header. In order to collect the guesser
390
    // fee, this nonce must be the post-image of a known pre-image under Tip5.
391
    let nonce: Digest = rng.random();
17,830,600✔
392

393
    // Check every N guesses if task has been cancelled.
394
    if (sleepy_guessing || (nonce.values()[0].raw_u64() % (1 << 16)) == 0) && sender.is_canceled() {
17,830,600✔
395
        debug!("Guesser was cancelled.");
×
396
        return GuessNonceResult::Cancelled;
×
397
    }
17,830,600✔
398

399
    let block_hash = fast_kernel_mast_hash(kernel_auth_path, bh_auth_path, nonce);
17,830,600✔
400
    let success = block_hash <= threshold;
17,830,600✔
401

402
    match success {
17,830,600✔
403
        false => GuessNonceResult::BlockNotFound,
17,830,577✔
404
        true => GuessNonceResult::NonceFound { nonce },
23✔
405
    }
406
}
17,830,600✔
407

408
/// Make a coinbase transaction rewarding the composer identified by receiving
409
/// address with the block subsidy minus the guesser fee. The rest, including
410
/// transaction fees, goes to the guesser.
411
pub(crate) async fn make_coinbase_transaction_stateless(
991✔
412
    latest_block: &Block,
991✔
413
    composer_parameters: ComposerParameters,
991✔
414
    timestamp: Timestamp,
991✔
415
    vm_job_queue: Arc<TritonVmJobQueue>,
991✔
416
    job_options: TritonVmProofJobOptions,
991✔
417
) -> Result<(Transaction, TxOutputList)> {
991✔
418
    let (composer_outputs, transaction_details) = prepare_coinbase_transaction_stateless(
991✔
419
        latest_block,
991✔
420
        composer_parameters,
991✔
421
        timestamp,
991✔
422
        job_options.job_settings.network,
991✔
423
    );
991✔
424

425
    let witness = PrimitiveWitness::from_transaction_details(&transaction_details);
991✔
426

427
    info!("Start: generate single proof for coinbase transaction");
991✔
428

429
    // note: we provide an owned witness to proof-builder and clone the kernel
430
    // because this fn accepts arbitrary proving power and generates proof to
431
    // match highest.  If we were guaranteed to NOT be generating a witness
432
    // proof, we could use primitive_witness_ref() instead to avoid clone.
433

434
    let kernel = witness.kernel.clone();
991✔
435

436
    let proof = TransactionProofBuilder::new()
991✔
437
        .transaction_details(&transaction_details)
991✔
438
        .primitive_witness(witness)
991✔
439
        .job_queue(vm_job_queue)
991✔
440
        .proof_job_options(job_options)
991✔
441
        .build()
991✔
442
        .await?;
991✔
443

444
    info!("Done: generating single proof for coinbase transaction");
988✔
445

446
    let transaction = TransactionBuilder::new()
988✔
447
        .transaction_kernel(kernel)
988✔
448
        .transaction_proof(proof)
988✔
449
        .build()?;
988✔
450

451
    Ok((transaction, composer_outputs))
988✔
452
}
991✔
453

454
/// Produce outputs spending a given portion of the coinbase amount. Returns
455
/// two outputs unless this composition awards the entire coinbase amount to
456
/// the guesser, in which case zero outputs are returned.
457
///
458
/// The coinbase amount is usually set to the block subsidy for this block
459
/// height.
460
///
461
/// There are two equal-value outputs because one is liquid immediately, and the
462
/// other is locked for 3 years. The portion of the entire block subsidy that
463
/// goes to the composer is determined by the `guesser_fee_fraction` field of
464
/// the composer parameters.
465
///
466
/// The sum of the value of the outputs is guaranteed to not exceed the
467
/// coinbase amount, since the guesser fee fraction is guaranteed to be in the
468
/// range \[0;1\].
469
///
470
/// Returns: Either the empty list, or two outputs, one immediately liquid, the
471
/// other timelocked for three years, as required by the consensus rules.
472
///
473
/// # Panics
474
///
475
/// If the provided guesser fee fraction is not between 0 and 1 (inclusive).
476
pub(crate) fn composer_outputs(
1,140✔
477
    coinbase_amount: NativeCurrencyAmount,
1,140✔
478
    composer_parameters: ComposerParameters,
1,140✔
479
    timestamp: Timestamp,
1,140✔
480
) -> TxOutputList {
1,140✔
481
    let guesser_fee =
1,140✔
482
        coinbase_amount.lossy_f64_fraction_mul(composer_parameters.guesser_fee_fraction());
1,140✔
483

484
    let amount_to_composer = coinbase_amount
1,140✔
485
        .checked_sub(&guesser_fee)
1,140✔
486
        .expect("total_composer_fee cannot exceed coinbase_amount");
1,140✔
487

488
    if amount_to_composer.is_zero() {
1,140✔
489
        return Vec::<TxOutput>::default().into();
6✔
490
    }
1,134✔
491

492
    // Note that this calculation guarantees that at least half of the coinbase
493
    // is timelocked, as a rounding error in the last digit subtracts from the
494
    // liquid amount. This quality is required by the NativeCurrency type
495
    // script.
496
    let mut liquid_composer_amount = amount_to_composer;
1,134✔
497
    liquid_composer_amount.div_two();
1,134✔
498
    let timelocked_composer_amount = amount_to_composer
1,134✔
499
        .checked_sub(&liquid_composer_amount)
1,134✔
500
        .expect("Amount to composer must be larger than liquid amount to composer.");
1,134✔
501

502
    let owned = true;
1,134✔
503
    let liquid_coinbase_output = TxOutput::native_currency(
1,134✔
504
        liquid_composer_amount,
1,134✔
505
        composer_parameters.sender_randomness(),
1,134✔
506
        composer_parameters.reward_address(),
1,134✔
507
        composer_parameters.notification_policy().into(),
1,134✔
508
        owned,
1,134✔
509
    );
510

511
    // Set the time lock to 3 years (minimum) plus 30 minutes margin, since the
512
    // timestamp might be bumped by future merges. These timestamp bumps affect
513
    // only the `timestamp` field of the transaction kernel and not the state
514
    // of the `TimeLock` type script. So in the end you might end up with a
515
    // transaction whose time-locked portion is not time-locked long enough.
516
    let timelocked_coinbase_output = TxOutput::native_currency(
1,134✔
517
        timelocked_composer_amount,
1,134✔
518
        composer_parameters.sender_randomness(),
1,134✔
519
        composer_parameters.reward_address(),
1,134✔
520
        composer_parameters.notification_policy().into(),
1,134✔
521
        owned,
1,134✔
522
    )
523
    .with_time_lock(timestamp + MINING_REWARD_TIME_LOCK_PERIOD + Timestamp::minutes(30));
1,134✔
524

525
    vec![liquid_coinbase_output, timelocked_coinbase_output].into()
1,134✔
526
}
1,140✔
527

528
/// Compute `TransactionDetails` and a list of `TxOutput`s for a coinbase
529
/// transaction.
530
pub(super) fn prepare_coinbase_transaction_stateless(
1,136✔
531
    latest_block: &Block,
1,136✔
532
    composer_parameters: ComposerParameters,
1,136✔
533
    timestamp: Timestamp,
1,136✔
534
    network: Network,
1,136✔
535
) -> (TxOutputList, TransactionDetails) {
1,136✔
536
    let mutator_set_accumulator = latest_block.mutator_set_accumulator_after().clone();
1,136✔
537
    let next_block_height: BlockHeight = latest_block.header().height.next();
1,136✔
538
    info!("Creating coinbase for block of height {next_block_height}.");
1,136✔
539

540
    let coinbase_amount = Block::block_subsidy(next_block_height);
1,136✔
541
    let composer_outputs = composer_outputs(coinbase_amount, composer_parameters, timestamp);
1,136✔
542
    let total_composer_fee = composer_outputs.total_native_coins();
1,136✔
543

544
    let guesser_fee = coinbase_amount
1,136✔
545
        .checked_sub(&total_composer_fee)
1,136✔
546
        .expect("total_composer_fee cannot exceed coinbase_amount");
1,136✔
547

548
    info!(
1,136✔
549
        "Coinbase amount is set to {coinbase_amount} and is divided between \
×
550
        composer fee ({total_composer_fee}) and guesser fee ({guesser_fee})."
×
551
    );
552

553
    let transaction_details = TransactionDetails::new_with_coinbase(
1,136✔
554
        TxInputList::empty(),
1,136✔
555
        composer_outputs.clone(),
1,136✔
556
        coinbase_amount,
1,136✔
557
        guesser_fee,
1,136✔
558
        timestamp,
1,136✔
559
        mutator_set_accumulator,
1,136✔
560
        network,
1,136✔
561
    );
562

563
    (composer_outputs, transaction_details)
1,136✔
564
}
1,136✔
565

566
/// Enumerates origins of transactions to be merged into a block transaction.
567
///
568
/// In the general case, this is (just) the mempool.
569
pub(crate) enum TxMergeOrigin {
570
    Mempool,
571
    #[cfg(test)]
572
    ExplicitList(Vec<Transaction>),
573
}
574

575
/// Create the transaction that goes into the block template. The transaction is
576
/// built from the mempool and from the coinbase transaction. Also returns the
577
/// "sender randomness" used in the coinbase transaction.
578
pub(crate) async fn create_block_transaction(
18✔
579
    predecessor_block: &Block,
18✔
580
    global_state_lock: &GlobalStateLock,
18✔
581
    timestamp: Timestamp,
18✔
582
    job_options: TritonVmProofJobOptions,
18✔
583
) -> Result<(Transaction, Vec<ExpectedUtxo>)> {
18✔
584
    create_block_transaction_from(
18✔
585
        predecessor_block,
18✔
586
        global_state_lock,
18✔
587
        timestamp,
18✔
588
        job_options,
18✔
589
        TxMergeOrigin::Mempool,
18✔
590
    )
18✔
591
    .await
18✔
592
}
18✔
593

594
pub(crate) async fn create_block_transaction_from(
25✔
595
    predecessor_block: &Block,
25✔
596
    global_state_lock: &GlobalStateLock,
25✔
597
    timestamp: Timestamp,
25✔
598
    job_options: TritonVmProofJobOptions,
25✔
599
    tx_merge_origin: TxMergeOrigin,
25✔
600
) -> Result<(Transaction, Vec<ExpectedUtxo>)> {
25✔
601
    let block_capacity_for_transactions = SIZE_20MB_IN_BYTES;
25✔
602

603
    let predecessor_block_ms = predecessor_block.mutator_set_accumulator_after();
25✔
604
    let mutator_set_hash = predecessor_block_ms.hash();
25✔
605
    debug!("Creating block transaction with mutator set hash: {mutator_set_hash}",);
25✔
606

607
    let mut rng: StdRng =
25✔
608
        SeedableRng::from_seed(global_state_lock.lock_guard().await.shuffle_seed());
25✔
609

610
    let composer_parameters = global_state_lock
25✔
611
        .lock_guard()
25✔
612
        .await
25✔
613
        .composer_parameters(predecessor_block.header().height.next());
25✔
614

615
    // A coinbase transaction implies mining. So you *must*
616
    // be able to create a SingleProof.
617
    let vm_job_queue = vm_job_queue();
25✔
618
    let (coinbase_transaction, composer_txos) = make_coinbase_transaction_stateless(
25✔
619
        predecessor_block,
25✔
620
        composer_parameters.clone(),
25✔
621
        timestamp,
25✔
622
        vm_job_queue.clone(),
25✔
623
        job_options.clone(),
25✔
624
    )
25✔
625
    .await?;
25✔
626

627
    // Get most valuable transactions from mempool.
628
    let only_merge_single_proofs = true;
22✔
629
    let mut transactions_to_merge = match tx_merge_origin {
22✔
630
        #[cfg(test)]
631
        TxMergeOrigin::ExplicitList(transactions) => transactions,
6✔
632
        TxMergeOrigin::Mempool => global_state_lock
16✔
633
            .lock_guard()
16✔
634
            .await
16✔
635
            .mempool
636
            .get_transactions_for_block(
16✔
637
                block_capacity_for_transactions,
16✔
638
                Some(MAX_NUM_TXS_TO_MERGE),
16✔
639
                only_merge_single_proofs,
16✔
640
                mutator_set_hash,
16✔
641
            ),
642
    };
643

644
    // If necessary, populate list with nop-tx.
645
    // Guarantees that some merge happens in below loop, which sets merge-bit.
646
    if transactions_to_merge.is_empty() {
22✔
647
        let nop = TransactionDetails::nop(
13✔
648
            predecessor_block.mutator_set_accumulator_after(),
13✔
649
            timestamp,
13✔
650
            global_state_lock.cli().network,
13✔
651
        );
652
        let nop = PrimitiveWitness::from_transaction_details(&nop);
13✔
653

654
        // ensure that proof-type is SingleProof
655
        let options = TritonVmProofJobOptionsBuilder::new()
13✔
656
            .template(&job_options)
13✔
657
            .proof_type(TransactionProofType::SingleProof)
13✔
658
            .build();
13✔
659

660
        let proof = TransactionProofBuilder::new()
13✔
661
            .primitive_witness_ref(&nop)
13✔
662
            .job_queue(vm_job_queue.clone())
13✔
663
            .proof_job_options(options)
13✔
664
            .build()
13✔
665
            .await?;
13✔
666
        let nop = Transaction {
13✔
667
            kernel: nop.kernel,
13✔
668
            proof,
13✔
669
        };
13✔
670

671
        transactions_to_merge = vec![nop];
13✔
672
    }
9✔
673

674
    let num_merges = transactions_to_merge.len();
22✔
675
    let mut block_transaction = coinbase_transaction;
22✔
676
    for (i, tx_to_include) in transactions_to_merge.into_iter().enumerate() {
22✔
677
        info!("Merging transaction {} / {}", i + 1, num_merges);
22✔
678
        info!(
22✔
679
            "Merging tx with {} inputs, {} outputs. With fee {}.",
×
680
            tx_to_include.kernel.inputs.len(),
×
681
            tx_to_include.kernel.outputs.len(),
×
682
            tx_to_include.kernel.fee
×
683
        );
684
        block_transaction = Transaction::merge_with(
22✔
685
            block_transaction,
22✔
686
            tx_to_include,
22✔
687
            rng.random(),
22✔
688
            vm_job_queue.clone(),
22✔
689
            job_options.clone(),
22✔
690
        )
22✔
691
        .await?; // fix #579.  propagate error up.
22✔
692
    }
693

694
    let own_expected_utxos = composer_parameters.extract_expected_utxos(composer_txos);
22✔
695

696
    Ok((block_transaction, own_expected_utxos))
22✔
697
}
25✔
698

699
///
700
///
701
/// Locking:
702
///   * acquires `global_state_lock` for write
703
pub(crate) async fn mine(
1✔
704
    mut from_main: mpsc::Receiver<MainToMiner>,
1✔
705
    to_main: mpsc::Sender<MinerToMain>,
1✔
706
    mut global_state_lock: GlobalStateLock,
1✔
707
) -> Result<()> {
1✔
708
    // Set PoW guessing to restart every N seconds, if it has been started. Only
709
    // the guesser task may set this to actually resolve, as this will otherwise
710
    // abort e.g. the composer.
711
    const GUESSING_RESTART_INTERVAL_IN_SECONDS: u64 = 20;
712

713
    // we disable the initial sleep when invoked for unit tests.
714
    //
715
    // note: it can take an arbitrary amount of time to obtain latest-block info
716
    // from peers.  If that is important, we should be listening on a channel
717
    // instead or better this task should not be started until obtained.
718
    #[cfg(not(test))]
719
    {
720
        // Wait before starting mining task to ensure that peers have sent us
721
        // information about their latest blocks. This should prevent the client
722
        // from finding blocks that will later be orphaned.
723
        const INITIAL_MINING_SLEEP_IN_SECONDS: u64 = 60;
724

725
        tracing::info!(
×
726
            "sleeping for {} seconds while node initializes",
×
727
            INITIAL_MINING_SLEEP_IN_SECONDS
728
        );
729
        tokio::time::sleep(Duration::from_secs(INITIAL_MINING_SLEEP_IN_SECONDS)).await;
×
730
    }
731

732
    let cli_args = global_state_lock.cli().clone();
1✔
733
    let network = cli_args.network;
1✔
734

735
    let guess_restart_interval = Duration::from_secs(GUESSING_RESTART_INTERVAL_IN_SECONDS);
1✔
736
    let infinite = Duration::from_secs(u32::MAX.into());
1✔
737
    let guess_restart_timer = time::sleep(infinite);
1✔
738
    tokio::pin!(guess_restart_timer);
1✔
739

740
    let mut pause_mine = false;
1✔
741
    let mut wait_for_confirmation = false;
1✔
742
    loop {
743
        // Ensure restart timer doesn't resolve again, without guesser
744
        // task actually being spawned.
745
        guess_restart_timer
3✔
746
            .as_mut()
3✔
747
            .reset(tokio::time::Instant::now() + infinite);
3✔
748

749
        let (is_connected, is_syncing, mining_status) = global_state_lock
3✔
750
            .lock(|s| {
3✔
751
                (
3✔
752
                    !s.net.peer_map.is_empty(),
3✔
753
                    s.net.sync_anchor.is_some(),
3✔
754
                    s.mining_state.mining_status,
3✔
755
                )
3✔
756
            })
3✔
757
            .await;
3✔
758
        if !is_connected {
3✔
759
            const WAIT_TIME_WHEN_DISCONNECTED_IN_SECONDS: u64 = 5;
760
            global_state_lock.set_mining_status_to_inactive().await;
×
761
            warn!("Not mining because client has no connections");
×
762
            sleep(Duration::from_secs(WAIT_TIME_WHEN_DISCONNECTED_IN_SECONDS)).await;
×
763
            continue;
×
764
        }
3✔
765

766
        let (guesser_tx, guesser_rx) = oneshot::channel::<NewBlockFound>();
3✔
767
        let (composer_tx, composer_rx) = oneshot::channel::<(Block, Vec<ExpectedUtxo>)>();
3✔
768

769
        let maybe_proposal = global_state_lock
3✔
770
            .lock_guard()
3✔
771
            .await
3✔
772
            .mining_state
773
            .block_proposal
774
            .clone();
3✔
775
        let guess = cli_args.guess;
3✔
776

777
        let should_guess = !wait_for_confirmation
3✔
778
            && guess
3✔
779
            && maybe_proposal.is_some()
×
780
            && !is_syncing
×
781
            && !pause_mine
×
782
            && is_connected;
×
783

784
        // if start_guessing is true, then we are in a state change from
785
        // inactive state to guessing state.
786
        //
787
        // if start_guessing is false and should_guess is true then we
788
        // have already been guessing and are restarting with new params.
789
        let start_guessing = matches!(
3✔
790
            (mining_status, should_guess),
3✔
791
            (MiningStatus::Inactive, true)
792
        );
793

794
        if start_guessing {
3✔
795
            let proposal = maybe_proposal.unwrap(); // is_some() verified above
×
796
            global_state_lock
×
797
                .set_mining_status_to_guessing(proposal)
×
798
                .await;
×
799
        }
3✔
800

801
        let guesser_task: Option<JoinHandle<()>> = if should_guess {
3✔
802
            // safe because above `is_some`
803
            let proposal = maybe_proposal.unwrap();
×
804
            let guesser_key = global_state_lock
×
805
                .lock_guard()
×
806
                .await
×
807
                .wallet_state
808
                .wallet_entropy
809
                .guesser_spending_key(proposal.header().prev_block_digest);
×
810

811
            let latest_block_header = global_state_lock
×
812
                .lock(|s| s.chain.light_state().header().to_owned())
×
813
                .await;
×
814
            let guesser_task = guess_nonce(
×
815
                network,
×
816
                proposal.to_owned(),
×
817
                latest_block_header,
×
818
                guesser_tx,
×
819
                guesser_key,
×
820
                GuessingConfiguration {
×
821
                    sleepy_guessing: cli_args.sleepy_guessing,
×
822
                    num_guesser_threads: cli_args.guesser_threads,
×
823
                },
×
824
            );
825

826
            // Only run for N seconds to allow for updating of block's timestamp
827
            // and difficulty.
828
            guess_restart_timer
×
829
                .as_mut()
×
830
                .reset(tokio::time::Instant::now() + guess_restart_interval);
×
831

832
            Some(
×
833
                tokio::task::Builder::new()
×
834
                    .name("guesser")
×
835
                    .spawn(guesser_task)
×
836
                    .expect("Failed to spawn guesser task"),
×
837
            )
×
838
        } else {
839
            None
3✔
840
        };
841

842
        let (cancel_compose_tx, cancel_compose_rx) = tokio::sync::watch::channel(());
3✔
843

844
        let compose = cli_args.compose;
3✔
845
        let mut composer_task = if !wait_for_confirmation
3✔
846
            && compose
3✔
847
            && guesser_task.is_none()
3✔
848
            && !is_syncing
3✔
849
            && !pause_mine
3✔
850
            && is_connected
2✔
851
        {
852
            global_state_lock.set_mining_status_to_composing().await;
2✔
853

854
            let latest_block = global_state_lock
2✔
855
                .lock(|s| s.chain.light_state().to_owned())
2✔
856
                .await;
2✔
857
            let compose_task = compose_block(
2✔
858
                latest_block,
2✔
859
                global_state_lock.clone(),
2✔
860
                composer_tx,
2✔
861
                cancel_compose_rx,
2✔
862
                Timestamp::now(),
2✔
863
            );
864

865
            let task = tokio::task::Builder::new()
2✔
866
                .name("composer")
2✔
867
                .spawn(compose_task)
2✔
868
                .expect("Failed to spawn composer task.");
2✔
869

870
            task
2✔
871
        } else {
872
            tokio::spawn(async { Ok(()) })
1✔
873
        };
874

875
        let mut restart_guessing = false;
3✔
876
        let mut stop_guessing = false;
3✔
877
        let mut stop_composing = false;
3✔
878
        let mut stop_looping = false;
3✔
879

880
        // Await a message from either the worker task or from the main loop,
881
        // or the restart of the guesser-task.
882
        select! {
3✔
883
            _ = &mut guess_restart_timer => {
3✔
884
                restart_guessing = true;
×
885
            }
×
886
            Ok(Err(e)) = &mut composer_task => {
3✔
887

888
                match e.root_cause().downcast_ref::<CreateProofError>() {
×
889
                    // address issue 579.
890
                    //
891
                    // check if error indicates job was cancelled. If so,
892
                    // simply log and continue, but ignore the error.
893
                    //
894
                    // this is a fail-safe and appears unreachable for present
895
                    // codebase during normal mining-loop operation.
896
                    //
897
                    // job cancellation can occur any time that the cancellation
898
                    // channel Sender gets dropped, which occurs if
899
                    // composer_task gets aborted which occurs if any other
900
                    // branch of this select!{} resolves first.  Common causes
901
                    // are NewBlock and NewBlockProposal messages from main.
902
                    //
903
                    // HOWEVER: if the composer_task is aborted because another
904
                    // branch of the select resolves first then this branch
905
                    // should not execute making this check unnecessary.
906
                    //
907
                    // The remaining sources of cancellation are:
908
                    // 1. mining loop exits, eg during graceful shutdown.
909
                    // 2. some future change to codebase
910
                    Some(CreateProofError::JobHandleError(JobHandleError::JobCancelled)) => {
911
                        debug!("composer job was cancelled. continuing normal operation");
×
912
                    }
913
                    _ => {
914
                        // Ensure graceful shutdown in case of error during composition.
915
                        stop_composing = true;
×
916
                        error!("Composition failed: {}", e);
×
917
                        to_main.send(MinerToMain::Shutdown(COMPOSITION_FAILED_EXIT_CODE)).await?;
×
918
                    }
919
                }
920
            },
921

922
            Some(main_message) = from_main.recv() => {
3✔
923
                debug!("Miner received message type: {}", main_message.get_type());
2✔
924

925
                match main_message {
2✔
926
                    MainToMiner::Shutdown => {
927
                        debug!("Miner shutting down.");
×
928

929
                        stop_guessing = true;
×
930
                        stop_composing = true;
×
931
                        stop_looping = true;
×
932
                    }
933
                    MainToMiner::NewBlock => {
934
                        stop_guessing = true;
×
935
                        stop_composing = true;
×
936

937
                        info!("Miner task received notification about new block");
×
938
                    }
939
                    MainToMiner::NewBlockProposal => {
940
                        stop_guessing = true;
×
941
                        stop_composing = true;
×
942

943
                        info!("Miner received message about new block proposal for guessing.");
×
944
                    }
945
                    MainToMiner::WaitForContinue => {
×
946
                        stop_guessing = true;
×
947
                        stop_composing = true;
×
948

×
949
                        wait_for_confirmation = true;
×
950
                    }
×
951
                    MainToMiner::Continue => {
×
952
                        wait_for_confirmation = false;
×
953
                    }
×
954
                    MainToMiner::StopMining => {
1✔
955
                        pause_mine = true;
1✔
956

1✔
957
                        stop_guessing = true;
1✔
958
                        stop_composing = true;
1✔
959
                    }
1✔
960
                    MainToMiner::StartMining => {
1✔
961
                        pause_mine = false;
1✔
962
                    }
1✔
963
                    MainToMiner::StopSyncing => {
×
964
                        // no need to do anything here.  Mining will
×
965
                        // resume or not at top of loop depending on
×
966
                        // pause_mine and syncing variables.
×
967
                    }
×
968
                    MainToMiner::StartSyncing => {
×
969
                        // when syncing begins, we must halt the mining
×
970
                        // task.  But we don't change the pause_mine
×
971
                        // variable, because it reflects the logical on/off
×
972
                        // of mining, which syncing can temporarily override
×
973
                        // but not alter the setting.
×
974
                        stop_guessing = true;
×
975
                        stop_composing = true;
×
976
                    }
×
977
                }
978
            }
979
            new_composition = composer_rx => {
3✔
980
                stop_composing = true;
×
981

982
                match new_composition {
×
983
                    Ok((new_block_proposal, composer_utxos)) => {
×
984
                        to_main.send(MinerToMain::BlockProposal(Box::new((new_block_proposal, composer_utxos)))).await?;
×
985
                        wait_for_confirmation = true;
×
986
                    },
987
                    Err(e) => warn!("composing task was cancelled prematurely. Got: {}", e),
×
988
                };
989
            }
990
            new_block = guesser_rx => {
3✔
991
                stop_guessing = true;
×
992

993
                match new_block {
×
994
                    Err(err) => {
×
995
                        warn!("Mining task was cancelled prematurely. Got: {}", err);
×
996
                    }
997
                    Ok(new_block_found) => {
×
998
                        debug!("Worker task reports new block of height {}", new_block_found.block.kernel.header.height);
×
999

1000
                        // Sanity check, remove for more efficient mining.
1001
                        // The below PoW check could fail due to race conditions. So we don't panic,
1002
                        // we only ignore what the worker task sent us.
1003
                        let latest_block = global_state_lock
×
1004
                            .lock(|s| s.chain.light_state().to_owned())
×
1005
                            .await;
×
1006

1007
                        if !new_block_found.block.has_proof_of_work(cli_args.network, latest_block.header()) {
×
1008
                            error!("Own mined block did not have valid PoW Discarding.");
×
1009
                        } else if !new_block_found.block.is_valid(&latest_block, Timestamp::now(), global_state_lock.cli().network).await {
×
1010
                                // Block could be invalid if for instance the proof and proof-of-work
1011
                                // took less time than the minimum block time.
1012
                                error!("Found block with valid proof-of-work but block is invalid.");
×
1013
                        } else {
1014

1015
                            info!("Found new {} block with block height {}. Hash: {}", global_state_lock.cli().network, new_block_found.block.kernel.header.height, new_block_found.block.hash());
×
1016

1017
                            to_main.send(MinerToMain::NewBlockFound(new_block_found)).await?;
×
1018

1019
                            wait_for_confirmation = true;
×
1020
                        }
1021
                    },
1022
                };
1023
            }
1024
        }
1025

1026
        if restart_guessing {
2✔
1027
            if let Some(gt) = &guesser_task {
×
1028
                gt.abort();
×
1029
                debug!("Abort-signal sent to guesser worker.");
×
1030
                debug!("Restarting guesser task with new parameters");
×
1031
            }
×
1032
        }
2✔
1033
        if stop_guessing {
2✔
1034
            if let Some(gt) = &guesser_task {
1✔
1035
                gt.abort();
×
1036
                debug!("Abort-signal sent to guesser worker.");
×
1037
            }
1✔
1038
            global_state_lock.set_mining_status_to_inactive().await;
1✔
1039
        }
1✔
1040
        if stop_composing {
2✔
1041
            if !composer_task.is_finished() {
1✔
1042
                cancel_compose_tx.send(())?;
1✔
1043
                debug!("Cancel signal sent to composer worker.");
1✔
1044
            }
×
1045
            // avoid duplicate call if stop_guessing is also true.
1046
            if !stop_guessing {
1✔
1047
                global_state_lock.set_mining_status_to_inactive().await;
×
1048
            }
1✔
1049
        }
1✔
1050

1051
        if stop_looping {
2✔
1052
            break;
×
1053
        }
2✔
1054
    }
1055
    debug!("Miner shut down gracefully.");
×
1056
    Ok(())
×
1057
}
×
1058

1059
#[cfg(test)]
1060
#[cfg_attr(coverage_nightly, coverage(off))]
1061
pub(crate) mod tests {
1062
    use std::hint::black_box;
1063

1064
    use block_appendix::BlockAppendix;
1065
    use block_body::BlockBody;
1066
    use block_header::tests::random_block_header;
1067
    use difficulty_control::Difficulty;
1068
    use itertools::Itertools;
1069
    use macro_rules_attr::apply;
1070
    use num_bigint::BigUint;
1071
    use num_traits::One;
1072
    use num_traits::Pow;
1073
    use num_traits::Zero;
1074
    use tracing_test::traced_test;
1075

1076
    use super::*;
1077
    use crate::config_models::cli_args;
1078
    use crate::config_models::fee_notification_policy::FeeNotificationPolicy;
1079
    use crate::config_models::network::Network;
1080
    use crate::job_queue::errors::JobHandleError;
1081
    use crate::models::blockchain::block::mock_block_generator::MockBlockGenerator;
1082
    use crate::models::blockchain::block::validity::block_primitive_witness::tests::deterministic_block_primitive_witness;
1083
    use crate::models::blockchain::transaction::validity::single_proof::SingleProof;
1084
    use crate::models::blockchain::type_scripts::native_currency_amount::NativeCurrencyAmount;
1085
    use crate::models::proof_abstractions::mast_hash::MastHash;
1086
    use crate::models::proof_abstractions::timestamp::Timestamp;
1087
    use crate::models::proof_abstractions::verifier::verify;
1088
    use crate::models::state::mempool::TransactionOrigin;
1089
    use crate::models::state::tx_creation_config::TxCreationConfig;
1090
    use crate::models::state::tx_proving_capability::TxProvingCapability;
1091
    use crate::models::state::wallet::address::symmetric_key::SymmetricKey;
1092
    use crate::models::state::wallet::transaction_output::TxOutput;
1093
    use crate::models::state::wallet::wallet_entropy::WalletEntropy;
1094
    use crate::tests::shared::dummy_expected_utxo;
1095
    use crate::tests::shared::invalid_empty_block;
1096
    use crate::tests::shared::make_mock_transaction_with_mutator_set_hash;
1097
    use crate::tests::shared::mock_genesis_global_state;
1098
    use crate::tests::shared::random_transaction_kernel;
1099
    use crate::tests::shared::wait_until;
1100
    use crate::tests::shared_tokio_runtime;
1101
    use crate::triton_vm_job_queue::TritonVmJobQueue;
1102
    use crate::util_types::test_shared::mutator_set::pseudorandom_addition_record;
1103
    use crate::util_types::test_shared::mutator_set::random_mmra;
1104
    use crate::util_types::test_shared::mutator_set::random_mutator_set_accumulator;
1105
    use crate::MINER_CHANNEL_CAPACITY;
1106

1107
    /// Produce a transaction that allocates the given fraction of the block
1108
    /// subsidy to the wallet in two UTXOs, one time-locked and one liquid.
1109
    pub(crate) async fn make_coinbase_transaction_from_state(
1110
        latest_block: &Block,
1111
        global_state_lock: &GlobalStateLock,
1112
        timestamp: Timestamp,
1113
        job_options: TritonVmProofJobOptions,
1114
    ) -> Result<(Transaction, Vec<ExpectedUtxo>)> {
1115
        // It's important to use the input `latest_block` here instead of
1116
        // reading it from state, since that could, because of a race condition
1117
        // lead to an inconsistent witness higher up in the call graph. This is
1118
        // done to avoid holding a read-lock throughout this function.
1119
        let next_block_height: BlockHeight = latest_block.header().height.next();
1120
        let vm_job_queue = vm_job_queue();
1121

1122
        let composer_parameters = global_state_lock
1123
            .lock_guard()
1124
            .await
1125
            .composer_parameters(next_block_height);
1126
        let (transaction, composer_outputs) = make_coinbase_transaction_stateless(
1127
            latest_block,
1128
            composer_parameters.clone(),
1129
            timestamp,
1130
            vm_job_queue,
1131
            job_options,
1132
        )
1133
        .await?;
1134

1135
        let own_expected_utxos = composer_parameters.extract_expected_utxos(composer_outputs);
1136

1137
        Ok((transaction, own_expected_utxos))
1138
    }
1139

1140
    /// Similar to [mine_iteration] function but intended for tests.
1141
    ///
1142
    /// Does *not* update the timestamp of the block and therefore also does not
1143
    /// update the difficulty field, as this applies to the next block and only
1144
    /// changes as a result of the timestamp of this block.
1145
    pub(crate) fn mine_iteration_for_tests(block: &mut Block, rng: &mut StdRng) {
1146
        let nonce = rng.random();
1147
        block.set_header_nonce(nonce);
1148
    }
1149

1150
    /// Estimates the hash rate in number of hashes per milliseconds
1151
    async fn estimate_own_hash_rate(
1152
        target_block_interval: Timestamp,
1153
        sleepy_guessing: bool,
1154
        num_outputs: usize,
1155
    ) -> f64 {
1156
        let mut rng = rand::rng();
1157
        let network = Network::RegTest;
1158
        let global_state_lock = mock_genesis_global_state(
1159
            2,
1160
            WalletEntropy::devnet_wallet(),
1161
            cli_args::Args::default_with_network(network),
1162
        )
1163
        .await;
1164

1165
        let previous_block = global_state_lock
1166
            .lock_guard()
1167
            .await
1168
            .chain
1169
            .light_state()
1170
            .clone();
1171

1172
        let (transaction, _coinbase_utxo_info) = {
1173
            let outputs = (0..num_outputs)
1174
                .map(|_| pseudorandom_addition_record(rng.random()))
1175
                .collect_vec();
1176
            (
1177
                make_mock_transaction_with_mutator_set_hash(
1178
                    vec![],
1179
                    outputs,
1180
                    previous_block.mutator_set_accumulator_after().hash(),
1181
                ),
1182
                dummy_expected_utxo(),
1183
            )
1184
        };
1185
        let start_time = Timestamp::now();
1186
        let block = Block::block_template_invalid_proof(
1187
            &previous_block,
1188
            transaction,
1189
            start_time,
1190
            target_block_interval,
1191
        );
1192
        let threshold = previous_block.header().difficulty.target();
1193
        let num_iterations_launched = 1_000_000;
1194
        let tick = std::time::SystemTime::now();
1195
        let (kernel_auth_path, header_auth_path) = precalculate_block_auth_paths(&block);
1196

1197
        let (worker_task_tx, worker_task_rx) = oneshot::channel::<NewBlockFound>();
1198
        let num_iterations_run =
1199
            rayon::iter::IntoParallelIterator::into_par_iter(0..num_iterations_launched)
1200
                .map_init(rand::rng, |prng, _i| {
1201
                    guess_nonce_iteration(
1202
                        kernel_auth_path,
1203
                        threshold,
1204
                        sleepy_guessing,
1205
                        prng,
1206
                        header_auth_path,
1207
                        &worker_task_tx,
1208
                    );
1209
                })
1210
                .count();
1211
        drop(worker_task_rx);
1212

1213
        let time_spent_mining = tick.elapsed().unwrap();
1214

1215
        (num_iterations_run as f64) / (time_spent_mining.as_millis() as f64)
1216
    }
1217

1218
    /// Estimate the time it takes to prepare a block so we can start guessing
1219
    /// nonces.
1220
    async fn estimate_block_preparation_time_invalid_proof() -> f64 {
1221
        let network = Network::Main;
1222
        let genesis_block = Block::genesis(network);
1223
        let guesser_fee_fraction = 0.0;
1224
        let cli_args = cli_args::Args {
1225
            guesser_fraction: guesser_fee_fraction,
1226
            network,
1227
            ..Default::default()
1228
        };
1229

1230
        let global_state_lock =
1231
            mock_genesis_global_state(2, WalletEntropy::devnet_wallet(), cli_args).await;
1232
        let tick = std::time::SystemTime::now();
1233
        let (transaction, _coinbase_utxo_info) = make_coinbase_transaction_from_state(
1234
            &genesis_block,
1235
            &global_state_lock,
1236
            network.launch_date(),
1237
            global_state_lock
1238
                .cli()
1239
                .proof_job_options_primitive_witness(),
1240
        )
1241
        .await
1242
        .unwrap();
1243

1244
        let in_seven_months = network.launch_date() + Timestamp::months(7);
1245
        let block = Block::block_template_invalid_proof(
1246
            &genesis_block,
1247
            transaction,
1248
            in_seven_months,
1249
            network.target_block_interval(),
1250
        );
1251
        let tock = tick.elapsed().unwrap().as_millis() as f64;
1252
        black_box(block);
1253
        tock
1254
    }
1255

1256
    #[traced_test]
1257
    #[apply(shared_tokio_runtime)]
1258
    async fn block_proposal_for_height_one_is_valid_for_various_guesser_fee_fractions() {
1259
        // Verify that a block template made with transaction from the mempool is a valid block
1260
        let network = Network::Main;
1261
        let mut alice = mock_genesis_global_state(
1262
            2,
1263
            WalletEntropy::devnet_wallet(),
1264
            cli_args::Args::default_with_network(network),
1265
        )
1266
        .await;
1267
        let genesis_block = Block::genesis(network);
1268
        let now = genesis_block.kernel.header.timestamp + Timestamp::months(7);
1269
        assert!(
1270
            !alice
1271
                .lock_guard()
1272
                .await
1273
                .get_wallet_status_for_tip()
1274
                .await
1275
                .synced_unspent_available_amount(now)
1276
                .is_zero(),
1277
            "Assumed to be premine-recipient"
1278
        );
1279

1280
        let mut rng = StdRng::seed_from_u64(u64::from_str_radix("2350404", 6).unwrap());
1281

1282
        let alice_key = alice
1283
            .lock_guard()
1284
            .await
1285
            .wallet_state
1286
            .wallet_entropy
1287
            .nth_generation_spending_key_for_tests(0);
1288
        let output_to_alice = TxOutput::offchain_native_currency(
1289
            NativeCurrencyAmount::coins(4),
1290
            rng.random(),
1291
            alice_key.to_address().into(),
1292
            false,
1293
        );
1294
        let config = TxCreationConfig::default()
1295
            .recover_change_off_chain(alice_key.into())
1296
            .with_prover_capability(TxProvingCapability::SingleProof);
1297
        let tx_from_alice = alice
1298
            .api()
1299
            .tx_initiator_internal()
1300
            .create_transaction(
1301
                vec![output_to_alice].into(),
1302
                NativeCurrencyAmount::coins(1),
1303
                now,
1304
                config,
1305
            )
1306
            .await
1307
            .unwrap()
1308
            .transaction;
1309

1310
        let mut cli = cli_args::Args::default();
1311
        for guesser_fee_fraction in [0f64, 0.5, 1.0] {
1312
            // Verify constructed coinbase transaction and block template when mempool is empty
1313
            assert!(
1314
                alice.lock_guard().await.mempool.is_empty(),
1315
                "Mempool must be empty at start of loop"
1316
            );
1317

1318
            cli.guesser_fraction = guesser_fee_fraction;
1319
            alice.set_cli(cli.clone()).await;
1320
            let (transaction_empty_mempool, coinbase_utxo_info) = {
1321
                create_block_transaction(
1322
                    &genesis_block,
1323
                    &alice,
1324
                    now,
1325
                    (TritonVmJobPriority::Normal, None).into(),
1326
                )
1327
                .await
1328
                .unwrap()
1329
            };
1330
            assert!(
1331
                coinbase_utxo_info.is_empty(),
1332
                "Default composer UTXO notification policy is onchain. \
1333
             So no expected UTXOs should be returned here."
1334
            );
1335

1336
            let cb_txkmh = transaction_empty_mempool.kernel.mast_hash();
1337
            let cb_tx_claim = SingleProof::claim(cb_txkmh);
1338
            assert!(
1339
                verify(
1340
                    cb_tx_claim.clone(),
1341
                    transaction_empty_mempool
1342
                        .proof
1343
                        .clone()
1344
                        .into_single_proof()
1345
                        .clone(),
1346
                    network,
1347
                )
1348
                .await,
1349
                "Transaction proof for coinbase transaction must be valid."
1350
            );
1351

1352
            let num_coinbase_outputs = if guesser_fee_fraction == 1.0 { 0 } else { 2 };
1353
            assert_eq!(
1354
                num_coinbase_outputs,
1355
                transaction_empty_mempool.kernel.outputs.len(),
1356
                "Coinbase transaction with empty mempool must have exactly {num_coinbase_outputs} outputs"
1357
            );
1358
            assert!(
1359
                transaction_empty_mempool.kernel.inputs.is_empty(),
1360
                "Coinbase transaction with empty mempool must have zero inputs"
1361
            );
1362
            let block_1_empty_mempool = Block::compose(
1363
                &genesis_block,
1364
                transaction_empty_mempool,
1365
                now,
1366
                TritonVmJobQueue::get_instance(),
1367
                TritonVmJobPriority::High.into(),
1368
            )
1369
            .await
1370
            .unwrap();
1371
            assert!(
1372
                block_1_empty_mempool
1373
                    .is_valid(&genesis_block, now, network)
1374
                    .await,
1375
                "Block template created by miner with empty mempool must be valid"
1376
            );
1377

1378
            {
1379
                let mut alice_gsm = alice.lock_guard_mut().await;
1380
                alice_gsm
1381
                    .mempool_insert((*tx_from_alice).clone(), TransactionOrigin::Own)
1382
                    .await;
1383
                assert_eq!(1, alice_gsm.mempool.len());
1384
            }
1385

1386
            // Build transaction for block
1387
            let (transaction_non_empty_mempool, _new_coinbase_sender_randomness) = {
1388
                create_block_transaction(
1389
                    &genesis_block,
1390
                    &alice,
1391
                    now,
1392
                    (TritonVmJobPriority::Normal, None).into(),
1393
                )
1394
                .await
1395
                .unwrap()
1396
            };
1397

1398
            let num_outputs_after_merge = num_coinbase_outputs + 2;
1399
            assert_eq!(
1400
                num_outputs_after_merge,
1401
                transaction_non_empty_mempool.kernel.outputs.len(),
1402
                "Transaction for block with non-empty mempool must contain {num_coinbase_outputs} outputs from coinbase, \
1403
                send output, and change output"
1404
            );
1405
            assert_eq!(
1406
                1,
1407
                transaction_non_empty_mempool.kernel.inputs.len(),
1408
                "Transaction for block with non-empty mempool must contain one input: the genesis UTXO being spent"
1409
            );
1410

1411
            // Build and verify block template
1412
            let block_1_nonempty_mempool = Block::compose(
1413
                &genesis_block,
1414
                transaction_non_empty_mempool,
1415
                now,
1416
                TritonVmJobQueue::get_instance(),
1417
                TritonVmJobPriority::default().into(),
1418
            )
1419
            .await
1420
            .unwrap();
1421
            assert!(
1422
                block_1_nonempty_mempool
1423
                    .is_valid(&genesis_block, now + Timestamp::seconds(2), network)
1424
                    .await,
1425
                "Block template created by miner with non-empty mempool must be valid"
1426
            );
1427

1428
            alice.lock_guard_mut().await.mempool_clear().await;
1429
        }
1430
    }
1431

1432
    #[traced_test]
1433
    #[apply(shared_tokio_runtime)]
1434
    async fn block_proposal_for_height_two_is_valid() {
1435
        // Verify that block proposals of both height 1 and 2 are valid.
1436
        let network = Network::Main;
1437

1438
        // force SingleProof capability.
1439
        let cli = cli_args::Args {
1440
            tx_proving_capability: Some(TxProvingCapability::SingleProof),
1441
            network,
1442
            ..Default::default()
1443
        };
1444

1445
        let mut alice = mock_genesis_global_state(2, WalletEntropy::devnet_wallet(), cli).await;
1446
        let genesis_block = Block::genesis(network);
1447
        let mocked_now = genesis_block.header().timestamp + Timestamp::months(7);
1448

1449
        assert!(
1450
            alice.lock_guard().await.mempool.is_empty(),
1451
            "Mempool must be empty at start of test"
1452
        );
1453
        let (sender_1, receiver_1) = oneshot::channel();
1454
        let (_cancel_compose_tx, cancel_compose_rx) = tokio::sync::watch::channel(());
1455
        compose_block(
1456
            genesis_block.clone(),
1457
            alice.clone(),
1458
            sender_1,
1459
            cancel_compose_rx.clone(),
1460
            mocked_now,
1461
        )
1462
        .await
1463
        .unwrap();
1464
        let (block_1, _) = receiver_1.await.unwrap();
1465
        assert!(block_1.is_valid(&genesis_block, mocked_now, network).await);
1466
        alice.set_new_tip(block_1.clone()).await.unwrap();
1467

1468
        let (sender_2, receiver_2) = oneshot::channel();
1469
        compose_block(
1470
            block_1.clone(),
1471
            alice.clone(),
1472
            sender_2,
1473
            cancel_compose_rx,
1474
            mocked_now,
1475
        )
1476
        .await
1477
        .unwrap();
1478
        let (block_2, _) = receiver_2.await.unwrap();
1479
        assert!(block_2.is_valid(&block_1, mocked_now, network).await);
1480
    }
1481

1482
    /// This test mines a single block at height 1 on the main network
1483
    /// and then validates it with `Block::is_valid()` and
1484
    /// `Block::has_proof_of_work()`.
1485
    ///
1486
    /// This is a regression test for issue #131.
1487
    /// https://github.com/Neptune-Crypto/neptune-core/issues/131
1488
    ///
1489
    /// The cause of the failure was that `mine_block_worker()` was comparing
1490
    /// hash(block_header) against difficulty threshold while
1491
    /// `Block::has_proof_of_work` uses hash(block) instead.
1492
    ///
1493
    /// The fix was to modify `mine_block_worker()` so that it also
1494
    /// uses hash(block) and subsequently the test passes (unmodified).
1495
    ///
1496
    /// This test is present and fails in commit
1497
    /// b093631fd0d479e6c2cc252b08f18d920a1ec2e5 which is prior to the fix.
1498
    #[traced_test]
1499
    #[apply(shared_tokio_runtime)]
1500
    async fn mined_block_has_proof_of_work() {
1501
        let network = Network::Main;
1502
        let cli_args = cli_args::Args {
1503
            guesser_fraction: 0.0,
1504
            network,
1505
            ..Default::default()
1506
        };
1507
        let global_state_lock =
1508
            mock_genesis_global_state(2, WalletEntropy::devnet_wallet(), cli_args).await;
1509
        let tip_block_orig = Block::genesis(network);
1510
        let launch_date = tip_block_orig.header().timestamp;
1511
        let (worker_task_tx, worker_task_rx) = oneshot::channel::<NewBlockFound>();
1512

1513
        let (transaction, _composer_utxo_info) = make_coinbase_transaction_from_state(
1514
            &tip_block_orig,
1515
            &global_state_lock,
1516
            launch_date,
1517
            global_state_lock
1518
                .cli()
1519
                .proof_job_options_primitive_witness(),
1520
        )
1521
        .await
1522
        .unwrap();
1523

1524
        let guesser_key = global_state_lock
1525
            .lock_guard()
1526
            .await
1527
            .wallet_state
1528
            .wallet_entropy
1529
            .guesser_spending_key(tip_block_orig.hash());
1530
        let mut block = Block::block_template_invalid_proof(
1531
            &tip_block_orig,
1532
            transaction,
1533
            launch_date,
1534
            network.target_block_interval(),
1535
        );
1536
        block.set_header_guesser_digest(guesser_key.after_image());
1537

1538
        let sleepy_guessing = false;
1539
        let num_guesser_threads = None;
1540

1541
        guess_worker(
1542
            network,
1543
            block,
1544
            tip_block_orig.header().to_owned(),
1545
            worker_task_tx,
1546
            guesser_key,
1547
            GuessingConfiguration {
1548
                sleepy_guessing,
1549
                num_guesser_threads,
1550
            },
1551
            Timestamp::now(),
1552
            None,
1553
        );
1554

1555
        let mined_block_info = worker_task_rx.await.unwrap();
1556

1557
        assert!(mined_block_info
1558
            .block
1559
            .has_proof_of_work(network, tip_block_orig.header()));
1560
    }
1561

1562
    /// This test mines a single block at height 1 on the main network
1563
    /// and then validates that the header timestamp has changed and
1564
    /// that it is within the 100 seconds (from now).
1565
    ///
1566
    /// This is a regression test for issue #149.
1567
    /// https://github.com/Neptune-Crypto/neptune-core/issues/149
1568
    ///
1569
    /// note: this test fails in 318b7a20baf11a7a99f249660f1f70484c586012
1570
    ///       and should always pass in later commits.
1571
    #[traced_test]
1572
    #[apply(shared_tokio_runtime)]
1573
    async fn block_timestamp_represents_time_guessing_started() -> Result<()> {
1574
        let network = Network::Main;
1575
        let cli_args = cli_args::Args {
1576
            guesser_fraction: 0.0,
1577
            network,
1578
            ..Default::default()
1579
        };
1580
        let global_state_lock =
1581
            mock_genesis_global_state(2, WalletEntropy::devnet_wallet(), cli_args).await;
1582
        let (worker_task_tx, worker_task_rx) = oneshot::channel::<NewBlockFound>();
1583

1584
        let tip_block_orig = global_state_lock
1585
            .lock_guard()
1586
            .await
1587
            .chain
1588
            .light_state()
1589
            .clone();
1590

1591
        let now = tip_block_orig.header().timestamp + Timestamp::minutes(10);
1592

1593
        // pretend/simulate that it takes at least 10 seconds to mine the block.
1594
        let ten_seconds_ago = now - Timestamp::seconds(10);
1595

1596
        let (transaction, _composer_utxo_info) = make_coinbase_transaction_from_state(
1597
            &tip_block_orig,
1598
            &global_state_lock,
1599
            ten_seconds_ago,
1600
            global_state_lock
1601
                .cli()
1602
                .proof_job_options_primitive_witness(),
1603
        )
1604
        .await
1605
        .unwrap();
1606

1607
        let guesser_key = HashLockKey::from_preimage(Digest::default());
1608

1609
        let template = Block::block_template_invalid_proof(
1610
            &tip_block_orig,
1611
            transaction,
1612
            ten_seconds_ago,
1613
            network.target_block_interval(),
1614
        );
1615

1616
        // sanity check that our initial state is correct.
1617
        let initial_header_timestamp = template.header().timestamp;
1618
        assert_eq!(ten_seconds_ago, initial_header_timestamp);
1619

1620
        let sleepy_guessing = false;
1621
        let num_guesser_threads = None;
1622

1623
        guess_worker(
1624
            network,
1625
            template,
1626
            tip_block_orig.header().to_owned(),
1627
            worker_task_tx,
1628
            guesser_key,
1629
            GuessingConfiguration {
1630
                sleepy_guessing,
1631
                num_guesser_threads,
1632
            },
1633
            Timestamp::now(),
1634
            None,
1635
        );
1636

1637
        let mined_block_info = worker_task_rx.await.unwrap();
1638

1639
        let block_timestamp = mined_block_info.block.kernel.header.timestamp;
1640

1641
        // Mining updates the timestamp. So block timestamp will be >= to what
1642
        // was set in the block template, and <= current time.
1643
        assert!(block_timestamp >= initial_header_timestamp);
1644
        assert!(block_timestamp <= Timestamp::now());
1645

1646
        // verify timestamp is within the last 100 seconds (allows for some CI slack).
1647
        assert!(Timestamp::now() - block_timestamp < Timestamp::seconds(100));
1648

1649
        Ok(())
1650
    }
1651

1652
    /// Test the difficulty adjustment algorithm.
1653
    ///
1654
    /// Specifically, verify that the observed concrete block interval when mining
1655
    /// tracks the target block interval, assuming:
1656
    ///  - No time is spent proving
1657
    ///  - Constant mining power
1658
    ///  - Mining power exceeds lower bound (hashing once every target interval).
1659
    ///
1660
    /// Note that the second assumption is broken when running the entire test suite.
1661
    /// So if this test fails when all others pass, it is not necessarily a cause
1662
    /// for worry.
1663
    ///
1664
    /// We mine ten blocks with a target block interval of 1 second, so all
1665
    /// blocks should be mined in approx 10 seconds.
1666
    ///
1667
    /// We set a test time limit of 3x the expected time, ie 30 seconds, and
1668
    /// panic if mining all blocks takes longer than that.
1669
    ///
1670
    /// We also assert upper and lower bounds for variance from the expected 10
1671
    /// seconds.  The variance limit is 1.3, so the upper bound is 13 seconds
1672
    /// and the lower bound is 7692ms.
1673
    ///
1674
    /// We ignore the first 2 blocks after genesis because they are typically
1675
    /// mined very fast.
1676
    ///
1677
    /// We avoid sleepy guessing to avoid complications from the
1678
    /// sleep(100 millis) call in mining loop when restricted mining is enabled.
1679
    ///
1680
    /// This serves as a regression test for issue #154.
1681
    /// https://github.com/Neptune-Crypto/neptune-core/issues/154
1682
    async fn mine_m_blocks_in_n_seconds<const NUM_BLOCKS: usize, const NUM_SECONDS: usize>(
1683
    ) -> Result<()> {
1684
        let network = Network::RegTest;
1685
        let global_state_lock = mock_genesis_global_state(
1686
            2,
1687
            WalletEntropy::devnet_wallet(),
1688
            cli_args::Args::default_with_network(network),
1689
        )
1690
        .await;
1691

1692
        let mut prev_block = global_state_lock
1693
            .lock_guard()
1694
            .await
1695
            .chain
1696
            .light_state()
1697
            .clone();
1698

1699
        // adjust these to simulate longer mining runs, possibly
1700
        // with shorter or longer target intervals.
1701
        // expected_duration = num_blocks * target_block_interval
1702
        let target_block_interval =
1703
            Timestamp::millis((1000.0 * (NUM_SECONDS as f64) / (NUM_BLOCKS as f64)).round() as u64);
1704
        println!(
1705
            "target block interval: {} ms",
1706
            target_block_interval.0.value()
1707
        );
1708

1709
        // set initial difficulty in accordance with own hash rate
1710
        let sleepy_guessing = false;
1711
        let num_guesser_threads = None;
1712
        let num_outputs = 0;
1713
        let hash_rate =
1714
            estimate_own_hash_rate(target_block_interval, sleepy_guessing, num_outputs).await;
1715
        println!("estimating hash rate at {} per millisecond", hash_rate);
1716
        let prepare_time = estimate_block_preparation_time_invalid_proof().await;
1717
        println!("estimating block preparation time at {prepare_time} ms");
1718
        if 1.5 * prepare_time > target_block_interval.0.value() as f64 {
1719
            println!(
1720
                "Cannot perform meaningful test! Block preparation time \
1721
            too large for target block interval."
1722
            );
1723
            return Ok(());
1724
        }
1725

1726
        let guessing_time = (target_block_interval.to_millis() as f64) - prepare_time;
1727
        let initial_difficulty = BigUint::from((hash_rate * guessing_time) as u128);
1728
        println!("initial difficulty: {}", initial_difficulty);
1729
        prev_block.set_header_timestamp_and_difficulty(
1730
            prev_block.header().timestamp,
1731
            Difficulty::from_biguint(initial_difficulty),
1732
        );
1733

1734
        let expected_duration = target_block_interval * NUM_BLOCKS;
1735
        let stddev = (guessing_time.pow(2.0_f64) / (NUM_BLOCKS as f64)).sqrt();
1736
        let allowed_standard_deviations = 4;
1737
        let min_duration = (expected_duration.0.value() as f64)
1738
            - f64::from(allowed_standard_deviations) * stddev * (NUM_BLOCKS as f64);
1739
        let max_duration = (expected_duration.0.value() as f64)
1740
            + f64::from(allowed_standard_deviations) * stddev * (NUM_BLOCKS as f64);
1741
        let max_test_time = expected_duration * 3;
1742

1743
        // we ignore the first 2 blocks after genesis because they are
1744
        // typically mined very fast.
1745
        let ignore_first_n_blocks = 2;
1746

1747
        let mut durations = Vec::with_capacity(NUM_BLOCKS);
1748
        let mut start_instant = std::time::SystemTime::now();
1749

1750
        for i in 0..NUM_BLOCKS + ignore_first_n_blocks {
1751
            if i <= ignore_first_n_blocks {
1752
                start_instant = std::time::SystemTime::now();
1753
            }
1754

1755
            let start_time = Timestamp::now();
1756
            let start_st = std::time::SystemTime::now();
1757

1758
            let transaction = make_mock_transaction_with_mutator_set_hash(
1759
                vec![],
1760
                vec![],
1761
                prev_block.mutator_set_accumulator_after().hash(),
1762
            );
1763

1764
            let guesser_key = HashLockKey::from_preimage(Digest::default());
1765

1766
            let block = Block::block_template_invalid_proof(
1767
                &prev_block,
1768
                transaction,
1769
                start_time,
1770
                target_block_interval,
1771
            );
1772

1773
            let (worker_task_tx, worker_task_rx) = oneshot::channel::<NewBlockFound>();
1774
            let height = block.header().height;
1775

1776
            guess_worker(
1777
                network,
1778
                block,
1779
                *prev_block.header(),
1780
                worker_task_tx,
1781
                guesser_key,
1782
                GuessingConfiguration {
1783
                    sleepy_guessing,
1784
                    num_guesser_threads,
1785
                },
1786
                Timestamp::now(),
1787
                Some(target_block_interval),
1788
            );
1789

1790
            let mined_block_info = worker_task_rx.await.unwrap();
1791

1792
            // note: this assertion often fails prior to fix for #154.
1793
            assert!(mined_block_info
1794
                .block
1795
                .has_proof_of_work(network, prev_block.header()));
1796

1797
            prev_block = *mined_block_info.block;
1798

1799
            let block_time = start_st.elapsed()?.as_millis();
1800
            println!(
1801
                "Found block {} in {block_time} milliseconds; \
1802
                difficulty was {}; total time elapsed so far: {} ms",
1803
                height,
1804
                BigUint::from(prev_block.header().difficulty),
1805
                start_instant.elapsed()?.as_millis()
1806
            );
1807
            if i > ignore_first_n_blocks {
1808
                durations.push(block_time as f64);
1809
            }
1810

1811
            let elapsed = start_instant.elapsed()?.as_millis();
1812
            assert!(
1813
                elapsed <= max_test_time.0.value().into(),
1814
                "test time limit exceeded. \
1815
                 expected_duration: {expected_duration}, limit: {max_test_time}, actual: {elapsed}"
1816
            );
1817
        }
1818

1819
        let actual_duration = start_instant.elapsed()?.as_millis() as u64;
1820

1821
        println!(
1822
            "actual duration: {actual_duration}\n\
1823
        expected duration: {expected_duration}\n\
1824
        min_duration: {min_duration}\n\
1825
        max_duration: {max_duration}\n\
1826
        allowed deviation: {allowed_standard_deviations}"
1827
        );
1828
        println!(
1829
            "average block time: {} whereas target: {}",
1830
            durations.into_iter().sum::<f64>() / (NUM_BLOCKS as f64),
1831
            target_block_interval
1832
        );
1833

1834
        assert!((actual_duration as f64) > min_duration);
1835
        assert!((actual_duration as f64) < max_duration);
1836

1837
        Ok(())
1838
    }
1839

1840
    #[test]
1841
    fn fast_kernel_mast_hash_agrees_with_mast_hash_function_invalid_block() {
1842
        let network = Network::Main;
1843
        let genesis = Block::genesis(network);
1844
        let block1 = invalid_empty_block(network, &genesis);
1845
        for block in [genesis, block1] {
1846
            let (kernel_auth_path, header_auth_path) = precalculate_block_auth_paths(&block);
1847
            assert_eq!(
1848
                block.kernel.mast_hash(),
1849
                fast_kernel_mast_hash(kernel_auth_path, header_auth_path, block.header().nonce)
1850
            );
1851
        }
1852
    }
1853

1854
    #[test]
1855
    fn fast_kernel_mast_hash_agrees_with_mast_hash_function_valid_block() {
1856
        let block_primitive_witness = deterministic_block_primitive_witness();
1857
        let a_block = block_primitive_witness.predecessor_block();
1858
        let (kernel_auth_path, header_auth_path) = precalculate_block_auth_paths(a_block);
1859
        assert_eq!(
1860
            a_block.kernel.mast_hash(),
1861
            fast_kernel_mast_hash(kernel_auth_path, header_auth_path, a_block.header().nonce)
1862
        );
1863
    }
1864

1865
    #[traced_test]
1866
    #[apply(shared_tokio_runtime)]
1867
    async fn mine_20_blocks_in_40_seconds() -> Result<()> {
1868
        mine_m_blocks_in_n_seconds::<20, 40>().await.unwrap();
1869
        Ok(())
1870
    }
1871

1872
    #[traced_test]
1873
    #[apply(shared_tokio_runtime)]
1874
    async fn hash_rate_independent_of_tx_size() {
1875
        let network = Network::Main;
1876

1877
        // It's crucial that the hash rate is independent of the size of the
1878
        // block, since miners are otherwise heavily incentivized to mine small
1879
        // or empty blocks.
1880
        let sleepy_guessing = false;
1881
        let hash_rate_empty_tx =
1882
            estimate_own_hash_rate(network.target_block_interval(), sleepy_guessing, 0).await;
1883
        println!("hash_rate_empty_tx: {hash_rate_empty_tx}");
1884

1885
        let hash_rate_big_tx =
1886
            estimate_own_hash_rate(network.target_block_interval(), sleepy_guessing, 10000).await;
1887
        println!("hash_rate_big_tx: {hash_rate_big_tx}");
1888

1889
        assert!(
1890
            hash_rate_empty_tx * 1.1 > hash_rate_big_tx
1891
                && hash_rate_empty_tx * 0.9 < hash_rate_big_tx,
1892
            "Hash rate for big and small block must be within 10 %"
1893
        );
1894
    }
1895

1896
    #[traced_test]
1897
    #[apply(shared_tokio_runtime)]
1898
    async fn coinbase_transaction_has_one_timelocked_and_one_liquid_output() {
1899
        for notification_policy in [
1900
            FeeNotificationPolicy::OffChain,
1901
            FeeNotificationPolicy::OnChainGeneration,
1902
            FeeNotificationPolicy::OnChainSymmetric,
1903
        ] {
1904
            let network = Network::Main;
1905
            let cli_args = cli_args::Args {
1906
                guesser_fraction: 0.0,
1907
                fee_notification: notification_policy,
1908
                network,
1909
                ..Default::default()
1910
            };
1911
            let global_state_lock =
1912
                mock_genesis_global_state(2, WalletEntropy::devnet_wallet(), cli_args).await;
1913
            let genesis_block = Block::genesis(network);
1914
            let launch_date = genesis_block.header().timestamp;
1915

1916
            let (transaction, coinbase_utxo_info) = make_coinbase_transaction_from_state(
1917
                &genesis_block,
1918
                &global_state_lock,
1919
                launch_date,
1920
                global_state_lock
1921
                    .cli()
1922
                    .proof_job_options_primitive_witness(),
1923
            )
1924
            .await
1925
            .unwrap();
1926

1927
            let expected_number_of_expected_utxos = match notification_policy {
1928
                FeeNotificationPolicy::OffChain => 2,
1929
                FeeNotificationPolicy::OnChainSymmetric
1930
                | FeeNotificationPolicy::OnChainGeneration => 0,
1931
            };
1932
            assert_eq!(
1933
                2,
1934
                transaction.kernel.outputs.len(),
1935
                "Expected two outputs in coinbase tx"
1936
            );
1937
            assert_eq!(
1938
                expected_number_of_expected_utxos,
1939
                coinbase_utxo_info.len(),
1940
                "Expected {expected_number_of_expected_utxos} expected UTXOs for composer."
1941
            );
1942

1943
            if notification_policy == FeeNotificationPolicy::OffChain {
1944
                assert!(
1945
                    coinbase_utxo_info
1946
                        .iter()
1947
                        .filter(|x| x.utxo.release_date().is_some())
1948
                        .count()
1949
                        .is_one(),
1950
                    "Expected one timelocked coinbase UTXO"
1951
                );
1952
                assert!(
1953
                    coinbase_utxo_info
1954
                        .iter()
1955
                        .filter(|x| x.utxo.release_date().is_none())
1956
                        .count()
1957
                        .is_one(),
1958
                    "Expected one liquid coinbase UTXO"
1959
                );
1960
            } else {
1961
                let announced_outputs = global_state_lock
1962
                    .lock_guard()
1963
                    .await
1964
                    .wallet_state
1965
                    .scan_for_utxos_announced_to_known_keys(&transaction.kernel)
1966
                    .collect_vec();
1967
                assert_eq!(2, announced_outputs.len());
1968
                assert_eq!(
1969
                    1,
1970
                    announced_outputs
1971
                        .iter()
1972
                        .filter(|x| x.utxo.release_date().is_some())
1973
                        .count()
1974
                );
1975
                assert_eq!(
1976
                    1,
1977
                    announced_outputs
1978
                        .iter()
1979
                        .filter(|x| x.utxo.release_date().is_none())
1980
                        .count()
1981
                );
1982
            }
1983
        }
1984
    }
1985

1986
    #[test]
1987
    fn composer_outputs_has_length_zero_if_guesser_fraction_is_1() {
1988
        let mut rng = rand::rng();
1989
        let address = SymmetricKey::from_seed(rng.random()).into();
1990
        let composer_parameters = ComposerParameters::new(
1991
            address,
1992
            rng.random(),
1993
            None,
1994
            1.0,
1995
            FeeNotificationPolicy::OffChain,
1996
        );
1997
        let composer_outputs = composer_outputs(
1998
            NativeCurrencyAmount::coins(1),
1999
            composer_parameters,
2000
            Timestamp::now(),
2001
        );
2002
        assert!(composer_outputs.is_empty());
2003
    }
2004

2005
    #[test]
2006
    fn composer_outputs_has_length_two_if_guesser_fraction_is_between_0_and_1() {
2007
        let mut rng = rand::rng();
2008
        let address = SymmetricKey::from_seed(rng.random()).into();
2009
        let guesser_fraction = rng.random_range(0f64..=0.99999f64);
2010
        let composer_parameters = ComposerParameters::new(
2011
            address,
2012
            rng.random(),
2013
            None,
2014
            guesser_fraction,
2015
            FeeNotificationPolicy::OffChain,
2016
        );
2017
        let composer_outputs = composer_outputs(
2018
            NativeCurrencyAmount::coins(1),
2019
            composer_parameters,
2020
            Timestamp::now(),
2021
        );
2022
        assert_eq!(2, composer_outputs.len());
2023
    }
2024

2025
    #[traced_test]
2026
    #[tokio::test]
2027
    async fn coinbase_tx_has_two_outputs_or_zero_outputs() {
2028
        for guesser_fraction in [0.6, 1.0] {
2029
            for notification_policy in [
2030
                FeeNotificationPolicy::OffChain,
2031
                FeeNotificationPolicy::OnChainGeneration,
2032
                FeeNotificationPolicy::OnChainSymmetric,
2033
            ] {
2034
                let network = Network::Main;
2035
                let cli_args = cli_args::Args {
2036
                    guesser_fraction,
2037
                    fee_notification: notification_policy,
2038
                    network,
2039
                    ..Default::default()
2040
                };
2041
                let global_state_lock =
2042
                    mock_genesis_global_state(2, WalletEntropy::devnet_wallet(), cli_args.clone())
2043
                        .await;
2044
                let genesis_block = Block::genesis(cli_args.network);
2045
                let launch_date = genesis_block.header().timestamp;
2046

2047
                let (transaction, expected_utxos) = make_coinbase_transaction_from_state(
2048
                    &genesis_block,
2049
                    &global_state_lock,
2050
                    launch_date,
2051
                    global_state_lock
2052
                        .cli()
2053
                        .proof_job_options_primitive_witness(),
2054
                )
2055
                .await
2056
                .unwrap();
2057

2058
                // Verify zero outputs if guesser gets it all, otherwise two outputs.
2059
                let num_expected_outputs = if guesser_fraction == 1.0 { 0 } else { 2 };
2060
                assert_eq!(num_expected_outputs, transaction.kernel.outputs.len());
2061

2062
                // Verify that the public notifications/expected UTXOs match
2063
                if notification_policy == FeeNotificationPolicy::OffChain {
2064
                    assert_eq!(num_expected_outputs, expected_utxos.len());
2065
                    assert!(transaction.kernel.public_announcements.is_empty());
2066
                } else {
2067
                    assert!(expected_utxos.is_empty());
2068
                    assert_eq!(
2069
                        num_expected_outputs,
2070
                        transaction.kernel.public_announcements.len()
2071
                    );
2072
                }
2073
            }
2074
        }
2075
    }
2076

2077
    #[test]
2078
    fn block_hash_relates_to_predecessor_difficulty() {
2079
        let network = Network::Main;
2080
        let difficulty = 100u32;
2081
        // Difficulty X means we expect X trials before success.
2082
        // Modeling the process as a geometric distribution gives the
2083
        // probability of success in a single trial, p = 1/X.
2084
        // Then the probability of seeing k failures is (1-1/X)^k.
2085
        // We want this to be five nines certain that we do get a success
2086
        // after k trials, so this quantity must be less than 0.0001.
2087
        // So: log_10 0.0001 = -4 > log_10 (1-1/X)^k = k * log_10 (1 - 1/X).
2088
        // Difficulty 100 sets k = 917.
2089
        let cofactor = (1.0 - (1.0 / f64::from(difficulty))).log10();
2090
        let k = (-4.0 / cofactor).ceil() as usize;
2091

2092
        let mut predecessor_header = random_block_header();
2093
        predecessor_header.difficulty = Difficulty::from(difficulty);
2094
        let predecessor_body = BlockBody::new(
2095
            random_transaction_kernel(),
2096
            random_mutator_set_accumulator(),
2097
            random_mmra(),
2098
            random_mmra(),
2099
        );
2100
        let appendix = BlockAppendix::default();
2101
        let predecessor_block = Block::new(
2102
            predecessor_header,
2103
            predecessor_body,
2104
            appendix.clone(),
2105
            BlockProof::Invalid,
2106
        );
2107

2108
        let mut successor_header = random_block_header();
2109
        successor_header.prev_block_digest = predecessor_block.hash();
2110
        // note that successor's difficulty is random
2111
        let successor_body = BlockBody::new(
2112
            random_transaction_kernel(),
2113
            random_mutator_set_accumulator(),
2114
            random_mmra(),
2115
            random_mmra(),
2116
        );
2117

2118
        let mut rng = rand::rng();
2119
        let mut counter = 0;
2120
        let mut successor_block = Block::new(
2121
            successor_header,
2122
            successor_body.clone(),
2123
            appendix,
2124
            BlockProof::Invalid,
2125
        );
2126
        loop {
2127
            successor_block.set_header_nonce(rng.random());
2128

2129
            if successor_block.has_proof_of_work(network, predecessor_block.header()) {
2130
                break;
2131
            }
2132

2133
            counter += 1;
2134

2135
            assert!(
2136
                counter < k,
2137
                "number of hash trials before finding valid pow exceeds statistical limit"
2138
            )
2139
        }
2140
    }
2141

2142
    // tests that a job cancel message cancels composing and results in JobCancelled error
2143
    //
2144
    // This test spawns a task that executes create_block_transaction_from()
2145
    // and then sends a job cancellation message to that task.
2146
    //
2147
    // It verifies that the task ends and the result is a JobCancelled error.
2148
    #[traced_test]
2149
    #[apply(shared_tokio_runtime)]
2150
    async fn job_cancel_msg_cancels_composing() -> anyhow::Result<()> {
2151
        let network = Network::Main;
2152
        let cli_args = cli_args::Args {
2153
            compose: true,
2154
            network,
2155
            ..Default::default()
2156
        };
2157
        let global_state_lock =
2158
            mock_genesis_global_state(2, WalletEntropy::devnet_wallet(), cli_args.clone()).await;
2159

2160
        let (cancel_job_tx, cancel_job_rx) = tokio::sync::watch::channel(());
2161

2162
        let compose_task = async move {
2163
            let genesis_block = Block::genesis(network);
2164
            let gsl = global_state_lock.clone();
2165
            let cli = &cli_args;
2166
            let mut job_options: TritonVmProofJobOptions = cli.into();
2167
            job_options.cancel_job_rx = Some(cancel_job_rx);
2168
            create_block_transaction_from(
2169
                &genesis_block,
2170
                &gsl,
2171
                Timestamp::now(),
2172
                job_options,
2173
                TxMergeOrigin::Mempool,
2174
            )
2175
            .await
2176
        };
2177

2178
        // start the task running
2179
        let jh = tokio::task::spawn(compose_task);
2180

2181
        // wait a little while for a job to get added to the queue.
2182
        tokio::time::sleep(std::time::Duration::from_secs(3)).await;
2183

2184
        // now cancel the job and wait for the task to complete (after job cancellation)
2185
        cancel_job_tx.send(()).unwrap();
2186
        let job_result = jh.await?;
2187

2188
        // we must receive an error
2189
        let error = job_result.unwrap_err();
2190

2191
        println!("error: {}", error);
2192

2193
        // the error must indicate the job was cancelled.
2194
        let job_cancelled = matches!(
2195
            error.root_cause().downcast_ref::<CreateProofError>(),
2196
            Some(CreateProofError::JobHandleError(
2197
                JobHandleError::JobCancelled
2198
            ))
2199
        );
2200

2201
        assert!(job_cancelled);
2202

2203
        Ok(())
2204
    }
2205

2206
    // tests that Stop/Start mining messages work as expected while composing.
2207
    //
2208
    // This test spawns task that executes the mining loop ie mine().
2209
    // and then sends StopMining, StartMining messages to the task.
2210
    //
2211
    // The StopMining message causes a job-cancelation message to be sent to
2212
    // prove_concensus_program() which forwards to to proving job.
2213
    //
2214
    // The result is that the composer_task terminates early with a JobCancelled error and for
2215
    // correct behavior, that error must not cause the mining loop to shut-down.
2216
    //
2217
    // The test verifies that the mining status actually changes after each message is
2218
    // sent and that the mining loop continues processing.
2219
    #[traced_test]
2220
    #[apply(shared_tokio_runtime)]
2221
    async fn msg_from_main_does_not_crash_composer() -> anyhow::Result<()> {
2222
        let network = Network::Main;
2223
        let cli_args = cli_args::Args {
2224
            compose: true,
2225
            network,
2226
            ..Default::default()
2227
        };
2228
        let global_state_lock =
2229
            mock_genesis_global_state(2, WalletEntropy::devnet_wallet(), cli_args).await;
2230

2231
        let (miner_to_main_tx, _miner_to_main_rx) =
2232
            mpsc::channel::<MinerToMain>(MINER_CHANNEL_CAPACITY);
2233
        let (main_to_miner_tx, main_to_miner_rx) =
2234
            mpsc::channel::<MainToMiner>(MINER_CHANNEL_CAPACITY);
2235

2236
        // create a task that for the mining-loop
2237
        let mine_task = mine(
2238
            main_to_miner_rx,
2239
            miner_to_main_tx,
2240
            global_state_lock.clone(),
2241
        );
2242

2243
        // spawn the mining-loop task.
2244
        let jh = tokio::task::spawn(mine_task);
2245

2246
        let timeout = std::time::Duration::from_secs(5);
2247

2248
        // wait until mining-status is Composing.
2249
        // we should have a proving job in queue
2250
        let gsl = global_state_lock.clone();
2251
        wait_until(timeout, move || {
2252
            let gsl = gsl.clone();
2253
            async move {
2254
                matches!(
2255
                    gsl.lock_guard().await.mining_state.mining_status,
2256
                    MiningStatus::Composing(_)
2257
                )
2258
            }
2259
        })
2260
        .await?;
2261

2262
        // send StopMining message to the mining loop
2263
        main_to_miner_tx.send(MainToMiner::StopMining).await?;
2264

2265
        // wait until mining status is inactive.
2266
        // job should have been cancelled and removed from queue, but we have no way to verify
2267
        let gsl2 = global_state_lock.clone();
2268
        wait_until(timeout, move || {
2269
            let gsl2 = gsl2.clone();
2270
            async move {
2271
                matches!(
2272
                    gsl2.lock_guard().await.mining_state.mining_status,
2273
                    MiningStatus::Inactive
2274
                )
2275
            }
2276
        })
2277
        .await?;
2278

2279
        // send StartMining message to the mining loop
2280
        main_to_miner_tx.send(MainToMiner::StartMining).await?;
2281

2282
        // wait until mining-status is Composing again.
2283
        // there should be a proving job in queue again
2284
        let gsl3 = global_state_lock.clone();
2285
        wait_until(timeout, move || {
2286
            let gsl3 = gsl3.clone();
2287
            async move {
2288
                matches!(
2289
                    gsl3.lock_guard().await.mining_state.mining_status,
2290
                    MiningStatus::Composing(_)
2291
                )
2292
            }
2293
        })
2294
        .await?;
2295

2296
        // wait a bit longer for mine-loop processing.
2297
        tokio::time::sleep(timeout).await;
2298

2299
        // ensure mine-loop is still up and running.
2300
        assert!(!main_to_miner_tx.is_closed());
2301
        assert!(!jh.is_finished());
2302

2303
        // abort the mining task, so we can ensure that cancels the job also.
2304
        jh.abort();
2305
        let _ = jh.await;
2306

2307
        // ensure mine-loop is gone.
2308
        assert!(main_to_miner_tx.is_closed());
2309

2310
        Ok(())
2311
    }
2312

2313
    /// A test for difficulty reset logic, which occurs for the TestnetMock
2314
    /// network.
2315
    ///
2316
    /// note: or any future network that returns a Some value from
2317
    /// Network::difficulty_reset_interval()
2318
    ///
2319
    /// The test performs guessing (via guess_worker()) for 20 blocks
2320
    /// and simulates a random block interval between (most) blocks.
2321
    ///
2322
    /// Heights 1, 5, 10, and 15 use a block interval that is >= to
2323
    /// Network::difficulty_reset_interval(), which triggers a difficulty
2324
    /// reset within guess_worker(), under test.
2325
    ///
2326
    /// For each mined block:
2327
    ///  + asserts Block::has_proof_of_work() is true
2328
    ///  + asserts Block::validate().is_ok() is true
2329
    ///
2330
    /// For each block with difficulty reset:
2331
    ///  + asserts block difficulty matches Network::genesis_difficulty()
2332
    ///
2333
    /// For each normal block (without difficulty reset):
2334
    ///  + asserts block difficulty does NOT match Network::genesis_difficulty()
2335
    #[traced_test]
2336
    #[apply(shared_tokio_runtime)]
2337
    async fn testnet_mock_reset_difficulty() -> anyhow::Result<()> {
2338
        // we are testing the TestnetMock network.
2339
        let network = Network::TestnetMock;
2340

2341
        // basic setup
2342
        let mut rng = rand::rng();
2343
        let sleepy_guessing = false;
2344
        let num_guesser_threads = None;
2345
        let num_blocks = 20; // generate 20 blocks
2346
        let mut block_time = Timestamp::now();
2347

2348
        // obtain global state
2349
        let global_state_lock = mock_genesis_global_state(
2350
            2,
2351
            WalletEntropy::devnet_wallet(),
2352
            cli_args::Args::default_with_network(network),
2353
        )
2354
        .await;
2355

2356
        // obtain previous (genesis) block
2357
        let mut prev_block = global_state_lock
2358
            .lock_guard()
2359
            .await
2360
            .chain
2361
            .light_state()
2362
            .clone();
2363

2364
        // generate 20 blocks
2365
        for i in 1..=num_blocks {
2366
            // we simulate a block_interval since doing this in real-time would
2367
            // be too slow.
2368

2369
            // height 1 resets because interval since genesis block is large.
2370
            // 5,10,15 we choose arbitrarily.
2371
            let reset_interval = network.difficulty_reset_interval().unwrap();
2372
            let (block_interval, should_reset) = if [1, 5, 10, 15].contains(&i) {
2373
                (reset_interval, true)
2374
            } else {
2375
                // generate random interval between min_block_time and difficulty_reset_interval.
2376
                // this encourages difficulty_control() to modify the difficulty.
2377
                let interval_millis = rng.random_range(
2378
                    network.minimum_block_time().to_millis()..reset_interval.to_millis(),
2379
                );
2380
                (Timestamp::millis(interval_millis), false)
2381
            };
2382
            block_time += block_interval;
2383

2384
            // create tx
2385
            let transaction = make_mock_transaction_with_mutator_set_hash(
2386
                vec![],
2387
                vec![],
2388
                prev_block.mutator_set_accumulator_after().hash(),
2389
            );
2390

2391
            // gen guesser key
2392
            let guesser_key = HashLockKey::from_preimage(Digest::default());
2393

2394
            // generate a block template / proposal
2395
            let block_template = MockBlockGenerator::mock_block_from_tx_without_pow(
2396
                prev_block.clone(),
2397
                transaction,
2398
                guesser_key,
2399
            );
2400

2401
            // create channel to listen for guessing results.
2402
            let (worker_task_tx, worker_task_rx) = oneshot::channel::<NewBlockFound>();
2403

2404
            // perform the guessing.
2405
            guess_worker(
2406
                network,
2407
                block_template,
2408
                *prev_block.header(),
2409
                worker_task_tx,
2410
                guesser_key,
2411
                GuessingConfiguration {
2412
                    sleepy_guessing,
2413
                    num_guesser_threads,
2414
                },
2415
                block_time,
2416
                None,
2417
            );
2418

2419
            // await a mined block
2420
            let mined_block_info = worker_task_rx.await.unwrap();
2421
            let block = *mined_block_info.block;
2422

2423
            // verify mined block has proof-of-work
2424
            assert!(block.has_proof_of_work(network, prev_block.header()));
2425

2426
            // verify mined block validates
2427
            assert!(block
2428
                .validate(&prev_block, block_time, network)
2429
                .await
2430
                .is_ok());
2431

2432
            if should_reset {
2433
                // verify difficulty matches genesis difficulty
2434
                assert_eq!(block.header().difficulty, network.genesis_difficulty());
2435
            } else {
2436
                // verify difficulty does NOT match genesis difficulty
2437
                assert_ne!(block.header().difficulty, network.genesis_difficulty());
2438
            }
2439

2440
            prev_block = block;
2441
        }
2442

2443
        Ok(())
2444
    }
2445
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc