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

Neptune-Crypto / neptune-core / 14703155990

28 Apr 2025 07:55AM UTC coverage: 79.634% (-0.09%) from 79.726%
14703155990

push

github

dan-da
test: fix shared_tokio_runtime macro

addresses review comments:
https://github.com/Neptune-Crypto/neptune-core/pull/569#discussion_r2062924684
https://github.com/Neptune-Crypto/neptune-core/pull/569#discussion_r2062938756

1. adds back a #[traced_test] that got removed in efe4da2f.

2. adds missing visibility specifier to async test fn inside macro.

The missing specifier didn't cause any error or warning during
compilation or linting, but it is clearly more correct to have it in
place.

1 of 2 new or added lines in 2 files covered. (50.0%)

46 existing lines in 3 files now uncovered.

37143 of 46642 relevant lines covered (79.63%)

232101.38 hits per line

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

50.37
/src/mine_loop.rs
1
pub(crate) mod composer_parameters;
2

3
use std::cmp::max;
4
use std::sync::Arc;
5
use std::time::Duration;
6

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

31
use crate::api::export::TxInputList;
32
use crate::api::tx_initiation::builder::transaction_builder::TransactionBuilder;
33
use crate::api::tx_initiation::builder::transaction_proof_builder::TransactionProofBuilder;
34
use crate::api::tx_initiation::builder::triton_vm_proof_job_options_builder::TritonVmProofJobOptionsBuilder;
35
use crate::config_models::network::Network;
36
use crate::job_queue::triton_vm::vm_job_queue;
37
use crate::job_queue::triton_vm::TritonVmJobPriority;
38
use crate::job_queue::triton_vm::TritonVmJobQueue;
39
use crate::models::blockchain::block::block_height::BlockHeight;
40
use crate::models::blockchain::block::block_kernel::BlockKernel;
41
use crate::models::blockchain::block::block_kernel::BlockKernelField;
42
use crate::models::blockchain::block::difficulty_control::difficulty_control;
43
use crate::models::blockchain::block::*;
44
use crate::models::blockchain::transaction::transaction_proof::TransactionProofType;
45
use crate::models::blockchain::transaction::*;
46
use crate::models::blockchain::type_scripts::native_currency_amount::NativeCurrencyAmount;
47
use crate::models::channel::*;
48
use crate::models::proof_abstractions::mast_hash::MastHash;
49
use crate::models::proof_abstractions::tasm::program::TritonVmProofJobOptions;
50
use crate::models::proof_abstractions::tasm::prover_job;
51
use crate::models::proof_abstractions::timestamp::Timestamp;
52
use crate::models::shared::MAX_NUM_TXS_TO_MERGE;
53
use crate::models::shared::SIZE_20MB_IN_BYTES;
54
use crate::models::state::mining_status::MiningStatus;
55
use crate::models::state::transaction_details::TransactionDetails;
56
use crate::models::state::wallet::address::hash_lock_key::HashLockKey;
57
use crate::models::state::wallet::expected_utxo::ExpectedUtxo;
58
use crate::models::state::wallet::transaction_output::TxOutput;
59
use crate::models::state::wallet::transaction_output::TxOutputList;
60
use crate::models::state::GlobalStateLock;
61
use crate::prelude::twenty_first;
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(
2✔
72
    latest_block: Block,
2✔
73
    global_state_lock: GlobalStateLock,
2✔
74
    sender: oneshot::Sender<(Block, Vec<ExpectedUtxo>)>,
2✔
75
    cancel_compose_rx: tokio::sync::watch::Receiver<()>,
2✔
76
    now: Timestamp,
2✔
77
) -> Result<()> {
2✔
78
    let timestamp = max(now, latest_block.header().timestamp + MINIMUM_BLOCK_TIME);
2✔
79

2✔
80
    let mut job_options = global_state_lock
2✔
81
        .cli()
2✔
82
        .proof_job_options(TritonVmJobPriority::High);
2✔
83
    job_options.cancel_job_rx = Some(cancel_compose_rx);
2✔
84

85
    let (transaction, composer_utxos) = create_block_transaction(
2✔
86
        &latest_block,
2✔
87
        &global_state_lock,
2✔
88
        timestamp,
2✔
89
        job_options.clone(),
2✔
90
    )
2✔
91
    .await?;
2✔
92

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

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

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

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

150
/// Return MAST nodes from which the block header MAST hash is calculated,
151
/// given a variable nonce.
152
fn precalculate_header_ap(
21✔
153
    block_header_template: &BlockHeader,
21✔
154
) -> [Digest; BlockHeader::MAST_HEIGHT] {
21✔
155
    let header_mt = block_header_template.merkle_tree();
21✔
156

21✔
157
    header_mt
21✔
158
        .authentication_structure(&[BlockHeaderField::Nonce as usize])
21✔
159
        .unwrap()
21✔
160
        .try_into()
21✔
161
        .unwrap()
21✔
162
}
21✔
163

164
/// Return MAST nodes from which the block kernel MAST hash is calculated,
165
/// given a variable header.
166
fn precalculate_kernel_ap(block_kernel: &BlockKernel) -> [Digest; BlockKernel::MAST_HEIGHT] {
21✔
167
    let block_mt = block_kernel.merkle_tree();
21✔
168

21✔
169
    block_mt
21✔
170
        .authentication_structure(&[BlockKernelField::Header as usize])
21✔
171
        .unwrap()
21✔
172
        .try_into()
21✔
173
        .unwrap()
21✔
174
}
21✔
175

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

21✔
190
    (kernel_ap, header_ap)
21✔
191
}
21✔
192

193
/// Guess the nonce in parallel until success.
194
fn guess_worker(
3✔
195
    mut block: Block,
3✔
196
    previous_block_header: BlockHeader,
3✔
197
    sender: oneshot::Sender<NewBlockFound>,
3✔
198
    guesser_key: HashLockKey,
3✔
199
    guessing_configuration: GuessingConfiguration,
3✔
200
    target_block_interval: Option<Timestamp>,
3✔
201
) {
3✔
202
    let GuessingConfiguration {
3✔
203
        sleepy_guessing,
3✔
204
        num_guesser_threads,
3✔
205
    } = guessing_configuration;
3✔
206

3✔
207
    // This must match the rules in `[Block::has_proof_of_work]`.
3✔
208
    let prev_difficulty = previous_block_header.difficulty;
3✔
209
    let threshold = prev_difficulty.target();
3✔
210
    let threads_to_use = num_guesser_threads.unwrap_or_else(rayon::current_num_threads);
3✔
211
    info!(
3✔
212
        "Guessing with {} threads on block {} with {} outputs and difficulty {}. Target: {}",
×
213
        threads_to_use,
×
214
        block.header().height,
×
215
        block.body().transaction_kernel.outputs.len(),
×
216
        previous_block_header.difficulty,
×
217
        threshold.to_hex()
×
218
    );
219

220
    // note: this article discusses rayon strategies for mining.
221
    // https://www.innoq.com/en/blog/2018/06/blockchain-mining-embarrassingly-parallel/
222
    //
223
    // note: number of rayon threads can be set with env var RAYON_NUM_THREADS
224
    // see:  https://docs.rs/rayon/latest/rayon/fn.max_num_threads.html
225
    let now = Timestamp::now();
3✔
226
    let new_difficulty = difficulty_control(
3✔
227
        now,
3✔
228
        previous_block_header.timestamp,
3✔
229
        previous_block_header.difficulty,
3✔
230
        target_block_interval,
3✔
231
        previous_block_header.height,
3✔
232
    );
3✔
233
    block.set_header_timestamp_and_difficulty(now, new_difficulty);
3✔
234

3✔
235
    block.set_header_guesser_digest(guesser_key.after_image());
3✔
236

3✔
237
    let (kernel_auth_path, header_auth_path) = precalculate_block_auth_paths(&block);
3✔
238

3✔
239
    let pool = ThreadPoolBuilder::new()
3✔
240
        .num_threads(threads_to_use)
3✔
241
        .build()
3✔
242
        .unwrap();
3✔
243
    let guess_result = pool.install(|| {
3✔
244
        rayon::iter::repeat(0)
3✔
245
            .map_init(rand::rng, |rng, _i| {
14,825✔
246
                guess_nonce_iteration(
14,825✔
247
                    kernel_auth_path,
14,825✔
248
                    threshold,
14,825✔
249
                    sleepy_guessing,
14,825✔
250
                    rng,
14,825✔
251
                    header_auth_path,
14,825✔
252
                    &sender,
14,825✔
253
                )
14,825✔
254
            })
14,825✔
255
            .find_any(|r| !r.block_not_found())
14,818✔
256
            .unwrap()
3✔
257
    });
3✔
258

259
    let nonce = match guess_result {
3✔
260
        GuessNonceResult::Cancelled => {
261
            info!("Restarting guessing task",);
×
262
            return;
×
263
        }
264
        GuessNonceResult::NonceFound { nonce } => nonce,
3✔
265
        GuessNonceResult::BlockNotFound => unreachable!(),
×
266
    };
267

268
    info!("Found valid block with nonce: ({nonce}).");
3✔
269

270
    block.set_header_nonce(nonce);
3✔
271

3✔
272
    let timestamp = block.header().timestamp;
3✔
273
    let timestamp_standard = timestamp.standard_format();
3✔
274
    let hash = block.hash();
3✔
275
    let hex = hash.to_hex();
3✔
276
    let height = block.kernel.header.height;
3✔
277
    let num_inputs = block.body().transaction_kernel.inputs.len();
3✔
278
    let num_outputs = block.body().transaction_kernel.outputs.len();
3✔
279
    info!(
3✔
280
        r#"Newly mined block details:
×
281
              Height: {height}
×
282
              Time  : {timestamp_standard} ({timestamp})
×
283
        Digest (Hex): {hex}
×
284
        Digest (Raw): {hash}
×
285
Difficulty threshold: {threshold}
×
286
          Difficulty: {prev_difficulty}
×
287
          #inputs   : {num_inputs}
×
288
          #outputs  : {num_outputs}
×
289
"#
×
290
    );
291

292
    let new_block_found = NewBlockFound {
3✔
293
        block: Box::new(block),
3✔
294
    };
3✔
295

3✔
296
    sender
3✔
297
        .send(new_block_found)
3✔
298
        .unwrap_or_else(|_| warn!("Receiver in mining loop closed prematurely"))
3✔
299
}
3✔
300

