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

tari-project / tari / 16467678859

23 Jul 2025 10:05AM UTC coverage: 54.21% (+0.004%) from 54.206%
16467678859

push

github

web-flow
docs: update ffi interface spec (#7367)

Description
---
Updated the FFI interface specification for `fn wallet_start_recovery`

Motivation and Context
---
The function interface was changed recently.

How Has This Been Tested?
---
The specification text was compared against the code in `pub async fn
recovery_event_monitoring(..)`

What process can a PR reviewer use to test or verify this change?
---
Code review

<!-- Checklist -->
<!-- 1. Is the title of your PR in the form that would make nice release
notes? The title, excluding the conventional commit
tag, will be included exactly as is in the CHANGELOG, so please think
about it carefully. -->


Breaking Changes
---

- [x] None
- [ ] Requires data directory on base node to be deleted
- [ ] Requires hard fork
- [ ] Other - Please specify

<!-- Does this include a breaking change? If so, include this line as a
footer -->
<!-- BREAKING CHANGE: Description what the user should do, e.g. delete a
database, resync the chain -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Updated the documentation for the wallet recovery progress callback to
clarify the event types and their arguments, simplifying descriptions
and removing outdated event information. No changes were made to
functionality or public interfaces.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

75384 of 139060 relevant lines covered (54.21%)

195873.36 hits per line

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

59.59
/base_layer/core/src/validation/helpers.rs
1
// Copyright 2019. The Tari Project
2
//
3
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
4
// following conditions are met:
5
//
6
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
7
// disclaimer.
8
//
9
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
10
// following disclaimer in the documentation and/or other materials provided with the distribution.
11
//
12
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
13
// products derived from this software without specific prior written permission.
14
//
15
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
16
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
18
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
20
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
21
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22

23
use std::convert::TryFrom;
24

25
use log::*;
26
use tari_common_types::{
27
    epoch::VnEpoch,
28
    types::{CompressedPublicKey, FixedHash},
29
};
30
use tari_crypto::tari_utilities::{epoch_time::EpochTime, hex::Hex};
31
use tari_script::TariScript;
32
use tari_sidechain::SidechainProofValidationError;
33

34
use crate::{
35
    blocks::{BlockHeader, BlockHeaderValidationError, BlockValidationError},
36
    borsh::SerializedSize,
37
    chain_storage::{BlockchainBackend, MmrRoots, MmrTree},
38
    consensus::{ConsensusConstants, ConsensusManager},
39
    covenants::Covenant,
40
    proof_of_work::{
41
        monero_randomx_difficulty,
42
        randomx_factory::RandomXFactory,
43
        sha3x_difficulty,
44
        tari_randomx_difficulty,
45
        AchievedTargetDifficulty,
46
        Difficulty,
47
        PowAlgorithm,
48
        PowError,
49
    },
50
    transactions::transaction_components::{
51
        encrypted_data::STATIC_ENCRYPTED_DATA_SIZE_TOTAL,
52
        EncryptedData,
53
        TransactionInput,
54
        TransactionKernel,
55
        TransactionOutput,
56
    },
57
    validation::ValidationError,
58
};
59

60
pub const LOG_TARGET: &str = "c::val::helpers";
61

62
/// Returns the median timestamp for the provided timestamps.
63
///
64
/// ## Panics
65
/// When an empty slice is given as this is undefined for median average.
66
/// https://math.stackexchange.com/a/3451015
67
pub fn calc_median_timestamp(timestamps: &[EpochTime]) -> Result<EpochTime, ValidationError> {
22✔
68
    let mut timestamps: Vec<EpochTime> = timestamps.to_vec();
22✔
69
    timestamps.sort();
22✔
70
    trace!(
22✔
71
        target: LOG_TARGET,
×
72
        "Calculate the median timestamp from {} timestamps",
×
73
        timestamps.len()
×
74
    );
75
    if timestamps.is_empty() {
22✔
76
        return Err(ValidationError::IncorrectNumberOfTimestampsProvided { expected: 1, actual: 0 });
1✔
77
    }
21✔
78

21✔
79
    let mid_index = timestamps.len() / 2;
21✔
80
    let median_timestamp = if timestamps.len() % 2 == 0 {
21✔
81
        trace!(
9✔
82
            target: LOG_TARGET,
×
83
            "No median timestamp available, estimating median as avg of [{}] and [{}]",
×
84
            timestamps[mid_index - 1],
×
85
            timestamps[mid_index],
×
86
        );
87
        // To compute this mean, we use `u128` to avoid overflow with the internal `u64` typing
88
        // Note that the final cast back to `u64` will never truncate since each summand is bounded by `u64`
89
        // To make the linter happy, we use `u64::MAX` in the impossible case that the cast fails
90
        EpochTime::from(
9✔
91
            u64::try_from(
9✔
92
                (u128::from(timestamps[mid_index - 1].as_u64()) + u128::from(timestamps[mid_index].as_u64())) / 2,
9✔
93
            )
9✔
94
            .unwrap_or(u64::MAX),
9✔
95
        )
9✔
96
    } else {
97
        timestamps[mid_index]
12✔
98
    };
99
    trace!(target: LOG_TARGET, "Median timestamp:{}", median_timestamp);
21✔
100
    Ok(median_timestamp)
21✔
101
}
22✔
102
pub fn check_header_timestamp_greater_than_median(
14✔
103
    block_header: &BlockHeader,
14✔
104
    timestamps: &[EpochTime],
14✔
105
) -> Result<(), ValidationError> {
14✔
106
    if timestamps.is_empty() {
14✔
107
        // unreachable due to sanity_check_timestamp_count
108
        return Err(ValidationError::BlockHeaderError(
×
109
            BlockHeaderValidationError::InvalidTimestamp("The timestamp is empty".to_string()),
×
110
        ));
×
111
    }
14✔
112

113
    let median_timestamp = calc_median_timestamp(timestamps)?;
14✔
114
    if block_header.timestamp <= median_timestamp {
14✔
115
        warn!(
×
116
            target: LOG_TARGET,
×
117
            "Block header timestamp {} is less or equal than median timestamp: {} for block:{}",
×
118
            block_header.timestamp,
×
119
            median_timestamp,
×
120
            block_header.hash().to_hex()
×
121
        );
122
        return Err(ValidationError::BlockHeaderError(
×
123
            BlockHeaderValidationError::InvalidTimestamp(format!(
×
124
                "The timestamp `{}` was less or equal than the median timestamp `{}`",
×
125
                block_header.timestamp, median_timestamp
×
126
            )),
×
127
        ));
×
128
    }
14✔
129

14✔
130
    Ok(())
14✔
131
}
14✔
132
pub fn check_target_difficulty(
186✔
133
    block_header: &BlockHeader,
186✔
134
    target: Difficulty,
186✔
135
    randomx_factory: &RandomXFactory,
186✔
136
    gen_hash: &FixedHash,
186✔
137
    consensus: &ConsensusManager,
186✔
138
    tari_vm_key: FixedHash,
186✔
139
) -> Result<AchievedTargetDifficulty, ValidationError> {
186✔
140
    let achieved = match block_header.pow_algo() {
186✔
141
        PowAlgorithm::RandomXM => monero_randomx_difficulty(block_header, randomx_factory, gen_hash, consensus)?,
×
142
        PowAlgorithm::RandomXT => tari_randomx_difficulty(block_header, randomx_factory, &tari_vm_key)?,
×
143
        PowAlgorithm::Sha3x => sha3x_difficulty(block_header)?,
186✔
144
    };
145
    match AchievedTargetDifficulty::try_construct(block_header.pow_algo(), target, achieved) {
186✔
146
        Some(achieved_target) => Ok(achieved_target),
186✔
147
        None => {
148
            warn!(
×
149
                target: LOG_TARGET,
×
150
                "Proof of work for {} at height {} was below the target difficulty. Achieved: {}, Target: {}",
×
151
                block_header.hash().to_hex(),
×
152
                block_header.height,
153
                achieved,
154
                target
155
            );
156
            Err(ValidationError::BlockHeaderError(
×
157
                BlockHeaderValidationError::ProofOfWorkError(PowError::AchievedDifficultyTooLow { achieved, target }),
×
158
            ))
×
159
        },
160
    }
161
}
186✔
162

163
pub fn is_all_unique_and_sorted<'a, I: IntoIterator<Item = &'a T>, T: PartialOrd + 'a>(items: I) -> bool {
66✔
164
    let mut items = items.into_iter();
66✔
165
    let prev_item = items.next();
66✔
166
    if prev_item.is_none() {
66✔
167
        return true;
4✔
168
    }
62✔
169
    let mut prev_item = prev_item.unwrap();
62✔
170
    for item in items {
101✔
171
        if item <= prev_item {
44✔
172
            return false;
5✔
173
        }
39✔
174
        prev_item = item;
39✔
175
    }
176

177
    true
57✔
178
}
66✔
179

180
/// This function checks that an input is a valid spendable UTXO in the database. It cannot confirm
181
/// zero confermation transactions.
182
pub fn check_input_is_utxo<B: BlockchainBackend>(db: &B, input: &TransactionInput) -> Result<(), ValidationError> {
11✔
183
    let output_hash = input.output_hash();
11✔
184
    if let Some(utxo_hash) = db.fetch_unspent_output_hash_by_commitment(input.commitment()?)? {
11✔
185
        // We know that the commitment exists in the UTXO set. Check that the output hash matches (i.e. all fields
186
        // like output features match)
187
        if utxo_hash == output_hash {
8✔
188
            // Because the retrieved hash matches the new input.output_hash() we know all the fields match and are all
189
            // still the same
190
            return Ok(());
5✔
191
        }
3✔
192

193
        let output = db.fetch_output(&utxo_hash)?;
3✔
194
        warn!(
3✔
195
            target: LOG_TARGET,
×
196
            "Input spends a UTXO but does not produce the same hash as the output it spends: Expected hash: {}, \
×
197
             provided hash:{}
×
198
            input: {:?}. output in db: {:?}",
×
199
            utxo_hash.to_hex(),
×
200
            output_hash.to_hex(),
×
201
            input,
202
            output
203
        );
204

205
        return Err(ValidationError::UnknownInput);
3✔
206
    }
3✔
207

3✔
208
    // Wallet needs to know if a transaction has already been mined and uses this error variant to do so.
3✔
209
    if db.fetch_output(&output_hash)?.is_some() {
3✔
210
        warn!(
×
211
            target: LOG_TARGET,
×
212
            "Validation failed due to already spent input: {}", input
×
213
        );
214
        // We know that the output here must be spent because `fetch_unspent_output_hash_by_commitment` would have
215
        // been Some
216
        return Err(ValidationError::ContainsSTxO);
×
217
    }
3✔
218

3✔
219
    debug!(
3✔
220
        target: LOG_TARGET,
×
221
        "Input ({}, {}) does not exist in the database yet", input.commitment()?.to_hex(), output_hash.to_hex()
×
222
    );
223
    Err(ValidationError::UnknownInput)
3✔
224
}
11✔
225