301
enum GuessNonceResult {
302
    NonceFound { nonce: Digest },
303
    BlockNotFound,
304
    Cancelled,
305
}
306
impl GuessNonceResult {
307
    fn block_not_found(&self) -> bool {
14,818✔
308
        matches!(self, Self::BlockNotFound)
14,818✔
309
    }
14,818✔
310
}
311

312
/// Return the block-kernel MAST hash given a variable nonce, holding all other
313
/// fields constant.
314
///
315
/// Calculates the block hash in as few Tip5 invocations as possible.
316
/// This function is required for benchmarks, but is not part of the public API.
317
#[inline(always)]
318
#[doc(hidden)]
319
pub fn fast_kernel_mast_hash(
31,044✔
320
    kernel_auth_path: [Digest; BlockKernel::MAST_HEIGHT],
31,044✔
321
    header_auth_path: [Digest; BlockHeader::MAST_HEIGHT],
31,044✔
322
    nonce: Digest,
31,044✔
323
) -> Digest {
31,044✔
324
    let header_mast_hash = Tip5::hash_pair(Tip5::hash_varlen(&nonce.encode()), header_auth_path[0]);
31,044✔
325
    let header_mast_hash = Tip5::hash_pair(header_mast_hash, header_auth_path[1]);
31,044✔
326
    let header_mast_hash = Tip5::hash_pair(header_auth_path[2], header_mast_hash);
31,044✔
327

31,044✔
328
    Tip5::hash_pair(
31,044✔
329
        Tip5::hash_pair(
31,044✔
330
            Tip5::hash_varlen(&header_mast_hash.encode()),
31,044✔
331
            kernel_auth_path[0],
31,044✔
332
        ),
31,044✔
333
        kernel_auth_path[1],
31,044✔
334
    )
31,044✔
335
}
31,044✔
336

337
/// Run a single iteration of the mining loop.
338
#[inline]
339
fn guess_nonce_iteration(
14,825✔
340
    kernel_auth_path: [Digest; BlockKernel::MAST_HEIGHT],
14,825✔
341
    threshold: Digest,
14,825✔
342
    sleepy_guessing: bool,
14,825✔
343
    rng: &mut rand::rngs::ThreadRng,
14,825✔
344
    bh_auth_path: [Digest; BlockHeader::MAST_HEIGHT],
14,825✔
345
    sender: &oneshot::Sender<NewBlockFound>,
14,825✔
346
) -> GuessNonceResult {
14,825✔
347
    if sleepy_guessing {
14,825✔
348
        std::thread::sleep(Duration::from_millis(100));
×
349
    }
14,825✔
350

351
    // Modify the nonce in the block header. In order to collect the guesser
352
    // fee, this nonce must be the post-image of a known pre-image under Tip5.
353
    let nonce: Digest = rng.random();
14,825✔
354

14,825✔
355
    // Check every N guesses if task has been cancelled.
14,825✔
356
    if (sleepy_guessing || (nonce.values()[0].raw_u64() % (1 << 16)) == 0) && sender.is_canceled() {
14,825✔
357
        debug!("Guesser was cancelled.");
×
358
        return GuessNonceResult::Cancelled;
×
359
    }
14,825✔
360

14,825✔
361
    let block_hash = fast_kernel_mast_hash(kernel_auth_path, bh_auth_path, nonce);
14,825✔
362
    let success = block_hash <= threshold;
14,825✔
363

14,825✔
364
    match success {
14,825✔
365
        false => GuessNonceResult::BlockNotFound,
14,822✔
366
        true => GuessNonceResult::NonceFound { nonce },
3✔
367
    }
368
}
14,825✔
369

370
/// Make a coinbase transaction rewarding the composer identified by receiving
371
/// address with the block subsidy minus the guesser fee. The rest, including
372
/// transaction fees, goes to the guesser.
373
pub(crate) async fn make_coinbase_transaction_stateless(
999✔
374
    latest_block: &Block,
999✔
375
    composer_parameters: ComposerParameters,
999✔
376
    timestamp: Timestamp,
999✔
377
    vm_job_queue: Arc<TritonVmJobQueue>,
999✔
378
    job_options: TritonVmProofJobOptions,
999✔
379
) -> Result<(Transaction, TxOutputList)> {
999✔
380
    let (composer_outputs, transaction_details) = prepare_coinbase_transaction_stateless(
999✔
381
        latest_block,
999✔
382
        composer_parameters,
999✔
383
        timestamp,
999✔
384
        job_options.job_settings.network,
999✔
385
    )?;
999✔
386

387
    let witness = PrimitiveWitness::from_transaction_details(&transaction_details);
999✔
388

999✔
389
    info!("Start: generate single proof for coinbase transaction");
999✔
390

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

396
    let kernel = witness.kernel.clone();
999✔
397

398
    let proof = TransactionProofBuilder::new()
999✔
399
        .transaction_details(&transaction_details)
999✔
400
        .primitive_witness(witness)
999✔
401
        .job_queue(vm_job_queue)
999✔
402
        .proof_job_options(job_options)
999✔
403
        .build()
999✔
404
        .await?;
999✔
405

406
    info!("Done: generating single proof for coinbase transaction");
999✔
407

408
    let transaction = TransactionBuilder::new()
999✔
409
        .transaction_kernel(kernel)
999✔
410
        .transaction_proof(proof)
999✔
411
        .build()?;
999✔
412

413
    Ok((transaction, composer_outputs))
999✔
414
}
999✔
415

416
/// Produce two outputs spending a given portion of the coinbase amount.
417
///
418
/// The coinbase amount is usually set to the block subsidy for this block
419
/// height.
420
///
421
/// There are two equal-value outputs because one is liquid immediately, and the
422
/// other is locked for 3 years. The portion of the entire block subsidy that
423
/// goes to the composer is determined by the `guesser_fee_fraction` field of
424
/// the composer parameters.
425
///
426
/// The sum of the value of the outputs is guaranteed to not exceed the
427
/// coinbase amount, since the guesser fee fraction is guaranteed to be in the
428
/// range \[0;1\].
429
///
430
/// Returns an array of two TxOutputs: liquid first, timelocked second.
431
pub(crate) fn composer_outputs(
1,145✔
432
    coinbase_amount: NativeCurrencyAmount,
1,145✔
433
    composer_parameters: ComposerParameters,
1,145✔
434
    timestamp: Timestamp,
1,145✔
435
) -> Result<[TxOutput; 2]> {
1,145✔
436
    let guesser_fee =
1,145✔
437
        coinbase_amount.lossy_f64_fraction_mul(composer_parameters.guesser_fee_fraction());
1,145✔
438

439
    let Some(amount_to_composer) = coinbase_amount.checked_sub(&guesser_fee) else {
1,145✔
440
        bail!(
×
441
            "Guesser fee may not exceed coinbase amount. coinbase_amount: {}; guesser_fee: {}; guesser fee fraction: {}.",
×
442
            coinbase_amount.to_nau(),
×
443
            guesser_fee.to_nau(),
×
444
            composer_parameters.guesser_fee_fraction()
×
445
        );
×
446
    };
447

448
    // Note that this calculation guarantees that at least half of the coinbase
449
    // is timelocked, as a rounding error in the last digit subtracts from the
450
    // liquid amount. This quality is required by the NativeCurrency type
451
    // script.
452
    let mut liquid_composer_amount = amount_to_composer;
1,145✔
453
    liquid_composer_amount.div_two();
1,145✔
454
    let timelocked_composer_amount = amount_to_composer
1,145✔
455
        .checked_sub(&liquid_composer_amount)
1,145✔
456
        .expect("Amount to composer must be larger than liquid amount to composer.");
1,145✔
457

1,145✔
458
    let owned = true;
1,145✔
459
    let liquid_coinbase_output = TxOutput::native_currency(
1,145✔
460
        liquid_composer_amount,
1,145✔
461
        composer_parameters.sender_randomness(),
1,145✔
462
        composer_parameters.reward_address(),
1,145✔
463
        composer_parameters.notification_policy().into(),
1,145✔
464
        owned,
1,145✔
465
    );
1,145✔
466

1,145✔
467
    // Set the time lock to 3 years (minimum) plus 30 minutes margin, since the
1,145✔
468
    // timestamp might be bumped by future merges. These timestamp bumps affect
1,145✔
469
    // only the `timestamp` field of the transaction kernel and not the state
1,145✔
470
    // of the `TimeLock` type script. So in the end you might end up with a
1,145✔
471
    // transaction whose time-locked portion is not time-locked long enough.
1,145✔
472
    let timelocked_coinbase_output = TxOutput::native_currency(
1,145✔
473
        timelocked_composer_amount,
1,145✔
474
        composer_parameters.sender_randomness(),
1,145✔
475
        composer_parameters.reward_address(),
1,145✔
476
        composer_parameters.notification_policy().into(),
1,145✔
477
        owned,
1,145✔
478
    )
1,145✔
479
    .with_time_lock(timestamp + MINING_REWARD_TIME_LOCK_PERIOD + Timestamp::minutes(30));
1,145✔
480

1,145✔
481
    Ok([liquid_coinbase_output, timelocked_coinbase_output])
1,145✔
482
}
1,145✔
483