226
/// Checks the byte size of TariScript is less than or equal to the given size, otherwise returns an error.
227
pub fn check_tari_script_byte_size(script: &TariScript, max_script_size: usize) -> Result<(), ValidationError> {
567✔
228
    let script_size = script
567✔
229
        .get_serialized_size()
567✔
230
        .map_err(|e| ValidationError::SerializationError(format!("Failed to get serialized script size: {}", e)))?;
567✔
231
    if script_size > max_script_size {
567✔
232
        return Err(ValidationError::TariScriptExceedsMaxSize {
1✔
233
            max_script_size,
1✔
234
            actual_script_size: script_size,
1✔
235
        });
1✔
236
    }
566✔
237
    Ok(())
566✔
238
}
567✔
239

240
/// Checks the byte size of TariScript is less than or equal to the given size, otherwise returns an error.
241
pub fn check_tari_encrypted_data_byte_size(
566✔
242
    encrypted_data: &EncryptedData,
566✔
243
    max_encrypted_data_size: usize,
566✔
244
) -> Result<(), ValidationError> {
566✔
245
    let encrypted_data_size = encrypted_data.as_bytes().len();
566✔
246
    if encrypted_data_size > max_encrypted_data_size + STATIC_ENCRYPTED_DATA_SIZE_TOTAL {
566✔
247
        return Err(ValidationError::EncryptedDataExceedsMaxSize {
1✔
248
            max_encrypted_data_size: max_encrypted_data_size + STATIC_ENCRYPTED_DATA_SIZE_TOTAL,
1✔
249
            actual_encrypted_data_size: encrypted_data_size,
1✔
250
        });
1✔
251
    }
565✔
252
    Ok(())
565✔
253
}
566✔
254

255
/// This function checks that the outputs do not already exist in the TxO set.
256
pub fn check_not_duplicate_txo<B: BlockchainBackend>(
509✔
257
    db: &B,
509✔
258
    output: &TransactionOutput,
509✔
259
) -> Result<(), ValidationError> {
509✔
260
    if db
509✔
261
        .fetch_unspent_output_hash_by_commitment(&output.commitment)?
509✔
262
        .is_some()
509✔
263
    {
264
        warn!(
×
265
            target: LOG_TARGET,
×
266
            "Duplicate UTXO set commitment found for output: {}", output
×
267
        );
268
        return Err(ValidationError::ContainsDuplicateUtxoCommitment);
×
269
    }
509✔
270

509✔
271
    Ok(())
509✔
272
}
509✔
273
/// This function checks the validity of the validator node registration if applicable
274
pub fn check_validator_node_registration<B: BlockchainBackend>(
509✔
275
    db: &B,
509✔
276
    output: &TransactionOutput,
509✔
277
    current_epoch: VnEpoch,
509✔
278
) -> Result<(), ValidationError> {
509✔
279
    let Some(sidechain_features) = output.features.sidechain_feature.as_ref() else {
509✔
280
        return Ok(());
509✔
281
    };
282
    let Some(vn_reg) = sidechain_features.validator_node_registration() else {
×
283
        return Ok(());
×
284
    };
285

286
    if vn_reg.max_epoch() < current_epoch {
×
287
        return Err(ValidationError::ValidatorNodeRegistrationMaxEpoch {
×
288
            public_key: vn_reg.public_key().to_string(),
×
289
            max_epoch: vn_reg.max_epoch(),
×
290
            current_epoch,
×
291
        });
×
292
    }
×
293

×
294
    if db.validator_node_exists(
×
295
        sidechain_features.sidechain_public_key(),
×
296
        current_epoch,
×
297
        vn_reg.public_key(),
×
298
    )? {
×
299
        return Err(ValidationError::ValidatorNodeAlreadyRegistered {
×
300
            public_key: vn_reg.public_key().to_string(),
×
301
        });
×
302
    }
×
303

×
304
    Ok(())
×
305
}
509✔
306

307
/// Checks the validity of the validator node exit if applicable
308
pub fn check_validator_node_exit<B: BlockchainBackend>(
509✔
309
    db: &B,
509✔
310
    output: &TransactionOutput,
509✔
311
    current_epoch: VnEpoch,
509✔
312
) -> Result<(), ValidationError> {
509✔
313
    let Some(sidechain_features) = output.features.sidechain_feature.as_ref() else {
509✔
314
        return Ok(());
509✔
315
    };
316
    let Some(exit) = sidechain_features.validator_node_exit() else {
×
317
        return Ok(());
×
318
    };
319

320
    if exit.max_epoch() < current_epoch {
×
321
        return Err(ValidationError::ValidatorNodeRegistrationMaxEpoch {
×
322
            public_key: exit.public_key().to_string(),
×
323
            max_epoch: exit.max_epoch(),
×
324
            current_epoch,
×
325
        });
×
326
    }
×
327

×
328
    if !db.validator_node_is_active(
×
329
        sidechain_features.sidechain_public_key(),
×
330
        current_epoch,
×
331
        exit.public_key(),
×
332
    )? {
×
333
        return Err(ValidationError::ValidatorNodeNotRegistered {
×
334
            public_key: exit.public_key().to_string(),
×
335
            details: format!("exit invalid for validator node that is not active/registered in {current_epoch}"),
×
336
        });
×
337
    }
×
338

×
339
    Ok(())
×
340
}
509✔
341

342
/// This function checks the validity of the eviction proof if applicable
343
pub fn check_eviction_proof<B: BlockchainBackend>(
509✔
344
    db: &B,
509✔
345
    output: &TransactionOutput,
509✔
346
    constants: &ConsensusConstants,
509✔
347
) -> Result<(), ValidationError> {
509✔
348
    let Some(sidechain_features) = output.features.sidechain_feature.as_ref() else {
509✔
349
        return Ok(());
509✔
350
    };
351
    let Some(eviction_proof) = sidechain_features.eviction_proof() else {
×
352
        return Ok(());
×
353
    };
354

355
    let epoch = eviction_proof.epoch();
×
356
    let shard_group = eviction_proof.shard_group();
×
357

358
    let chain_metadata = db.fetch_chain_metadata()?;
×
359
    let tip_height = chain_metadata.best_block_height();
×
360
    let tip_epoch = constants.block_height_to_epoch(tip_height);
×
361
    if epoch > tip_epoch {
×
362
        return Err(ValidationError::SidechainEvictionProofInvalidEpoch {
×
363
            epoch,
×
364
            tip_height: chain_metadata.best_block_height(),
×
365
        });
×
366
    }
×
367

×
368
    let validator_pk = eviction_proof.node_to_evict();
×
369

×
370
    // Only allow a single exit or evict on an active validator
×
371
    if !db.validator_node_is_active_for_shard_group(
×
372
        sidechain_features.sidechain_public_key(),
×
373
        tip_epoch,
×
374
        validator_pk,
×
375
        shard_group,
×
376
    )? {
×
377
        return Err(ValidationError::SidechainEvictionProofValidatorNotFound {
×
378
            validator_pk: validator_pk.to_string(),
×
379
        });
×
380
    }
×
381

382
    let committee_size =
×
383
        db.validator_nodes_count_for_shard_group(sidechain_features.sidechain_public_key(), tip_epoch, shard_group)?;
×
384
    let quorum_threshold = committee_size - (committee_size - 1) / 3;
×
385

×
386
    let sidechain_pk = sidechain_features.sidechain_public_key();
×
387

×
388
    let check_vn = |public_key: &CompressedPublicKey| {
×
389
        let is_active = db
×
390
            .validator_node_is_active_for_shard_group(sidechain_pk, tip_epoch, public_key, shard_group)
×
391
            .map_err(SidechainProofValidationError::internal_error)?;
×
392

393
        Ok(is_active)
×
394
    };
×
395

396
    eviction_proof.validate(quorum_threshold, &check_vn)?;
×
397

398
    Ok(())
×
399
}
509✔
400