484
/// Compute `TransactionDetails` and a list of `TxOutput`s for a coinbase
485
/// transaction.
486
pub(super) fn prepare_coinbase_transaction_stateless(
1,143✔
487
    latest_block: &Block,
1,143✔
488
    composer_parameters: ComposerParameters,
1,143✔
489
    timestamp: Timestamp,
1,143✔
490
    network: Network,
1,143✔
491
) -> Result<(TxOutputList, TransactionDetails)> {
1,143✔
492
    let mutator_set_accumulator = latest_block.mutator_set_accumulator_after().clone();
1,143✔
493
    let next_block_height: BlockHeight = latest_block.header().height.next();
1,143✔
494
    info!("Creating coinbase for block of height {next_block_height}.");
1,143✔
495

496
    let coinbase_amount = Block::block_subsidy(next_block_height);
1,143✔
497
    let [liquid_coinbase_output, timelocked_coinbase_output] =
1,143✔
498
        composer_outputs(coinbase_amount, composer_parameters, timestamp)?;
1,143✔
499
    let total_composer_fee = liquid_coinbase_output.utxo().get_native_currency_amount()
1,143✔
500
        + timelocked_coinbase_output
1,143✔
501
            .utxo()
1,143✔
502
            .get_native_currency_amount();
1,143✔
503
    let guesser_fee = coinbase_amount
1,143✔
504
        .checked_sub(&total_composer_fee)
1,143✔
505
        .expect("total_composer_fee cannot exceed coinbase_amount");
1,143✔
506

1,143✔
507
    info!(
1,143✔
508
        "Coinbase amount is set to {coinbase_amount} and is divided between \
×
509
        composer fee ({total_composer_fee}) and guesser fee ({guesser_fee})."
×
510
    );
511

512
    let composer_outputs: TxOutputList = vec![
1,143✔
513
        liquid_coinbase_output.clone(),
1,143✔
514
        timelocked_coinbase_output.clone(),
1,143✔
515
    ]
1,143✔
516
    .into();
1,143✔
517
    let transaction_details = TransactionDetails::new_with_coinbase(
1,143✔
518
        TxInputList::empty(),
1,143✔
519
        composer_outputs.clone(),
1,143✔
520
        coinbase_amount,
1,143✔
521
        guesser_fee,
1,143✔
522
        timestamp,
1,143✔
523
        mutator_set_accumulator,
1,143✔
524
        network,
1,143✔
525
    )
1,143✔
526
    .expect(
1,143✔
527
        "all inputs' ms membership proofs must be valid because inputs are empty;\
1,143✔
528
 and tx must be balanced because the one output receives exactly the coinbase amount",
1,143✔
529
    );
1,143✔
530

1,143✔
531
    Ok((composer_outputs, transaction_details))
1,143✔
532
}
1,143✔
533

534
/// Enumerates origins of transactions to be merged into a block transaction.
535
///
536
/// In the general case, this is (just) the mempool.
537
pub(crate) enum TxMergeOrigin {
538
    Mempool,
539
    #[cfg(test)]
540
    ExplicitList(Vec<Transaction>),
541
}
542

543
/// Create the transaction that goes into the block template. The transaction is
544
/// built from the mempool and from the coinbase transaction. Also returns the
545
/// "sender randomness" used in the coinbase transaction.
546
pub(crate) async fn create_block_transaction(
16✔
547
    predecessor_block: &Block,
16✔
548
    global_state_lock: &GlobalStateLock,
16✔
549
    timestamp: Timestamp,
16✔
550
    job_options: TritonVmProofJobOptions,
16✔
551
) -> Result<(Transaction, Vec<ExpectedUtxo>)> {
16✔
552
    create_block_transaction_from(
16✔
553
        predecessor_block,
16✔
554
        global_state_lock,
16✔
555
        timestamp,
16✔
556
        job_options,
16✔
557
        TxMergeOrigin::Mempool,
16✔
558
    )
16✔
559
    .await
16✔
560
}
16✔
561

562
pub(crate) async fn create_block_transaction_from(
22✔
563
    predecessor_block: &Block,
22✔
564
    global_state_lock: &GlobalStateLock,
22✔
565
    timestamp: Timestamp,
22✔
566
    job_options: TritonVmProofJobOptions,
22✔
567
    tx_merge_origin: TxMergeOrigin,
22✔
568
) -> Result<(Transaction, Vec<ExpectedUtxo>)> {
22✔
569
    let block_capacity_for_transactions = SIZE_20MB_IN_BYTES;
22✔
570

22✔
571
    let predecessor_block_ms = predecessor_block.mutator_set_accumulator_after();
22✔
572
    let mutator_set_hash = predecessor_block_ms.hash();
22✔
573
    debug!("Creating block transaction with mutator set hash: {mutator_set_hash}",);
22✔
574

575
    let mut rng: StdRng =
22✔
576
        SeedableRng::from_seed(global_state_lock.lock_guard().await.shuffle_seed());
22✔
577

578
    let composer_parameters = global_state_lock
22✔
579
        .lock_guard()
22✔
580
        .await
22✔
581
        .composer_parameters(predecessor_block.header().height.next());
22✔
582

22✔
583
    // A coinbase transaction implies mining. So you *must*
22✔
584
    // be able to create a SingleProof.
22✔
585
    let vm_job_queue = vm_job_queue();
22✔
586
    let (coinbase_transaction, composer_txos) = make_coinbase_transaction_stateless(
22✔
587
        predecessor_block,
22✔
588
        composer_parameters.clone(),
22✔
589
        timestamp,
22✔
590
        vm_job_queue.clone(),
22✔
591
        job_options.clone(),
22✔
592
    )
22✔
593
    .await?;
22✔
594

595
    // Get most valuable transactions from mempool.
596
    let only_merge_single_proofs = true;
22✔
597
    let mut transactions_to_merge = match tx_merge_origin {
22✔
598
        #[cfg(test)]
×
599
        TxMergeOrigin::ExplicitList(transactions) => transactions,
6✔
600
        TxMergeOrigin::Mempool => global_state_lock
16✔
601
            .lock_guard()
16✔
602
            .await
16✔
603
            .mempool
604
            .get_transactions_for_block(
16✔
605
                block_capacity_for_transactions,
16✔
606
                Some(MAX_NUM_TXS_TO_MERGE),
16✔
607
                only_merge_single_proofs,
16✔
608
                mutator_set_hash,
16✔
609
            ),
16✔
610
    };
×
611

×
612
    // If necessary, populate list with nop-tx.
×
613
    // Guarantees that some merge happens in below loop, which sets merge-bit.
×
614
    if transactions_to_merge.is_empty() {
22✔
615
        let nop = TransactionDetails::nop(
13✔
616
            predecessor_block.mutator_set_accumulator_after(),
13✔
617
            timestamp,
13✔
618
            global_state_lock.cli().network,
13✔
619
        );
13✔
620
        let nop = PrimitiveWitness::from_transaction_details(&nop);
13✔
621

13✔
622
        // ensure that proof-type is SingleProof
13✔
623
        let options = TritonVmProofJobOptionsBuilder::new()
13✔
624
            .template(&job_options)
13✔
625
            .proof_type(TransactionProofType::SingleProof)
13✔
626
            .build();
13✔
627

628
        let proof = TransactionProofBuilder::new()
13✔
629
            .primitive_witness_ref(&nop)
13✔
630
            .job_queue(vm_job_queue.clone())
13✔
631
            .proof_job_options(options)
13✔
632
            .build()
13✔
633
            .await?;
13✔
634
        let nop = Transaction {
13✔
635
            kernel: nop.kernel,
13✔
636
            proof,
13✔
637
        };
13✔
638

13✔
639
        transactions_to_merge = vec![nop];
13✔
640
    }
9✔
641

642
    let num_merges = transactions_to_merge.len();
22✔
643
    let mut block_transaction = coinbase_transaction;
22✔
644
    for (i, tx_to_include) in transactions_to_merge.into_iter().enumerate() {
22✔
645
        info!("Merging transaction {} / {}", i + 1, num_merges);
22✔
646
        info!(
22✔
647
            "Merging tx with {} inputs, {} outputs. With fee {}.",
×
648
            tx_to_include.kernel.inputs.len(),
×
649
            tx_to_include.kernel.outputs.len(),
×
650
            tx_to_include.kernel.fee
×
651
        );
652
        block_transaction = Transaction::merge_with(
22✔
653
            block_transaction,
22✔
654
            tx_to_include,
22✔
655
            rng.random(),
22✔
656
            vm_job_queue.clone(),
22✔
657
            job_options.clone(),
22✔
658
        )
22✔
659
        .await
22✔
660
        .expect("Must be able to merge transactions in mining context");
22✔
661
    }
662

663
    let own_expected_utxos = composer_parameters.extract_expected_utxos(composer_txos);
22✔
664

22✔
665
    Ok((block_transaction, own_expected_utxos))
22✔
666
}
22✔
667

668
///
669
///
670
/// Locking:
671
///   * acquires `global_state_lock` for write
672
pub(crate) async fn mine(
×
673
    mut from_main: mpsc::Receiver<MainToMiner>,
×
674
    to_main: mpsc::Sender<MinerToMain>,
×
675
    mut global_state_lock: GlobalStateLock,
×
676
) -> Result<()> {
×
677
    // Wait before starting mining task to ensure that peers have sent us information about
678
    // their latest blocks. This should prevent the client from finding blocks that will later
679
    // be orphaned.
680
    const INITIAL_MINING_SLEEP_IN_SECONDS: u64 = 60;
681

682
    // Set PoW guessing to restart every N seconds, if it has been started. Only
683
    // the guesser task may set this to actually resolve, as this will otherwise
684
    // abort e.g. the composer.
685
    const GUESSING_RESTART_INTERVAL_IN_SECONDS: u64 = 20;
686

687
    tokio::time::sleep(Duration::from_secs(INITIAL_MINING_SLEEP_IN_SECONDS)).await;
×
688
    let cli_args = global_state_lock.cli().clone();
×
689

×
690
    let guess_restart_interval = Duration::from_secs(GUESSING_RESTART_INTERVAL_IN_SECONDS);
×
691
    let infinite = Duration::from_secs(u32::MAX.into());
×
692
    let guess_restart_timer = time::sleep(infinite);
×
693
    tokio::pin!(guess_restart_timer);
×
694

×
695
    let mut pause_mine = false;
×
696
    let mut wait_for_confirmation = false;
×
697
    loop {
698
        // Ensure restart timer doesn't resolve again, without guesser
699
        // task actually being spawned.
700
        guess_restart_timer
×
701
            .as_mut()
×
702
            .reset(tokio::time::Instant::now() + infinite);
×
703

704
        let (is_connected, is_syncing, mining_status) = global_state_lock
×
705
            .lock(|s| {
×
706
                (
×
707
                    !s.net.peer_map.is_empty(),
×
708
                    s.net.sync_anchor.is_some(),
×
709
                    s.mining_state.mining_status,
×
710
                )
×
711
            })
×
712
            .await;
×
713
        if !is_connected {
×
714
            const WAIT_TIME_WHEN_DISCONNECTED_IN_SECONDS: u64 = 5;
715
            global_state_lock.set_mining_status_to_inactive().await;
×
716
            warn!("Not mining because client has no connections");
×
717
            sleep(Duration::from_secs(WAIT_TIME_WHEN_DISCONNECTED_IN_SECONDS)).await;
×
718
            continue;
×
719
        }
×
720

×
721
        let (guesser_tx, guesser_rx) = oneshot::channel::<NewBlockFound>();
×
722
        let (composer_tx, composer_rx) = oneshot::channel::<(Block, Vec<ExpectedUtxo>)>();
×
723

724
        let maybe_proposal = global_state_lock
×
725
            .lock_guard()
×
726
            .await
×
727
            .mining_state
728
            .block_proposal
729
            .clone();
×
730
        let guess = cli_args.guess;
×
731

732
        let should_guess = !wait_for_confirmation
×
733
            && guess
×
734
            && maybe_proposal.is_some()
×
735
            && !is_syncing
×
736
            && !pause_mine
×
737
            && is_connected;
×
738

739
        // if start_guessing is true, then we are in a state change from
740
        // inactive state to guessing state.
741
        //
742
        // if start_guessing is false and should_guess is true then we
743
        // have already been guessing and are restarting with new params.
744
        let start_guessing = matches!(
×
745
            (mining_status, should_guess),
×
746
            (MiningStatus::Inactive, true)
747
        );
748

749
        if start_guessing {
×
750
            let proposal = maybe_proposal.unwrap(); // is_some() verified above
×
751
            global_state_lock
×
752
                .set_mining_status_to_guessing(proposal)
×
753
                .await;
×
754
        }
×
755

756
        let guesser_task: Option<JoinHandle<()>> = if should_guess {
×
757
            // safe because above `is_some`
758
            let proposal = maybe_proposal.unwrap();
×
759
            let guesser_key = global_state_lock
×
760
                .lock_guard()
×
761
                .await
×
762
                .wallet_state
763
                .wallet_entropy
764
                .guesser_spending_key(proposal.header().prev_block_digest);
×
765

766
            let latest_block_header = global_state_lock
×
767
                .lock(|s| s.chain.light_state().header().to_owned())
×
768
                .await;
×
769
            let guesser_task = guess_nonce(
×
770
                proposal.to_owned(),
×
771
                latest_block_header,
×
772
                guesser_tx,
×
773
                guesser_key,
×
774
                GuessingConfiguration {
×
775
                    sleepy_guessing: cli_args.sleepy_guessing,
×
776
                    num_guesser_threads: cli_args.guesser_threads,
×
777
                },
×
778
                None, // use default TARGET_BLOCK_INTERVAL
×
779
            );
×
780

×
781
            // Only run for N seconds to allow for updating of block's timestamp
×
782
            // and difficulty.
×
783
            guess_restart_timer
×
784
                .as_mut()
×
785
                .reset(tokio::time::Instant::now() + guess_restart_interval);
×
786

×
787
            Some(
×
788
                tokio::task::Builder::new()
×
789
                    .name("guesser")
×
790
                    .spawn(guesser_task)
×
791
                    .expect("Failed to spawn guesser task"),
×
792
            )
×
793
        } else {
794
            None
×
795
        };
796

797
        let (cancel_compose_tx, cancel_compose_rx) = tokio::sync::watch::channel(());
×
798

×
799
        let compose = cli_args.compose;
×
800
        let mut composer_task = if !wait_for_confirmation
×
801
            && compose
×
802
            && guesser_task.is_none()
×
803
            && !is_syncing
×
804
            && !pause_mine
×
805
            && is_connected
×
806
        {
807
            global_state_lock.set_mining_status_to_composing().await;
×
808

809
            let latest_block = global_state_lock
×
810
                .lock(|s| s.chain.light_state().to_owned())
×
811
                .await;
×
812
            let compose_task = compose_block(
×
813
                latest_block,
×
814
                global_state_lock.clone(),
×
815
                composer_tx,
×
816
                cancel_compose_rx,
×
817
                Timestamp::now(),
×
818
            );
×
819

×
820
            let task = tokio::task::Builder::new()
×
821
                .name("composer")
×
822
                .spawn(compose_task)
×
823
                .expect("Failed to spawn composer task.");
×
824

×
825
            task
×
826
        } else {
827
            tokio::spawn(async { Ok(()) })
×
828
        };
829

830
        let mut restart_guessing = false;
×
831
        let mut stop_guessing = false;
×
832
        let mut stop_composing = false;
×
833
        let mut stop_looping = false;
×
834

×
835
        // Await a message from either the worker task or from the main loop,
×
836
        // or the restart of the guesser-task.
×
837
        select! {
×
838
            _ = &mut guess_restart_timer => {
×
839
                restart_guessing = true;
×
840
            }
×
841
            Ok(Err(e)) = &mut composer_task => {
×
842
                stop_composing = true;
×
843

×
844
                match e.downcast_ref::<prover_job::ProverJobError>() {
×
845
                    Some(prover_job::ProverJobError::ProofComplexityLimitExceeded{..} ) => {
846
                        pause_mine = true;
×
847
                        tracing::error!("exceeded proof complexity limit.  mining paused.  details: {}", e.to_string())
×
848
                    },
849
                    _ => {
850
                        // Ensure graceful shutdown in case of error during
851
                        // composition.
852
                        tracing::error!("Composition failed:\n{e}\n. \
×
853
                            Try adjusting the environment variables \
×
854
                            \"TVM_LDE_TRACE\" and \"RAYON_NUM_THREADS\".");
×
855
                        to_main.send(MinerToMain::Shutdown(COMPOSITION_FAILED_EXIT_CODE)).await?;
×
856
                    }
857
                }
858
            },
859

860
            Some(main_message) = from_main.recv() => {
×
861
                debug!("Miner received message type: {}", main_message.get_type());
×
862

863
                match main_message {
×
864
                    MainToMiner::Shutdown => {
865
                        debug!("Miner shutting down.");
×
866

867
                        stop_guessing = true;
×
868
                        stop_composing = true;
×
869
                        stop_looping = true;
×
870
                    }
871
                    MainToMiner::NewBlock => {
872
                        stop_guessing = true;
×
873
                        stop_composing = true;
×
874

×
875
                        info!("Miner task received notification about new block");
×
876
                    }
877
                    MainToMiner::NewBlockProposal => {
878
                        stop_guessing = true;
×
879
                        stop_composing = true;
×
880

×
881
                        info!("Miner received message about new block proposal for guessing.");
×
882
                    }
883
                    MainToMiner::WaitForContinue => {
×
884
                        stop_guessing = true;
×
885
                        stop_composing = true;
×
886

×
887
                        wait_for_confirmation = true;
×
888
                    }
×
889
                    MainToMiner::Continue => {
×
890
                        wait_for_confirmation = false;
×
891
                    }
×
892
                    MainToMiner::StopMining => {
×
893
                        pause_mine = true;
×
894

×
895
                        stop_guessing = true;
×
896
                        stop_composing = true;
×
897
                    }
×
898
                    MainToMiner::StartMining => {
×
899
                        pause_mine = false;
×
900
                    }
×
901
                    MainToMiner::StopSyncing => {
×
902
                        // no need to do anything here.  Mining will
×
903
                        // resume or not at top of loop depending on
×
904
                        // pause_mine and syncing variables.
×
905
                    }
×
906
                    MainToMiner::StartSyncing => {
×
907
                        // when syncing begins, we must halt the mining
×
908
                        // task.  But we don't change the pause_mine
×
909
                        // variable, because it reflects the logical on/off
×
910
                        // of mining, which syncing can temporarily override
×
911
                        // but not alter the setting.
×
912
                        stop_guessing = true;
×
913
                        stop_composing = true;
×
914
                    }
×
915
                }
916
            }
917
            new_composition = composer_rx => {
×
918
                stop_composing = true;
×
919

×
920
                match new_composition {
×
921
                    Ok((new_block_proposal, composer_utxos)) => {
×
922
                        to_main.send(MinerToMain::BlockProposal(Box::new((new_block_proposal, composer_utxos)))).await?;
×
923
                        wait_for_confirmation = true;
×
924
                    },
925
                    Err(e) => warn!("composing task was cancelled prematurely. Got: {}", e),
×
926
                };
927
            }
928
            new_block = guesser_rx => {
×
929
                stop_guessing = true;
×
930

×
931
                match new_block {
×
932
                    Err(err) => {
×
933
                        warn!("Mining task was cancelled prematurely. Got: {}", err);
×
934
                    }
935
                    Ok(new_block_found) => {
×
936
                        debug!("Worker task reports new block of height {}", new_block_found.block.kernel.header.height);
×
937

938
                        // Sanity check, remove for more efficient mining.
939
                        // The below PoW check could fail due to race conditions. So we don't panic,
940
                        // we only ignore what the worker task sent us.
941
                        let latest_block = global_state_lock
×
942
                            .lock(|s| s.chain.light_state().to_owned())
×
943
                            .await;
×
944

945
                        if !new_block_found.block.has_proof_of_work(latest_block.header()) {
×
946
                            error!("Own mined block did not have valid PoW Discarding.");
×
947
                        } else if !new_block_found.block.is_valid(&latest_block, Timestamp::now(), global_state_lock.cli().network).await {
×
948
                                // Block could be invalid if for instance the proof and proof-of-work
949
                                // took less time than the minimum block time.
950
                                error!("Found block with valid proof-of-work but block is invalid.");
×
951
                        } else {
952

953
                            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());
×
954

955
                            to_main.send(MinerToMain::NewBlockFound(new_block_found)).await?;
×
956

957
                            wait_for_confirmation = true;
×
958
                        }
959
                    },
960
                };
961
            }
962
        }
963

964
        if restart_guessing {
×
965
            if let Some(gt) = &guesser_task {
×
966
                gt.abort();
×
967
                debug!("Abort-signal sent to guesser worker.");
×
968
                debug!("Restarting guesser task with new parameters");
×
969
            }
×
970
        }
×
971
        if stop_guessing {
×
972
            if let Some(gt) = &guesser_task {
×
973
                gt.abort();
×
974
                debug!("Abort-signal sent to guesser worker.");
×
975
            }
×
976
            global_state_lock.set_mining_status_to_inactive().await;
×
977
        }
×
978
        if stop_composing {
×
979
            if !composer_task.is_finished() {
×
980
                cancel_compose_tx.send(())?;
×
981
                debug!("Cancel signal sent to composer worker.");
×
982
            }
×
983
            // avoid duplicate call if stop_guessing is also true.
984
            if !stop_guessing {
×
985
                global_state_lock.set_mining_status_to_inactive().await;
×
986
            }
×
987
        }
×
988

989
        if stop_looping {
×
990
            break;
×
991
        }
×
992
    }