401
#[allow(clippy::too_many_lines)]
402
pub fn check_mmr_roots(header: &BlockHeader, mmr_roots: &MmrRoots) -> Result<(), ValidationError> {
2✔
403
    if header.kernel_mr != mmr_roots.kernel_mr {
2✔
404
        warn!(
×
405
            target: LOG_TARGET,
×
406
            "Block header kernel MMR roots in #{} {} do not match calculated roots. Expected: {}, Actual:{}",
×
407
            header.height,
×
408
            header.hash().to_hex(),
×
409
            header.kernel_mr.to_hex(),
×
410
            mmr_roots.kernel_mr.to_hex()
×
411
        );
412
        return Err(ValidationError::BlockError(BlockValidationError::MismatchedMmrRoots {
×
413
            kind: "Kernel",
×
414
        }));
×
415
    };
2✔
416
    if header.kernel_mmr_size != mmr_roots.kernel_mmr_size {
2✔
417
        warn!(
×
418
            target: LOG_TARGET,
×
419
            "Block header kernel MMR size in #{} {} does not match. Expected: {}, Actual:{}",
×
420
            header.height,
×
421
            header.hash().to_hex(),
×
422
            header.kernel_mmr_size,
423
            mmr_roots.kernel_mmr_size
424
        );
425
        return Err(ValidationError::BlockError(BlockValidationError::MismatchedMmrSize {
×
426
            mmr_tree: MmrTree::Kernel.to_string(),
×
427
            expected: mmr_roots.kernel_mmr_size,
×
428
            actual: header.kernel_mmr_size,
×
429
        }));
×
430
    }
2✔
431
    if header.output_mr != mmr_roots.output_mr {
2✔
432
        warn!(
×
433
            target: LOG_TARGET,
×
434
            "Block header output MMR roots in #{} {} do not match calculated roots. Expected: {}, Actual:{}",
×
435
            header.height,
×
436
            header.hash().to_hex(),
×
437
            header.output_mr.to_hex(),
×
438
            mmr_roots.output_mr.to_hex()
×
439
        );
440
        return Err(ValidationError::BlockError(BlockValidationError::MismatchedMmrRoots {
×
441
            kind: "Utxos",
×
442
        }));
×
443
    };
2✔
444
    if header.output_smt_size != mmr_roots.output_smt_size {
2✔
445
        warn!(
×
446
            target: LOG_TARGET,
×
447
            "Block header output MMR size in {} does not match. Expected: {}, Actual: {}",
×
448
            header.hash().to_hex(),
×
449
            header.output_smt_size,
450
            mmr_roots.output_smt_size
451
        );
452
        return Err(ValidationError::BlockError(BlockValidationError::MismatchedMmrSize {
×
453
            mmr_tree: "UTXO".to_string(),
×
454
            expected: mmr_roots.output_smt_size,
×
455
            actual: header.output_smt_size,
×
456
        }));
×
457
    };
2✔
458
    if header.block_output_mr != mmr_roots.block_output_mr {
2✔
459
        warn!(
×
460
            target: LOG_TARGET,
×
461
            "Block header block output MMR roots in #{} {} do not match calculated roots. Expected: {}, Actual:{}",
×
462
            header.height,
×
463
            header.hash().to_hex(),
×
464
            header.block_output_mr,
465
            mmr_roots.block_output_mr,
466
        );
467
        return Err(ValidationError::BlockError(BlockValidationError::MismatchedMmrRoots {
×
468
            kind: "block outputs",
×
469
        }));
×
470
    };
2✔
471
    if header.input_mr != mmr_roots.input_mr {
2✔
472
        warn!(
×
473
            target: LOG_TARGET,
×
474
            "Block header input merkle root in {} do not match calculated root. Header.input_mr: {}, Calculated: {}",
×
475
            header.hash().to_hex(),
×
476
            header.input_mr.to_hex(),
×
477
            mmr_roots.input_mr.to_hex()
×
478
        );
479
        return Err(ValidationError::BlockError(BlockValidationError::MismatchedMmrRoots {
×
480
            kind: "Input",
×
481
        }));
×
482
    }
2✔
483
    if header.validator_node_mr != mmr_roots.validator_node_mr {
2✔
484
        warn!(
×
485
            target: LOG_TARGET,
×
486
            "Block header validator node merkle root in {} do not match calculated root. Header.validator_node_mr: \
×
487
             {}, Calculated: {}",
×
488
            header.hash().to_hex(),
×
489
            header.validator_node_mr.to_hex(),
×
490
            mmr_roots.validator_node_mr.to_hex()
×
491
        );
492
        return Err(ValidationError::BlockError(BlockValidationError::MismatchedMmrRoots {
×
493
            kind: "Validator Node",
×
494
        }));
×
495
    }
2✔
496

2✔
497
    if header.validator_node_size != mmr_roots.validator_node_size {
2✔
498
        warn!(
×
499
            target: LOG_TARGET,
×
500
            "Block header validator size in #{} {} does not match. Expected: {}, Actual:{}",
×
501
            header.height,
×
502
            header.hash().to_hex(),
×
503
            header.validator_node_size,
504
            mmr_roots.validator_node_size
505
        );
506
        return Err(ValidationError::BlockError(BlockValidationError::MismatchedMmrSize {
×
507
            mmr_tree: "Validator_node".to_string(),
×
508
            expected: mmr_roots.validator_node_size,
×
509
            actual: header.validator_node_size,
×
510
        }));
×
511
    }
2✔
512
    Ok(())
2✔
513
}
2✔
514

515
pub fn check_permitted_output_types(
57✔
516
    constants: &ConsensusConstants,
57✔
517
    output: &TransactionOutput,
57✔
518
) -> Result<(), ValidationError> {
57✔
519
    if !constants
57✔
520
        .permitted_output_types()
57✔
521
        .contains(&output.features.output_type)
57✔
522
    {
523
        return Err(ValidationError::OutputTypeNotPermitted {
1✔
524
            output_type: output.features.output_type,
1✔
525
        });
1✔
526
    }
56✔
527

56✔
528
    Ok(())
56✔
529
}
57✔
530

531
pub fn check_covenant_length(covenant: &Covenant, max_token_len: u32) -> Result<(), ValidationError> {
56✔
532
    if covenant.num_tokens() > max_token_len as usize {
56✔
533
        return Err(ValidationError::CovenantTooLarge {
×
534
            max_size: max_token_len as usize,
×
535
            actual_size: covenant.num_tokens(),
×
536
        });
×
537
    }
56✔
538

56✔
539
    Ok(())
56✔
540
}
56✔
541

542
pub fn check_permitted_range_proof_types(
56✔
543
    constants: &ConsensusConstants,
56✔
544
    output: &TransactionOutput,
56✔
545
) -> Result<(), ValidationError> {
56✔
546
    let binding = constants.permitted_range_proof_types();
56✔
547
    let permitted_range_proof_types = binding.iter().find(|&&t| t.0 == output.features.output_type).ok_or(
63✔
548
        ValidationError::OutputTypeNotMatchedToRangeProofType {
56✔
549
            output_type: output.features.output_type,
56✔
550
        },
56✔
551
    )?;
56✔
552

553
    if !permitted_range_proof_types
55✔
554
        .1
55✔
555
        .contains(&output.features.range_proof_type)
55✔
556
    {
557
        return Err(ValidationError::RangeProofTypeNotPermitted {
1✔
558
            range_proof_type: output.features.range_proof_type,
1✔
559
        });
1✔
560
    }
54✔
561

54✔
562
    Ok(())
54✔
563
}
56✔
564

565
pub fn validate_input_version(
28✔
566
    consensus_constants: &ConsensusConstants,
28✔
567
    input: &TransactionInput,
28✔
568
) -> Result<(), ValidationError> {
28✔
569
    if !consensus_constants.input_version_range().contains(&input.version) {
28✔
570
        let msg = format!(
×
571
            "Transaction input contains a version not allowed by consensus ({:?})",
×
572
            input.version
×
573
        );
×
574
        return Err(ValidationError::ConsensusError(msg));
×
575
    }
28✔
576

28✔
577
    Ok(())
28✔
578
}
28✔
579

580
pub fn validate_output_version(
552✔
581
    consensus_constants: &ConsensusConstants,
552✔
582
    output: &TransactionOutput,
552✔
583
) -> Result<(), ValidationError> {
552✔
584
    let valid_output_version = consensus_constants
552✔
585
        .output_version_range()
552✔
586
        .outputs
552✔
587
        .contains(&output.version);
552✔
588

552✔
589
    if !valid_output_version {
552✔
590
        let msg = format!(
×
591
            "Transaction output version is not allowed by consensus ({:?})",
×
592
            output.version
×
593
        );
×
594
        return Err(ValidationError::ConsensusError(msg));
×
595
    }
552✔
596

552✔
597
    let valid_features_version = consensus_constants
552✔
598
        .output_version_range()
552✔
599
        .features
552✔
600
        .contains(&output.features.version);
552✔
601

552✔
602
    if !valid_features_version {
552✔
603
        let msg = format!(
×
604
            "Transaction output features version is not allowed by consensus ({:?})",
×
605
            output.features.version
×
606
        );
×
607
        return Err(ValidationError::ConsensusError(msg));
×
608
    }
552✔
609

610
    for opcode in output.script.as_slice() {
552✔
611
        if !consensus_constants
552✔
612
            .output_version_range()
552✔
613
            .opcode
552✔
614
            .contains(&opcode.get_version())
552✔
615
        {
616
            let msg = format!(
×
617
                "Transaction output script opcode is not allowed by consensus ({})",
×
618
                opcode
×
619
            );
×
620
            return Err(ValidationError::ConsensusError(msg));
×
621
        }
552✔
622
    }
623

624
    Ok(())
552✔
625
}
552✔
626