993
    debug!("Miner shut down gracefully.");
×
994
    Ok(())
×
995
}
×
996

997
#[cfg(test)]
998
pub(crate) mod mine_loop_tests {
999
    use std::hint::black_box;
1000

1001
    use block_appendix::BlockAppendix;
1002
    use block_body::BlockBody;
1003
    use block_header::block_header_tests::random_block_header;
1004
    use difficulty_control::Difficulty;
1005
    use itertools::Itertools;
1006
    use macro_rules_attr::apply;
1007
    use num_bigint::BigUint;
1008
    use num_traits::One;
1009
    use num_traits::Pow;
1010
    use num_traits::Zero;
1011
    use tracing_test::traced_test;
1012

1013
    use super::*;
1014
    use crate::config_models::cli_args;
1015
    use crate::config_models::fee_notification_policy::FeeNotificationPolicy;
1016
    use crate::config_models::network::Network;
1017
    use crate::job_queue::triton_vm::TritonVmJobQueue;
1018
    use crate::models::blockchain::block::validity::block_primitive_witness::test::deterministic_block_primitive_witness;
1019
    use crate::models::blockchain::transaction::validity::single_proof::SingleProof;
1020
    use crate::models::blockchain::type_scripts::native_currency_amount::NativeCurrencyAmount;
1021
    use crate::models::proof_abstractions::mast_hash::MastHash;
1022
    use crate::models::proof_abstractions::timestamp::Timestamp;
1023
    use crate::models::proof_abstractions::verifier::verify;
1024
    use crate::models::state::mempool::TransactionOrigin;
1025
    use crate::models::state::tx_creation_config::TxCreationConfig;
1026
    use crate::models::state::tx_proving_capability::TxProvingCapability;
1027
    use crate::models::state::wallet::transaction_output::TxOutput;
1028
    use crate::models::state::wallet::wallet_entropy::WalletEntropy;
1029
    use crate::tests::shared::dummy_expected_utxo;
1030
    use crate::tests::shared::invalid_empty_block;
1031
    use crate::tests::shared::make_mock_transaction_with_mutator_set_hash;
1032
    use crate::tests::shared::mock_genesis_global_state;
1033
    use crate::tests::shared::random_transaction_kernel;
1034
    use crate::tests::shared_tokio_runtime;
1035
    use crate::util_types::test_shared::mutator_set::pseudorandom_addition_record;
1036
    use crate::util_types::test_shared::mutator_set::random_mmra;
1037
    use crate::util_types::test_shared::mutator_set::random_mutator_set_accumulator;
1038

1039
    /// Produce a transaction that allocates the given fraction of the block
1040
    /// subsidy to the wallet in two UTXOs, one time-locked and one liquid.
1041
    pub(crate) async fn make_coinbase_transaction_from_state(
17✔
1042
        latest_block: &Block,
17✔
1043
        global_state_lock: &GlobalStateLock,
17✔
1044
        timestamp: Timestamp,
17✔
1045
        job_options: TritonVmProofJobOptions,
17✔
1046
    ) -> Result<(Transaction, Vec<ExpectedUtxo>)> {
17✔
1047
        // It's important to use the input `latest_block` here instead of
17✔
1048
        // reading it from state, since that could, because of a race condition
17✔
1049
        // lead to an inconsistent witness higher up in the call graph. This is
17✔
1050
        // done to avoid holding a read-lock throughout this function.
17✔
1051
        let next_block_height: BlockHeight = latest_block.header().height.next();
17✔
1052
        let vm_job_queue = vm_job_queue();
17✔
1053

1054
        let composer_parameters = global_state_lock
17✔
1055
            .lock_guard()
17✔
1056
            .await
17✔
1057
            .composer_parameters(next_block_height);
17✔
1058
        let (transaction, composer_outputs) = make_coinbase_transaction_stateless(
17✔
1059
            latest_block,
17✔
1060
            composer_parameters.clone(),
17✔
1061
            timestamp,
17✔
1062
            vm_job_queue,
17✔
1063
            job_options,
17✔
1064
        )
17✔
1065
        .await?;
17✔
1066

1067
        let own_expected_utxos = composer_parameters.extract_expected_utxos(composer_outputs);
17✔
1068

17✔
1069
        Ok((transaction, own_expected_utxos))
17✔
1070
    }
17✔
1071

1072
    /// Similar to [mine_iteration] function but intended for tests.
1073
    ///
1074
    /// Does *not* update the timestamp of the block and therefore also does not
1075
    /// update the difficulty field, as this applies to the next block and only
1076
    /// changes as a result of the timestamp of this block.
1077
    pub(crate) fn mine_iteration_for_tests(block: &mut Block, rng: &mut StdRng) {
453,274✔
1078
        let nonce = rng.random();
453,274✔
1079
        block.set_header_nonce(nonce);
453,274✔
1080
    }
453,274✔
1081

1082
    /// Estimates the hash rate in number of hashes per milliseconds
1083
    async fn estimate_own_hash_rate(
×
1084
        target_block_interval: Option<Timestamp>,
×
1085
        sleepy_guessing: bool,
×
1086
        num_outputs: usize,
×
1087
    ) -> f64 {
×
1088
        let mut rng = rand::rng();
×
1089
        let network = Network::RegTest;
×
1090
        let global_state_lock = mock_genesis_global_state(
×
1091
            network,
×
1092
            2,
×
1093
            WalletEntropy::devnet_wallet(),
×
1094
            cli_args::Args::default(),
×
1095
        )
×
1096
        .await;
×
1097

1098
        let previous_block = global_state_lock
×
1099
            .lock_guard()
×
1100
            .await
×
1101
            .chain
1102
            .light_state()
×
1103
            .clone();
×
1104

×
1105
        let (transaction, _coinbase_utxo_info) = {
×
1106
            let outputs = (0..num_outputs)
×
1107
                .map(|_| pseudorandom_addition_record(rng.random()))
×
1108
                .collect_vec();
×
1109
            (
×
1110
                make_mock_transaction_with_mutator_set_hash(
×
1111
                    vec![],
×
1112
                    outputs,
×
1113
                    previous_block.mutator_set_accumulator_after().hash(),
×
1114
                ),
×
1115
                dummy_expected_utxo(),
×
1116
            )
×
1117
        };
×
1118
        let start_time = Timestamp::now();
×
1119
        let block = Block::block_template_invalid_proof(
×
1120
            &previous_block,
×
1121
            transaction,
×
1122
            start_time,
×
1123
            target_block_interval,
×
1124
        );
×
1125
        let threshold = previous_block.header().difficulty.target();
×
1126
        let num_iterations_launched = 1_000_000;
×
1127
        let tick = std::time::SystemTime::now();
×
1128
        let (kernel_auth_path, header_auth_path) = precalculate_block_auth_paths(&block);
×
1129

×
1130
        let (worker_task_tx, worker_task_rx) = oneshot::channel::<NewBlockFound>();
×
1131
        let num_iterations_run =
×
1132
            rayon::iter::IntoParallelIterator::into_par_iter(0..num_iterations_launched)
×
1133
                .map_init(rand::rng, |prng, _i| {
×
1134
                    guess_nonce_iteration(
×
1135
                        kernel_auth_path,
×
1136
                        threshold,
×
1137
                        sleepy_guessing,
×
1138
                        prng,
×
1139
                        header_auth_path,
×
1140
                        &worker_task_tx,
×
1141
                    );
×
1142
                })
×
1143
                .count();
×
1144
        drop(worker_task_rx);
×
1145

×
1146
        let time_spent_mining = tick.elapsed().unwrap();
×
1147

×
1148
        (num_iterations_run as f64) / (time_spent_mining.as_millis() as f64)
×
1149
    }
×
1150

1151
    /// Estimate the time it takes to prepare a block so we can start guessing
1152
    /// nonces.
1153
    async fn estimate_block_preparation_time_invalid_proof() -> f64 {
×
1154
        let network = Network::Main;
×
1155
        let genesis_block = Block::genesis(network);
×
1156
        let guesser_fee_fraction = 0.0;
×
1157
        let cli_args = cli_args::Args {
×
1158
            guesser_fraction: guesser_fee_fraction,
×
1159
            ..Default::default()
×
1160
        };
×
1161

1162
        let global_state_lock =
×
1163
            mock_genesis_global_state(network, 2, WalletEntropy::devnet_wallet(), cli_args).await;
×
1164
        let tick = std::time::SystemTime::now();
×
1165
        let (transaction, _coinbase_utxo_info) = make_coinbase_transaction_from_state(
×
1166
            &genesis_block,
×
1167
            &global_state_lock,
×
1168
            network.launch_date(),
×
1169
            global_state_lock
×
1170
                .cli()
×
1171
                .proof_job_options_primitive_witness(),
×
1172
        )
×
1173
        .await
×
1174
        .unwrap();
×
1175

×
1176
        let in_seven_months = network.launch_date() + Timestamp::months(7);
×
1177
        let block =
×
1178
            Block::block_template_invalid_proof(&genesis_block, transaction, in_seven_months, None);
×
1179
        let tock = tick.elapsed().unwrap().as_millis() as f64;
×
1180
        black_box(block);
×
1181
        tock
×
1182
    }
×
1183

NEW
1184
    #[traced_test]
×
1185
    #[apply(shared_tokio_runtime)]
1186
    async fn block_proposal_for_height_one_is_valid_for_various_guesser_fee_fractions() {
1187
        // Verify that a block template made with transaction from the mempool is a valid block
1188
        let network = Network::Main;
1189
        let mut alice = mock_genesis_global_state(
1190
            network,
1191
            2,
1192
            WalletEntropy::devnet_wallet(),
1193
            cli_args::Args::default(),
1194
        )
1195
        .await;
1196
        let genesis_block = Block::genesis(network);
1197
        let now = genesis_block.kernel.header.timestamp + Timestamp::months(7);
1198
        assert!(
1199
            !alice
1200
                .lock_guard()
1201
                .await
1202
                .get_wallet_status_for_tip()
1203
                .await
1204
                .synced_unspent_available_amount(now)
1205
                .is_zero(),
1206
            "Assumed to be premine-recipient"
1207
        );
1208

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

1211
        let alice_key = alice
1212
            .lock_guard()
1213
            .await
1214
            .wallet_state
1215
            .wallet_entropy
1216
            .nth_generation_spending_key_for_tests(0);
1217
        let output_to_alice = TxOutput::offchain_native_currency(
1218
            NativeCurrencyAmount::coins(4),
1219
            rng.random(),
1220
            alice_key.to_address().into(),
1221
            false,
1222
        );
1223
        let config = TxCreationConfig::default()
1224
            .recover_change_off_chain(alice_key.into())
1225
            .with_prover_capability(TxProvingCapability::SingleProof);
1226
        let tx_from_alice = alice
1227
            .api()
1228
            .tx_initiator_internal()
1229
            .create_transaction(
1230
                vec![output_to_alice].into(),
1231
                NativeCurrencyAmount::coins(1),
1232
                now,
1233
                config,
1234
            )
1235
            .await
1236
            .unwrap()
1237
            .transaction;
1238

1239
        let mut cli = cli_args::Args::default();
1240
        for guesser_fee_fraction in [0f64, 0.5, 1.0] {
1241
            // Verify constructed coinbase transaction and block template when mempool is empty
1242
            assert!(
1243
                alice.lock_guard().await.mempool.is_empty(),
1244
                "Mempool must be empty at start of loop"
1245
            );
1246

1247
            cli.guesser_fraction = guesser_fee_fraction;
1248
            alice.set_cli(cli.clone()).await;
1249
            let (transaction_empty_mempool, coinbase_utxo_info) = {
1250
                create_block_transaction(
1251
                    &genesis_block,
1252
                    &alice,
1253
                    now,
1254
                    (TritonVmJobPriority::Normal, None).into(),
1255
                )
1256
                .await
1257
                .unwrap()
1258
            };
1259
            assert!(
1260
                coinbase_utxo_info.is_empty(),
1261
                "Default composer UTXO notification policy is onchain. \
1262
             So no expected UTXOs should be returned here."
1263
            );
1264

1265
            let cb_txkmh = transaction_empty_mempool.kernel.mast_hash();
1266
            let cb_tx_claim = SingleProof::claim(cb_txkmh);
1267
            assert!(
1268
                verify(
1269
                    cb_tx_claim.clone(),
1270
                    transaction_empty_mempool
1271
                        .proof
1272
                        .clone()
1273
                        .into_single_proof()
1274
                        .clone(),
1275
                    network,
1276
                )
1277
                .await,
1278
                "Transaction proof for coinbase transaction must be valid."
1279
            );
1280

1281
            assert_eq!(
1282
                2,
1283
                transaction_empty_mempool.kernel.outputs.len(),
1284
                "Coinbase transaction with empty mempool must have exactly two outputs"
1285
            );
1286
            assert!(
1287
                transaction_empty_mempool.kernel.inputs.is_empty(),
1288
                "Coinbase transaction with empty mempool must have zero inputs"
1289
            );
1290
            let block_1_empty_mempool = Block::compose(
1291
                &genesis_block,
1292
                transaction_empty_mempool,
1293
                now,
1294
                None,
1295
                TritonVmJobQueue::get_instance(),
1296
                TritonVmJobPriority::High.into(),
1297
            )
1298
            .await
1299
            .unwrap();
1300
            assert!(
1301
                block_1_empty_mempool
1302
                    .is_valid(&genesis_block, now, network)
1303
                    .await,
1304
                "Block template created by miner with empty mempool must be valid"
1305
            );
1306

1307
            {
1308
                let mut alice_gsm = alice.lock_guard_mut().await;
1309
                alice_gsm
1310
                    .mempool_insert((*tx_from_alice).clone(), TransactionOrigin::Own)
1311
                    .await;
1312
                assert_eq!(1, alice_gsm.mempool.len());
1313
            }
1314

1315
            // Build transaction for block
1316
            let (transaction_non_empty_mempool, _new_coinbase_sender_randomness) = {
1317
                create_block_transaction(
1318
                    &genesis_block,
1319
                    &alice,
1320
                    now,
1321
                    (TritonVmJobPriority::Normal, None).into(),
1322
                )
1323
                .await
1324
                .unwrap()
1325
            };
1326
            assert_eq!(
1327
            4,
1328
            transaction_non_empty_mempool.kernel.outputs.len(),
1329
            "Transaction for block with non-empty mempool must contain two coinbase outputs, send output, and change output"
1330
        );
1331
            assert_eq!(1, transaction_non_empty_mempool.kernel.inputs.len(), "Transaction for block with non-empty mempool must contain one input: the genesis UTXO being spent");
1332

1333
            // Build and verify block template
1334
            let block_1_nonempty_mempool = Block::compose(
1335
                &genesis_block,
1336
                transaction_non_empty_mempool,
1337
                now,
1338
                None,
1339
                TritonVmJobQueue::get_instance(),
1340
                TritonVmJobPriority::default().into(),
1341
            )
1342
            .await
1343
            .unwrap();
1344
            assert!(
1345
                block_1_nonempty_mempool
1346
                    .is_valid(&genesis_block, now + Timestamp::seconds(2), network)
1347
                    .await,
1348
                "Block template created by miner with non-empty mempool must be valid"
1349
            );
1350

1351
            alice.lock_guard_mut().await.mempool_clear().await;
1352
        }
1353
    }
1354

1355
    #[traced_test]
×
1356
    #[apply(shared_tokio_runtime)]
1357
    async fn block_proposal_for_height_two_is_valid() {
1358
        // Verify that block proposals of both height 1 and 2 are valid.
1359
        let network = Network::Main;
1360

1361
        // force SingleProof capability.
1362
        let cli = cli_args::Args {
1363
            tx_proving_capability: Some(TxProvingCapability::SingleProof),
1364
            ..Default::default()
1365
        };
1366

1367
        let mut alice =
1368
            mock_genesis_global_state(network, 2, WalletEntropy::devnet_wallet(), cli).await;
1369
        let genesis_block = Block::genesis(network);
1370
        let mocked_now = genesis_block.header().timestamp + Timestamp::months(7);
1371

1372
        assert!(
1373
            alice.lock_guard().await.mempool.is_empty(),
1374
            "Mempool must be empty at start of test"
1375
        );
1376
        let (sender_1, receiver_1) = oneshot::channel();
1377
        let (_cancel_compose_tx, cancel_compose_rx) = tokio::sync::watch::channel(());
1378
        compose_block(
1379
            genesis_block.clone(),
1380
            alice.clone(),
1381
            sender_1,
1382
            cancel_compose_rx.clone(),
1383
            mocked_now,
1384
        )
1385
        .await
1386
        .unwrap();
1387
        let (block_1, _) = receiver_1.await.unwrap();
1388
        assert!(block_1.is_valid(&genesis_block, mocked_now, network).await);
1389
        alice.set_new_tip(block_1.clone()).await.unwrap();
1390

1391
        let (sender_2, receiver_2) = oneshot::channel();
1392
        compose_block(
1393
            block_1.clone(),
1394
            alice.clone(),
1395
            sender_2,
1396
            cancel_compose_rx,
1397
            mocked_now,
1398
        )
1399
        .await
1400
        .unwrap();
1401
        let (block_2, _) = receiver_2.await.unwrap();
1402
        assert!(block_2.is_valid(&block_1, mocked_now, network).await);
1403
    }
1404

1405
    /// This test mines a single block at height 1 on the main network
1406
    /// and then validates it with `Block::is_valid()` and
1407
    /// `Block::has_proof_of_work()`.
1408
    ///
1409
    /// This is a regression test for issue #131.
1410
    /// https://github.com/Neptune-Crypto/neptune-core/issues/131
1411
    ///
1412
    /// The cause of the failure was that `mine_block_worker()` was comparing
1413
    /// hash(block_header) against difficulty threshold while
1414
    /// `Block::has_proof_of_work` uses hash(block) instead.
1415
    ///
1416
    /// The fix was to modify `mine_block_worker()` so that it also
1417
    /// uses hash(block) and subsequently the test passes (unmodified).
1418
    ///
1419
    /// This test is present and fails in commit
1420
    /// b093631fd0d479e6c2cc252b08f18d920a1ec2e5 which is prior to the fix.
1421
    #[traced_test]
×
1422
    #[apply(shared_tokio_runtime)]
1423
    async fn mined_block_has_proof_of_work() {
1424
        let network = Network::Main;
1425
        let cli_args = cli_args::Args {
1426
            guesser_fraction: 0.0,
1427
            ..Default::default()
1428
        };
1429
        let global_state_lock =
1430
            mock_genesis_global_state(network, 2, WalletEntropy::devnet_wallet(), cli_args).await;
1431
        let tip_block_orig = Block::genesis(network);
1432
        let launch_date = tip_block_orig.header().timestamp;
1433
        let (worker_task_tx, worker_task_rx) = oneshot::channel::<NewBlockFound>();
1434

1435
        let (transaction, _composer_utxo_info) = make_coinbase_transaction_from_state(
1436
            &tip_block_orig,
1437
            &global_state_lock,
1438
            launch_date,
1439
            global_state_lock
1440
                .cli()
1441
                .proof_job_options_primitive_witness(),
1442
        )
1443
        .await
1444
        .unwrap();
1445

1446
        let guesser_key = global_state_lock
1447
            .lock_guard()
1448
            .await
1449
            .wallet_state
1450
            .wallet_entropy
1451
            .guesser_spending_key(tip_block_orig.hash());
1452
        let mut block =
1453
            Block::block_template_invalid_proof(&tip_block_orig, transaction, launch_date, None);
1454
        block.set_header_guesser_digest(guesser_key.after_image());
1455

1456
        let sleepy_guessing = false;
1457
        let num_guesser_threads = None;
1458

1459
        guess_worker(
1460
            block,
1461
            tip_block_orig.header().to_owned(),
1462
            worker_task_tx,
1463
            guesser_key,
1464
            GuessingConfiguration {
1465
                sleepy_guessing,
1466
                num_guesser_threads,
1467
            },
1468
            None,
1469
        );
1470

1471
        let mined_block_info = worker_task_rx.await.unwrap();
1472

1473
        assert!(mined_block_info
1474
            .block
1475
            .has_proof_of_work(tip_block_orig.header()));
1476
    }
1477

1478
    /// This test mines a single block at height 1 on the main network
1479
    /// and then validates that the header timestamp has changed and
1480
    /// that it is within the 100 seconds (from now).
1481
    ///
1482
    /// This is a regression test for issue #149.
1483
    /// https://github.com/Neptune-Crypto/neptune-core/issues/149
1484
    ///
1485
    /// note: this test fails in 318b7a20baf11a7a99f249660f1f70484c586012
1486
    ///       and should always pass in later commits.
1487
    #[traced_test]
×
1488
    #[apply(shared_tokio_runtime)]
1489
    async fn block_timestamp_represents_time_guessing_started() -> Result<()> {
1490
        let network = Network::Main;
1491
        let cli_args = cli_args::Args {
1492
            guesser_fraction: 0.0,
1493
            ..Default::default()
1494
        };
1495
        let global_state_lock =
1496
            mock_genesis_global_state(network, 2, WalletEntropy::devnet_wallet(), cli_args).await;
1497
        let (worker_task_tx, worker_task_rx) = oneshot::channel::<NewBlockFound>();
1498

1499
        let tip_block_orig = global_state_lock
1500
            .lock_guard()
1501
            .await
1502
            .chain
1503
            .light_state()
1504
            .clone();
1505

1506
        let now = tip_block_orig.header().timestamp + Timestamp::minutes(10);
1507

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

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

1522
        let guesser_key = HashLockKey::from_preimage(Digest::default());
1523

1524
        let template = Block::block_template_invalid_proof(
1525
            &tip_block_orig,
1526
            transaction,
1527
            ten_seconds_ago,
1528
            None,
1529
        );
1530

1531
        // sanity check that our initial state is correct.
1532
        let initial_header_timestamp = template.header().timestamp;
1533
        assert_eq!(ten_seconds_ago, initial_header_timestamp);
1534

1535
        let sleepy_guessing = false;
1536
        let num_guesser_threads = None;
1537

1538
        guess_worker(
1539
            template,
1540
            tip_block_orig.header().to_owned(),
1541
            worker_task_tx,
1542
            guesser_key,
1543
            GuessingConfiguration {
1544
                sleepy_guessing,
1545
                num_guesser_threads,
1546
            },
1547
            None,
1548
        );
1549

1550
        let mined_block_info = worker_task_rx.await.unwrap();
1551

1552
        let block_timestamp = mined_block_info.block.kernel.header.timestamp;
1553

1554
        // Mining updates the timestamp. So block timestamp will be >= to what
1555
        // was set in the block template, and <= current time.
1556
        assert!(block_timestamp >= initial_header_timestamp);
1557
        assert!(block_timestamp <= Timestamp::now());
1558

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

1562
        Ok(())
1563
    }
1564

1565
    /// Test the difficulty adjustment algorithm.
1566
    ///
1567
    /// Specifically, verify that the observed concrete block interval when mining
1568
    /// tracks the target block interval, assuming:
1569
    ///  - No time is spent proving
1570
    ///  - Constant mining power
1571
    ///  - Mining power exceeds lower bound (hashing once every target interval).
1572
    ///
1573
    /// Note that the second assumption is broken when running the entire test suite.
1574
    /// So if this test fails when all others pass, it is not necessarily a cause
1575
    /// for worry.
1576
    ///
1577
    /// We mine ten blocks with a target block interval of 1 second, so all
1578
    /// blocks should be mined in approx 10 seconds.
1579
    ///
1580
    /// We set a test time limit of 3x the expected time, ie 30 seconds, and
1581
    /// panic if mining all blocks takes longer than that.
1582
    ///
1583
    /// We also assert upper and lower bounds for variance from the expected 10
1584
    /// seconds.  The variance limit is 1.3, so the upper bound is 13 seconds
1585
    /// and the lower bound is 7692ms.
1586
    ///
1587
    /// We ignore the first 2 blocks after genesis because they are typically
1588
    /// mined very fast.
1589
    ///
1590
    /// We avoid sleepy guessing to avoid complications from the
1591
    /// sleep(100 millis) call in mining loop when restricted mining is enabled.
1592
    ///
1593
    /// This serves as a regression test for issue #154.
1594
    /// https://github.com/Neptune-Crypto/neptune-core/issues/154
1595
    async fn mine_m_blocks_in_n_seconds<const NUM_BLOCKS: usize, const NUM_SECONDS: usize>(
×
1596
    ) -> Result<()> {
×
1597
        let network = Network::RegTest;
×
1598
        let global_state_lock = mock_genesis_global_state(
×
1599
            network,
×
1600
            2,
×
1601
            WalletEntropy::devnet_wallet(),
×
1602
            cli_args::Args::default(),
×
1603
        )
×
1604
        .await;
×
1605

1606
        let mut prev_block = global_state_lock
×
1607
            .lock_guard()
×
1608
            .await
×
1609
            .chain
1610
            .light_state()
×
1611
            .clone();
×
1612

×
1613
        // adjust these to simulate longer mining runs, possibly
×
1614
        // with shorter or longer target intervals.
×
1615
        // expected_duration = num_blocks * target_block_interval
×
1616
        let target_block_interval =
×
1617
            Timestamp::millis((1000.0 * (NUM_SECONDS as f64) / (NUM_BLOCKS as f64)).round() as u64);
×
1618
        println!(
×
1619
            "target block interval: {} ms",
×
1620
            target_block_interval.0.value()
×
1621
        );
×
1622

×
1623
        // set initial difficulty in accordance with own hash rate
×
1624
        let sleepy_guessing = false;
×
1625
        let num_guesser_threads = None;
×
1626
        let num_outputs = 0;
×
1627
        let hash_rate =
×
1628
            estimate_own_hash_rate(Some(target_block_interval), sleepy_guessing, num_outputs).await;
×
1629
        println!("estimating hash rate at {} per millisecond", hash_rate);
×
1630
        let prepare_time = estimate_block_preparation_time_invalid_proof().await;
×
1631
        println!("estimating block preparation time at {prepare_time} ms");
×
1632
        if 1.5 * prepare_time > target_block_interval.0.value() as f64 {
×
1633
            println!(
×
1634
                "Cannot perform meaningful test! Block preparation time \
×
1635
            too large for target block interval."
×
1636
            );
×
1637
            return Ok(());
×
1638
        }
×
1639

×
1640
        let guessing_time = (target_block_interval.to_millis() as f64) - prepare_time;
×
1641
        let initial_difficulty = BigUint::from((hash_rate * guessing_time) as u128);
×
1642
        println!("initial difficulty: {}", initial_difficulty);
×
1643
        prev_block.set_header_timestamp_and_difficulty(
×
1644
            prev_block.header().timestamp,
×
1645
            Difficulty::from_biguint(initial_difficulty),
×
1646
        );
×
1647

×
1648
        let expected_duration = target_block_interval * NUM_BLOCKS;
×
1649
        let stddev = (guessing_time.pow(2.0_f64) / (NUM_BLOCKS as f64)).sqrt();
×
1650
        let allowed_standard_deviations = 4;
×
1651
        let min_duration = (expected_duration.0.value() as f64)
×
1652
            - f64::from(allowed_standard_deviations) * stddev * (NUM_BLOCKS as f64);
×
1653
        let max_duration = (expected_duration.0.value() as f64)
×
1654
            + f64::from(allowed_standard_deviations) * stddev * (NUM_BLOCKS as f64);
×
1655
        let max_test_time = expected_duration * 3;
×
1656

×
1657
        // we ignore the first 2 blocks after genesis because they are
×
1658
        // typically mined very fast.
×
1659
        let ignore_first_n_blocks = 2;
×
1660

×
1661
        let mut durations = Vec::with_capacity(NUM_BLOCKS);
×
1662
        let mut start_instant = std::time::SystemTime::now();
×
1663

1664
        for i in 0..NUM_BLOCKS + ignore_first_n_blocks {
×
1665
            if i <= ignore_first_n_blocks {
×
1666
                start_instant = std::time::SystemTime::now();
×
1667
            }
×
1668

1669
            let start_time = Timestamp::now();
×
1670
            let start_st = std::time::SystemTime::now();
×
1671

×
1672
            let transaction = make_mock_transaction_with_mutator_set_hash(
×
1673
                vec![],
×
1674
                vec![],
×
1675
                prev_block.mutator_set_accumulator_after().hash(),
×
1676
            );
×
1677

×
1678
            let guesser_key = HashLockKey::from_preimage(Digest::default());
×
1679

×
1680
            let block = Block::block_template_invalid_proof(
×
1681
                &prev_block,
×
1682
                transaction,
×
1683
                start_time,
×
1684
                Some(target_block_interval),
×
1685
            );
×
1686

×
1687
            let (worker_task_tx, worker_task_rx) = oneshot::channel::<NewBlockFound>();
×
1688
            let height = block.header().height;
×
1689

×
1690
            guess_worker(
×
1691
                block,
×
1692
                *prev_block.header(),
×
1693
                worker_task_tx,
×
1694
                guesser_key,
×
1695
                GuessingConfiguration {
×
1696
                    sleepy_guessing,
×
1697
                    num_guesser_threads,
×
1698
                },
×
1699
                Some(target_block_interval),
×
1700
            );
×
1701

1702
            let mined_block_info = worker_task_rx.await.unwrap();
×
1703

×
1704
            // note: this assertion often fails prior to fix for #154.
×
1705
            // Also note that `is_valid` is a wrapper around `is_valid_internal`
×
1706
            // which is the method we need here because it allows us to override
×
1707
            // default values for the target block interval and the minimum
×
1708
            // block interval.
×
1709
            assert!(mined_block_info
×
1710
                .block
×
1711
                .has_proof_of_work(prev_block.header()));
×
1712

1713
            prev_block = *mined_block_info.block;
×
1714

1715
            let block_time = start_st.elapsed()?.as_millis();
×
1716
            println!(
×
1717
                "Found block {} in {block_time} milliseconds; \
×
1718
                difficulty was {}; total time elapsed so far: {} ms",
×
1719
                height,
×
1720
                BigUint::from(prev_block.header().difficulty),
×
1721
                start_instant.elapsed()?.as_millis()
×
1722
            );
×
1723
            if i > ignore_first_n_blocks {
×
1724
                durations.push(block_time as f64);
×
1725
            }
×
1726

1727
            let elapsed = start_instant.elapsed()?.as_millis();
×
1728
            assert!(
×
1729
                elapsed <= max_test_time.0.value().into(),
×
1730
                "test time limit exceeded. \
×
1731
                 expected_duration: {expected_duration}, limit: {max_test_time}, actual: {elapsed}"
×
1732
            );
1733
        }
1734

1735
        let actual_duration = start_instant.elapsed()?.as_millis() as u64;
×
1736

×
1737
        println!(
×
1738
            "actual duration: {actual_duration}\n\
×
1739
        expected duration: {expected_duration}\n\
×
1740
        min_duration: {min_duration}\n\
×
1741
        max_duration: {max_duration}\n\
×
1742
        allowed deviation: {allowed_standard_deviations}"
×
1743
        );
×
1744
        println!(
×
1745
            "average block time: {} whereas target: {}",
×
1746
            durations.into_iter().sum::<f64>() / (NUM_BLOCKS as f64),
×
1747
            target_block_interval
×
1748
        );
×
1749

×
1750
        assert!((actual_duration as f64) > min_duration);
×
1751
        assert!((actual_duration as f64) < max_duration);
×
1752

1753
        Ok(())
×
1754
    }
×
1755

1756
    #[test]
1757
    fn fast_kernel_mast_hash_agrees_with_mast_hash_function_invalid_block() {
1✔
1758
        let genesis = Block::genesis(Network::Main);
1✔
1759
        let block1 = invalid_empty_block(&genesis);
1✔
1760
        for block in [genesis, block1] {
2✔
1761
            let (kernel_auth_path, header_auth_path) = precalculate_block_auth_paths(&block);
2✔
1762
            assert_eq!(
2✔
1763
                block.kernel.mast_hash(),
2✔
1764
                fast_kernel_mast_hash(kernel_auth_path, header_auth_path, block.header().nonce)
2✔
1765
            );
2✔
1766
        }
1767
    }
1✔
1768

1769
    #[test]
1770
    fn fast_kernel_mast_hash_agrees_with_mast_hash_function_valid_block() {
1✔
1771
        let block_primitive_witness = deterministic_block_primitive_witness();
1✔
1772
        let a_block = block_primitive_witness.predecessor_block();
1✔
1773
        let (kernel_auth_path, header_auth_path) = precalculate_block_auth_paths(a_block);
1✔
1774
        assert_eq!(
1✔
1775
            a_block.kernel.mast_hash(),
1✔
1776
            fast_kernel_mast_hash(kernel_auth_path, header_auth_path, a_block.header().nonce)
1✔
1777
        );
1✔
1778
    }
1✔
1779

1780
    #[traced_test]
×
1781
    #[apply(shared_tokio_runtime)]
1782
    async fn mine_20_blocks_in_40_seconds() -> Result<()> {
1783
        mine_m_blocks_in_n_seconds::<20, 40>().await.unwrap();
1784
        Ok(())
1785
    }
1786

1787
    #[traced_test]
×
1788
    #[apply(shared_tokio_runtime)]
1789
    async fn hash_rate_independent_of_tx_size() {
1790
        // It's crucial that the hash rate is independent of the size of the
1791
        // block, since miners are otherwise heavily incentivized to mine small
1792
        // or empty blocks.
1793
        let sleepy_guessing = false;
1794
        let hash_rate_empty_tx = estimate_own_hash_rate(None, sleepy_guessing, 0).await;
1795
        println!("hash_rate_empty_tx: {hash_rate_empty_tx}");
1796

1797
        let hash_rate_big_tx = estimate_own_hash_rate(None, sleepy_guessing, 10000).await;
1798
        println!("hash_rate_big_tx: {hash_rate_big_tx}");
1799

1800
        assert!(
1801
            hash_rate_empty_tx * 1.1 > hash_rate_big_tx
1802
                && hash_rate_empty_tx * 0.9 < hash_rate_big_tx,
1803
            "Hash rate for big and small block must be within 10 %"
1804
        );
1805
    }
1806

1807
    #[traced_test]
×
1808
    #[apply(shared_tokio_runtime)]
1809
    async fn coinbase_transaction_has_one_timelocked_and_one_liquid_output() {
1810
        for notification_policy in [
1811
            FeeNotificationPolicy::OffChain,
1812
            FeeNotificationPolicy::OnChainGeneration,
1813
            FeeNotificationPolicy::OnChainSymmetric,
1814
        ] {
1815
            let network = Network::Main;
1816
            let cli_args = cli_args::Args {
1817
                guesser_fraction: 0.0,
1818
                fee_notification: notification_policy,
1819
                ..Default::default()
1820
            };
1821
            let global_state_lock =
1822
                mock_genesis_global_state(network, 2, WalletEntropy::devnet_wallet(), cli_args)
1823
                    .await;
1824
            let genesis_block = Block::genesis(network);
1825
            let launch_date = genesis_block.header().timestamp;
1826

1827
            let (transaction, coinbase_utxo_info) = make_coinbase_transaction_from_state(
1828
                &genesis_block,
1829
                &global_state_lock,
1830
                launch_date,
1831
                global_state_lock
1832
                    .cli()
1833
                    .proof_job_options_primitive_witness(),
1834
            )
1835
            .await
1836
            .unwrap();
1837

1838
            let expected_number_of_expected_utxos = match notification_policy {
1839
                FeeNotificationPolicy::OffChain => 2,
1840
                FeeNotificationPolicy::OnChainSymmetric
1841
                | FeeNotificationPolicy::OnChainGeneration => 0,
1842
            };
1843
            assert_eq!(
1844
                2,
1845
                transaction.kernel.outputs.len(),
1846
                "Expected two outputs in coinbase tx"
1847
            );
1848
            assert_eq!(
1849
                expected_number_of_expected_utxos,
1850
                coinbase_utxo_info.len(),
1851
                "Expected {expected_number_of_expected_utxos} expected UTXOs for composer."
1852
            );
1853

1854
            if notification_policy == FeeNotificationPolicy::OffChain {
1855
                assert!(
1856
                    coinbase_utxo_info
1857
                        .iter()
1858
                        .filter(|x| x.utxo.release_date().is_some())
2✔
1859
                        .count()
1860
                        .is_one(),
1861
                    "Expected one timelocked coinbase UTXO"
1862
                );
1863
                assert!(
1864
                    coinbase_utxo_info
1865
                        .iter()
1866
                        .filter(|x| x.utxo.release_date().is_none())
2✔
1867
                        .count()
1868
                        .is_one(),
1869
                    "Expected one liquid coinbase UTXO"
1870
                );
1871
            } else {
1872
                let announced_outputs = global_state_lock
1873
                    .lock_guard()
1874
                    .await
1875
                    .wallet_state
1876
                    .scan_for_utxos_announced_to_known_keys(&transaction.kernel)
1877
                    .collect_vec();
1878
                assert_eq!(2, announced_outputs.len());
1879
                assert_eq!(
1880
                    1,
1881
                    announced_outputs
1882
                        .iter()
1883
                        .filter(|x| x.utxo.release_date().is_some())
4✔
1884
                        .count()
1885
                );
1886
                assert_eq!(
1887
                    1,
1888
                    announced_outputs
1889
                        .iter()
1890
                        .filter(|x| x.utxo.release_date().is_none())
4✔
1891
                        .count()
1892
                );
1893
            }
1894
        }
1895
    }
1896

1897
    #[test]
1898
    fn block_hash_relates_to_predecessor_difficulty() {
1✔
1899
        let difficulty = 100u32;
1✔
1900
        // Difficulty X means we expect X trials before success.
1✔
1901
        // Modeling the process as a geometric distribution gives the
1✔
1902
        // probability of success in a single trial, p = 1/X.
1✔
1903
        // Then the probability of seeing k failures is (1-1/X)^k.
1✔
1904
        // We want this to be five nines certain that we do get a success
1✔
1905
        // after k trials, so this quantity must be less than 0.0001.
1✔
1906
        // So: log_10 0.0001 = -4 > log_10 (1-1/X)^k = k * log_10 (1 - 1/X).
1✔
1907
        // Difficulty 100 sets k = 917.
1✔
1908
        let cofactor = (1.0 - (1.0 / f64::from(difficulty))).log10();
1✔
1909
        let k = (-4.0 / cofactor).ceil() as usize;
1✔
1910

1✔
1911
        let mut predecessor_header = random_block_header();
1✔
1912
        predecessor_header.difficulty = Difficulty::from(difficulty);
1✔
1913
        let predecessor_body = BlockBody::new(
1✔
1914
            random_transaction_kernel(),
1✔
1915
            random_mutator_set_accumulator(),
1✔
1916
            random_mmra(),
1✔
1917
            random_mmra(),
1✔
1918
        );
1✔
1919
        let appendix = BlockAppendix::default();
1✔
1920
        let predecessor_block = Block::new(
1✔
1921
            predecessor_header,
1✔
1922
            predecessor_body,
1✔
1923
            appendix.clone(),
1✔
1924
            BlockProof::Invalid,
1✔
1925
        );
1✔
1926

1✔
1927
        let mut successor_header = random_block_header();
1✔
1928
        successor_header.prev_block_digest = predecessor_block.hash();
1✔
1929
        // note that successor's difficulty is random
1✔
1930
        let successor_body = BlockBody::new(
1✔
1931
            random_transaction_kernel(),
1✔
1932
            random_mutator_set_accumulator(),
1✔
1933
            random_mmra(),
1✔
1934
            random_mmra(),
1✔
1935
        );
1✔
1936

1✔
1937
        let mut rng = rand::rng();
1✔
1938
        let mut counter = 0;
1✔
1939
        let mut successor_block = Block::new(
1✔
1940
            successor_header,
1✔
1941
            successor_body.clone(),
1✔
1942
            appendix,
1✔
1943
            BlockProof::Invalid,
1✔
1944
        );
1✔
1945
        loop {
1946
            successor_block.set_header_nonce(rng.random());
193✔
1947

193✔
1948
            if successor_block.has_proof_of_work(predecessor_block.header()) {
193✔
1949
                break;
1✔
1950
            }
192✔
1951

192✔
1952
            counter += 1;
192✔
1953

192✔
1954
            assert!(
192✔
1955
                counter < k,
192✔
1956
                "number of hash trials before finding valid pow exceeds statistical limit"
×
1957
            )
1958
        }
1959
    }
1✔
1960
}
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