627
pub fn validate_kernel_version(
34✔
628
    consensus_constants: &ConsensusConstants,
34✔
629
    kernel: &TransactionKernel,
34✔
630
) -> Result<(), ValidationError> {
34✔
631
    if !consensus_constants.kernel_version_range().contains(&kernel.version) {
34✔
632
        let msg = format!(
×
633
            "Transaction kernel version is not allowed by consensus ({:?})",
×
634
            kernel.version
×
635
        );
×
636
        return Err(ValidationError::ConsensusError(msg));
×
637
    }
34✔
638
    Ok(())
34✔
639
}
34✔
640

641
#[cfg(test)]
642
mod test {
643
    use tari_test_utils::unpack_enum;
644

645
    use super::*;
646
    use crate::transactions::{test_helpers, test_helpers::TestParams, CryptoFactories};
647

648
    mod is_all_unique_and_sorted {
649
        use super::*;
650

651
        #[test]
652
        fn it_returns_true_when_nothing_to_compare() {
1✔
653
            assert!(is_all_unique_and_sorted::<_, usize>(&[]));
1✔
654
            assert!(is_all_unique_and_sorted(&[1]));
1✔
655
        }
1✔
656

657
        #[test]
658
        fn it_returns_true_when_unique_and_sorted() {
1✔
659
            let v = [1, 2, 3, 4, 5];
1✔
660
            assert!(is_all_unique_and_sorted(&v));
1✔
661
        }
1✔
662

663
        #[test]
664
        fn it_returns_false_when_unsorted() {
1✔
665
            let v = [2, 1, 3, 4, 5];
1✔
666
            assert!(!is_all_unique_and_sorted(&v));
1✔
667
        }
1✔
668

669
        #[test]
670
        fn it_returns_false_when_duplicate() {
1✔
671
            let v = [1, 2, 3, 4, 4];
1✔
672
            assert!(!is_all_unique_and_sorted(&v));
1✔
673
        }
1✔
674

675
        #[test]
676
        fn it_returns_false_when_duplicate_and_unsorted() {
1✔
677
            let v = [4, 2, 3, 0, 4];
1✔
678
            assert!(!is_all_unique_and_sorted(&v));
1✔
679
        }
1✔
680
    }
681

682
    mod calc_median_timestamp {
683
        use super::*;
684

685
        #[test]
686
        fn it_errors_on_empty() {
1✔
687
            assert!(calc_median_timestamp(&[]).is_err());
1✔
688
        }
1✔
689

690
        #[test]
691
        fn it_calculates_the_correct_median_timestamp() {
1✔
692
            let median_timestamp = calc_median_timestamp(&[0.into()]).unwrap();
1✔
693
            assert_eq!(median_timestamp, 0.into());
1✔
694

695
            let median_timestamp = calc_median_timestamp(&[123.into()]).unwrap();
1✔
696
            assert_eq!(median_timestamp, 123.into());
1✔
697

698
            let median_timestamp = calc_median_timestamp(&[2.into(), 4.into()]).unwrap();
1✔
699
            assert_eq!(median_timestamp, 3.into());
1✔
700

701
            let median_timestamp = calc_median_timestamp(&[0.into(), 100.into(), 0.into()]).unwrap();
1✔
702
            assert_eq!(median_timestamp, 0.into());
1✔
703

704
            let median_timestamp = calc_median_timestamp(&[1.into(), 2.into(), 3.into(), 4.into()]).unwrap();
1✔
705
            assert_eq!(median_timestamp, 2.into());
1✔
706

707
            let median_timestamp = calc_median_timestamp(&[1.into(), 2.into(), 3.into(), 4.into(), 5.into()]).unwrap();
1✔
708
            assert_eq!(median_timestamp, 3.into());
1✔
709
        }
1✔
710
    }
711

712
    mod check_coinbase_maturity {
713
        use futures::executor::block_on;
714

715
        use super::*;
716
        use crate::transactions::{
717
            aggregated_body::AggregateBody,
718
            transaction_components::{RangeProofType, TransactionError},
719
            transaction_key_manager::create_memory_db_key_manager,
720
        };
721

722
        #[tokio::test]
723
        async fn it_succeeds_for_valid_coinbase() {
1✔
724
            let height = 1;
1✔
725
            let key_manager = create_memory_db_key_manager().unwrap();
1✔
726
            let test_params = TestParams::new(&key_manager).await;
1✔
727
            let rules = test_helpers::create_consensus_manager();
1✔
728
            let key_manager = create_memory_db_key_manager().unwrap();
1✔
729
            let coinbase = block_on(test_helpers::create_coinbase_wallet_output(
1✔
730
                &test_params,
1✔
731
                height,
1✔
732
                None,
1✔
733
                RangeProofType::RevealedValue,
1✔
734
            ));
1✔
735
            let coinbase_output = coinbase.to_transaction_output(&key_manager).await.unwrap();
1✔
736
            let coinbase_kernel = test_helpers::create_coinbase_kernel(&coinbase.spending_key_id, &key_manager).await;
1✔
737

1✔
738
            let body = AggregateBody::new(vec![], vec![coinbase_output], vec![coinbase_kernel]);
1✔
739

1✔
740
            let reward = rules.calculate_coinbase_and_fees(height, body.kernels()).unwrap();
1✔
741
            let coinbase_lock_height = rules.consensus_constants(height).coinbase_min_maturity();
1✔
742
            body.check_coinbase_output(reward, coinbase_lock_height, &CryptoFactories::default(), height, 1)
1✔
743
                .unwrap();
1✔
744
        }
1✔
745

746
        #[tokio::test]
747
        async fn it_returns_error_for_invalid_coinbase_maturity() {
1✔
748
            let height = 1;
1✔
749
            let key_manager = create_memory_db_key_manager().unwrap();
1✔
750
            let test_params = TestParams::new(&key_manager).await;
1✔
751
            let rules = test_helpers::create_consensus_manager();
1✔
752
            let mut coinbase =
1✔
753
                test_helpers::create_coinbase_wallet_output(&test_params, height, None, RangeProofType::RevealedValue)
1✔
754
                    .await;
1✔
755
            coinbase.features.maturity = 0;
1✔
756
            let coinbase_output = coinbase.to_transaction_output(&key_manager).await.unwrap();
1✔
757
            let coinbase_kernel = test_helpers::create_coinbase_kernel(&coinbase.spending_key_id, &key_manager).await;
1✔
758

1✔
759
            let body = AggregateBody::new(vec![], vec![coinbase_output], vec![coinbase_kernel]);
1✔
760

1✔
761
            let reward = rules.calculate_coinbase_and_fees(height, body.kernels()).unwrap();
1✔
762
            let coinbase_lock_height = rules.consensus_constants(height).coinbase_min_maturity();
1✔
763

1✔
764
            let err = body
1✔
765
                .check_coinbase_output(reward, coinbase_lock_height, &CryptoFactories::default(), height, 1)
1✔
766
                .unwrap_err();
1✔
767
            unpack_enum!(TransactionError::InvalidCoinbaseMaturity = err);
1✔
768
        }
1✔
769

770
        #[tokio::test]
771
        async fn it_returns_error_for_invalid_coinbase_reward() {
1✔
772
            let height = 1;
1✔
773
            let key_manager = create_memory_db_key_manager().unwrap();
1✔
774
            let test_params = TestParams::new(&key_manager).await;
1✔
775
            let rules = test_helpers::create_consensus_manager();
1✔
776
            let mut coinbase = test_helpers::create_coinbase_wallet_output(
1✔
777
                &test_params,
1✔
778
                height,
1✔
779
                None,
1✔
780
                RangeProofType::BulletProofPlus,
1✔
781
            )
1✔
782
            .await;
1✔
783
            coinbase.value = 123.into();
1✔
784
            let coinbase_output = coinbase.to_transaction_output(&key_manager).await.unwrap();
1✔
785
            let coinbase_kernel = test_helpers::create_coinbase_kernel(&coinbase.spending_key_id, &key_manager).await;
1✔
786

1✔
787
            let body = AggregateBody::new(vec![], vec![coinbase_output], vec![coinbase_kernel]);
1✔
788
            let reward = rules.calculate_coinbase_and_fees(height, body.kernels()).unwrap();
1✔
789
            let coinbase_lock_height = rules.consensus_constants(height).coinbase_min_maturity();
1✔
790

1✔
791
            let err = body
1✔
792
                .check_coinbase_output(reward, coinbase_lock_height, &CryptoFactories::default(), height, 1)
1✔
793
                .unwrap_err();
1✔
794
            unpack_enum!(TransactionError::InvalidCoinbase = err);
1✔
795
        }
1✔
796
    }
797
}
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