#![allow(clippy::indexing_slicing)]
use std::{cmp, sync::Arc};
use jmt::{JellyfishMerkleTree, KeyHash, mock::MockTreeStore};
use tari_common::configuration::Network;
use tari_common_types::types::{CompressedCommitment, UncompressedCommitment};
use tari_node_components::blocks::{BlockHeader, ChainBlock, ChainHeader};
use tari_script::TariScript;
use tari_test_utils::unpack_enum;
use tari_transaction_components::{
consensus::ConsensusConstantsBuilder,
crypto_factories::CryptoFactories,
key_manager::{KeyManager, TxoStage},
tari_amount::{MicroMinotari, uT},
test_helpers::{create_random_signature_from_secret_key, create_utxo},
transaction_components::{
KernelBuilder,
KernelFeatures,
OutputFeatures,
RangeProofType,
TransactionKernel,
covenants::Covenant,
},
tx,
};
use tari_utilities::ByteArray;
use crate::{
blocks::BlockHeaderAccumulatedDataBuilder,
chain_storage::{BlockchainBackend, BlockchainDatabase, ChainStorageError, DbTransaction, SmtHasher},
proof_of_work::AchievedTargetDifficulty,
test_helpers::{blockchain::create_store_with_consensus, create_chain_header},
validation::{ChainBalanceValidator, DifficultyCalculator, FinalHorizonStateValidation, ValidationError},
};
mod header_validators {
use tari_common_types::types::FixedHash;
use tari_utilities::epoch_time::EpochTime;
use super::*;
use crate::{
block_specs,
consensus::{BaseNodeConsensusManager, BaseNodeConsensusManagerBuilder},
test_helpers::blockchain::{create_main_chain, create_new_blockchain},
validation::{HeaderChainLinkedValidator, header::HeaderFullValidator},
};
#[test]
fn header_iter_empty_and_invalid_height() {
let consensus_manager = BaseNodeConsensusManager::builder(Network::LocalNet).build().unwrap();
let genesis = consensus_manager.get_genesis_block();
let db = create_store_with_consensus(consensus_manager);
let iter = HeaderIter::new(&db, 0, 10);
let headers = iter.map(Result::unwrap).collect::<Vec<_>>();
assert_eq!(headers.len(), 1);
assert_eq!(genesis.header(), &headers[0]);
let iter = HeaderIter::new(&db, 1, 10);
let headers = iter.collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(headers.len(), 1);
}
#[test]
fn header_iter_fetch_in_chunks() {
let consensus_manager = BaseNodeConsensusManagerBuilder::new(Network::LocalNet).build().unwrap();
let db = create_store_with_consensus(consensus_manager.clone());
let headers = (1..=15).fold(vec![db.fetch_chain_header(0).unwrap()], |mut acc, i| {
let prev = acc.last().unwrap();
let mut header = BlockHeader::new(0);
header.height = i;
header.prev_hash = *prev.hash();
header.kernel_mmr_size = 2 + i;
header.output_smt_size = 4001 + i;
let chain_header = create_chain_header(header, prev.accumulated_data());
acc.push(chain_header);
acc
});
db.insert_valid_headers(headers.into_iter().skip(1).collect()).unwrap();
let iter = HeaderIter::new(&db, 11, 3);
let headers = iter.map(Result::unwrap).collect::<Vec<_>>();
assert_eq!(headers.len(), 12);
let genesis = consensus_manager.get_genesis_block();
assert_eq!(genesis.header(), &headers[0]);
(1..=11).for_each(|i| {
assert_eq!(headers[i].height, i as u64);
})
}
#[test]
fn it_validates_that_version_is_in_range() {
let consensus_manager = BaseNodeConsensusManagerBuilder::new(Network::LocalNet).build().unwrap();
let db = create_store_with_consensus(consensus_manager.clone());
let genesis = db.fetch_chain_header(0).unwrap();
let mut header = BlockHeader::from_previous(genesis.header());
header.version = u16::MAX;
let difficulty_calculator = DifficultyCalculator::new(consensus_manager.clone(), Default::default());
let validator = HeaderFullValidator::new(consensus_manager, difficulty_calculator);
let err = validator
.validate(
&*db.db_read_access().unwrap(),
&header,
genesis.header(),
&[],
None,
FixedHash::zero(),
)
.unwrap_err();
assert!(matches!(err, ValidationError::InvalidBlockchainVersion {
version: u16::MAX
}));
}
#[tokio::test]
async fn it_does_a_sanity_check_on_the_number_of_timestamps_provided() {
let consensus_manager = BaseNodeConsensusManagerBuilder::new(Network::LocalNet).build().unwrap();
let db = create_new_blockchain();
let (_, blocks) = create_main_chain(&db, block_specs!(["1->GB"], ["2->1"], ["3->2"]));
let last_block = blocks.get("3").unwrap();
let candidate_header = BlockHeader::from_previous(last_block.header());
let difficulty_calculator = DifficultyCalculator::new(consensus_manager.clone(), Default::default());
let validator = HeaderFullValidator::new(consensus_manager, difficulty_calculator);
let mut timestamps = db.fetch_block_timestamps(*blocks.get("3").unwrap().hash()).unwrap();
validator
.validate(
&*db.db_read_access().unwrap(),
&candidate_header,
last_block.header(),
×tamps,
None,
FixedHash::zero(),
)
.unwrap();
timestamps.push(EpochTime::now());
let err = validator
.validate(
&*db.db_read_access().unwrap(),
&candidate_header,
last_block.header(),
×tamps,
None,
FixedHash::zero(),
)
.unwrap_err();
assert!(matches!(err, ValidationError::IncorrectNumberOfTimestampsProvided {
actual: 5,
expected: 4
}));
}
}
use crate::consensus::BaseNodeConsensusManagerBuilder;
#[tokio::test]
#[allow(clippy::too_many_lines)]
async fn chain_balance_validation() {
let factories = CryptoFactories::default();
let consensus_manager = BaseNodeConsensusManagerBuilder::new(Network::Esmeralda)
.build()
.unwrap();
let genesis = consensus_manager.get_genesis_block();
let pre_mine_value = 5000 * uT;
let key_manager = KeyManager::new_random().unwrap();
let (pre_mine_utxo, pre_mine_key_id, _) = create_utxo(
pre_mine_value,
&key_manager,
&OutputFeatures::default(),
&TariScript::default(),
&Covenant::default(),
MicroMinotari::zero(),
);
let (pk, sig) = create_random_signature_from_secret_key(
&key_manager,
pre_mine_key_id,
0.into(),
0,
KernelFeatures::empty(),
TxoStage::Output,
);
let excess = CompressedCommitment::from_public_key(pk.to_public_key().unwrap());
let kernel =
TransactionKernel::new_current_version(KernelFeatures::empty(), MicroMinotari::from(0), 0, excess, sig, None);
let mut gen_block = genesis.block().clone();
gen_block.body.add_output(pre_mine_utxo);
gen_block.body.add_kernels([kernel]);
let mut utxo_sum = UncompressedCommitment::default();
let mut kernel_sum = UncompressedCommitment::default();
let burned_sum = UncompressedCommitment::default();
let mock_store = MockTreeStore::new(true);
let smt = JellyfishMerkleTree::<_, SmtHasher>::new(&mock_store);
let mut batch = vec![];
for output in gen_block.body.outputs() {
utxo_sum = &output.commitment.to_commitment().unwrap() + &utxo_sum;
let smt_key = KeyHash(output.commitment.as_bytes().try_into().expect("commitment is 32 bytes"));
let smt_value = output.smt_hash(0);
batch.push((smt_key, Some(smt_value.to_vec())));
}
for input in gen_block.body.inputs() {
utxo_sum = &utxo_sum - &input.commitment().unwrap().to_commitment().unwrap();
let smt_key = KeyHash(
input
.commitment()
.unwrap()
.as_bytes()
.try_into()
.expect("Commitment is 32 bytes"),
);
batch.push((smt_key, None));
}
for kernel in gen_block.body.kernels() {
kernel_sum = &kernel.excess.to_commitment().unwrap() + &kernel_sum;
}
let root = smt.put_value_set(batch, 0).unwrap();
gen_block.header.output_mr = root.0.0.into();
let mut accum = genesis.accumulated_data().clone();
accum.hash = gen_block.header.hash();
let genesis = ChainBlock::try_construct(Arc::new(gen_block), accum).unwrap();
let total_pre_mine = pre_mine_value + consensus_manager.consensus_constants(0).pre_mine_value();
let constants = ConsensusConstantsBuilder::new(Network::LocalNet)
.with_consensus_constants(consensus_manager.consensus_constants(0).clone())
.with_pre_mine_value(total_pre_mine)
.build();
let consensus_manager = BaseNodeConsensusManagerBuilder::new(Network::LocalNet)
.with_block(genesis.clone())
.add_consensus_constants(constants)
.build()
.unwrap();
let db = create_store_with_consensus(consensus_manager.clone());
let validator = ChainBalanceValidator::new(consensus_manager.clone(), factories.clone());
validator
.validate(
&*db.db_read_access().unwrap(),
0,
&CompressedCommitment::from_commitment(utxo_sum.clone()),
&CompressedCommitment::from_commitment(kernel_sum.clone()),
&CompressedCommitment::from_commitment(burned_sum.clone()),
)
.unwrap();
let mut txn = DbTransaction::new();
let coinbase_value = consensus_manager.get_block_reward_at(1);
let (coinbase, coinbase_key_id, _) = create_utxo(
coinbase_value,
&key_manager,
&OutputFeatures::create_coinbase(1, None, RangeProofType::BulletProofPlus),
&TariScript::default(),
&Covenant::default(),
MicroMinotari::zero(),
);
let (pk, sig) = create_random_signature_from_secret_key(
&key_manager,
coinbase_key_id,
0.into(),
0,
KernelFeatures::create_coinbase(),
TxoStage::Output,
);
let excess = CompressedCommitment::from_compressed_key(pk);
let kernel = KernelBuilder::new()
.with_signature(sig)
.with_excess(&excess)
.with_features(KernelFeatures::COINBASE_KERNEL)
.build()
.unwrap();
let mut header1 = BlockHeader::from_previous(genesis.header());
header1.kernel_mmr_size += 1;
header1.output_smt_size += 1;
let achieved_difficulty = AchievedTargetDifficulty::try_construct(
genesis.header().pow_algo(),
genesis.accumulated_data().target_difficulty,
genesis.accumulated_data().achieved_difficulty,
)
.unwrap();
let accumulated_data = BlockHeaderAccumulatedDataBuilder::from_previous(genesis.accumulated_data())
.with_hash(header1.hash())
.with_achieved_target_difficulty(achieved_difficulty)
.with_total_kernel_offset(header1.total_kernel_offset.clone())
.build(consensus_manager.consensus_constants(header1.height))
.unwrap();
let header1 = ChainHeader::try_construct(header1, accumulated_data).unwrap();
txn.insert_chain_header(header1.clone());
let mut mmr_position = 4;
txn.insert_kernel(kernel.clone(), *header1.hash(), mmr_position);
txn.insert_utxo(coinbase.clone(), *header1.hash(), 1, 0);
db.commit(txn).unwrap();
utxo_sum = &coinbase.commitment.to_commitment().unwrap() + &utxo_sum;
kernel_sum = &kernel.excess.to_commitment().unwrap() + &kernel_sum;
validator
.validate(
&*db.db_read_access().unwrap(),
1,
&CompressedCommitment::from_commitment(utxo_sum.clone()),
&CompressedCommitment::from_commitment(kernel_sum.clone()),
&CompressedCommitment::from_commitment(burned_sum.clone()),
)
.unwrap();
let mut txn = DbTransaction::new();
let v = consensus_manager.get_block_reward_at(2) + uT;
let (coinbase, spending_key_id, _) = create_utxo(
v,
&key_manager,
&OutputFeatures::create_coinbase(1, None, RangeProofType::BulletProofPlus),
&TariScript::default(),
&Covenant::default(),
MicroMinotari::zero(),
);
let (pk, sig) = create_random_signature_from_secret_key(
&key_manager,
spending_key_id,
0.into(),
0,
KernelFeatures::create_coinbase(),
TxoStage::Output,
);
let excess = CompressedCommitment::from_compressed_key(pk);
let kernel = KernelBuilder::new()
.with_signature(sig)
.with_excess(&excess)
.with_features(KernelFeatures::COINBASE_KERNEL)
.build()
.unwrap();
let mut header2 = BlockHeader::from_previous(header1.header());
header2.kernel_mmr_size += 1;
header2.output_smt_size += 1;
let achieved_difficulty = AchievedTargetDifficulty::try_construct(
genesis.header().pow_algo(),
genesis.accumulated_data().target_difficulty,
genesis.accumulated_data().achieved_difficulty,
)
.unwrap();
let accumulated_data = BlockHeaderAccumulatedDataBuilder::from_previous(genesis.accumulated_data())
.with_hash(header2.hash())
.with_achieved_target_difficulty(achieved_difficulty)
.with_total_kernel_offset(header2.total_kernel_offset.clone())
.build(consensus_manager.consensus_constants(header2.height))
.unwrap();
let header2 = ChainHeader::try_construct(header2, accumulated_data).unwrap();
txn.insert_chain_header(header2.clone());
utxo_sum = &coinbase.commitment.to_commitment().unwrap() + &utxo_sum;
kernel_sum = &kernel.excess.to_commitment().unwrap() + &kernel_sum;
txn.insert_utxo(coinbase, *header2.hash(), 2, 0);
mmr_position += 1;
txn.insert_kernel(kernel, *header2.hash(), mmr_position);
db.commit(txn).unwrap();
validator
.validate(
&*db.db_read_access().unwrap(),
2,
&CompressedCommitment::from_commitment(utxo_sum),
&CompressedCommitment::from_commitment(kernel_sum),
&CompressedCommitment::from_commitment(burned_sum),
)
.unwrap_err();
}
#[tokio::test]
#[allow(clippy::too_many_lines)]
async fn chain_balance_validation_burned() {
let factories = CryptoFactories::default();
let consensus_manager = BaseNodeConsensusManagerBuilder::new(Network::Esmeralda)
.build()
.unwrap();
let genesis = consensus_manager.get_genesis_block();
let pre_mine_value = 5000 * uT;
let key_manager = KeyManager::new_random().unwrap();
let (pre_mine_utxo, pre_mine_key_id, _) = create_utxo(
pre_mine_value,
&key_manager,
&OutputFeatures::default(),
&TariScript::default(),
&Covenant::default(),
MicroMinotari::zero(),
);
let (pk, sig) = create_random_signature_from_secret_key(
&key_manager,
pre_mine_key_id,
0.into(),
0,
KernelFeatures::empty(),
TxoStage::Output,
);
let excess = CompressedCommitment::from_compressed_key(pk);
let kernel =
TransactionKernel::new_current_version(KernelFeatures::empty(), MicroMinotari::from(0), 0, excess, sig, None);
let mut gen_block = genesis.block().clone();
gen_block.body.add_output(pre_mine_utxo);
gen_block.body.add_kernels([kernel]);
let mut utxo_sum = UncompressedCommitment::default();
let mut kernel_sum = UncompressedCommitment::default();
let mut burned_sum = UncompressedCommitment::default();
let mock_store = MockTreeStore::new(true);
let smt = JellyfishMerkleTree::<_, SmtHasher>::new(&mock_store);
let mut batch = vec![];
for output in gen_block.body.outputs() {
utxo_sum = &output.commitment.to_commitment().unwrap() + &utxo_sum;
if !output.is_burned() {
let smt_key = KeyHash(output.commitment.as_bytes().try_into().expect("commitment is 32 bytes"));
let smt_value = output.smt_hash(0);
batch.push((smt_key, Some(smt_value.to_vec())));
}
}
for input in gen_block.body.inputs() {
utxo_sum = &utxo_sum - &input.commitment().unwrap().to_commitment().unwrap();
let smt_key = KeyHash(
input
.commitment()
.unwrap()
.as_bytes()
.try_into()
.expect("Commitment is 32 bytes"),
);
batch.push((smt_key, None));
}
for kernel in gen_block.body.kernels() {
kernel_sum = &kernel.excess.to_commitment().unwrap() + &kernel_sum;
}
let root = smt.put_value_set(batch, 0).unwrap();
gen_block.header.output_mr = root.0.0.into();
let mut accum = genesis.accumulated_data().clone();
accum.hash = gen_block.header.hash();
let genesis = ChainBlock::try_construct(Arc::new(gen_block), accum).unwrap();
let total_pre_mine = pre_mine_value + consensus_manager.consensus_constants(0).pre_mine_value();
let constants = ConsensusConstantsBuilder::new(Network::LocalNet)
.with_consensus_constants(consensus_manager.consensus_constants(0).clone())
.with_pre_mine_value(total_pre_mine)
.build();
let consensus_manager = BaseNodeConsensusManagerBuilder::new(Network::LocalNet)
.with_block(genesis.clone())
.add_consensus_constants(constants)
.build()
.unwrap();
let db = create_store_with_consensus(consensus_manager.clone());
let validator = ChainBalanceValidator::new(consensus_manager.clone(), factories.clone());
validator
.validate(
&*db.db_read_access().unwrap(),
0,
&CompressedCommitment::from_commitment(utxo_sum.clone()),
&CompressedCommitment::from_commitment(kernel_sum.clone()),
&CompressedCommitment::from_commitment(burned_sum.clone()),
)
.unwrap();
let mut txn = DbTransaction::new();
let coinbase_value = consensus_manager.get_block_reward_at(1) - MicroMinotari::from(100);
let (coinbase, coinbase_key_id, _) = create_utxo(
coinbase_value,
&key_manager,
&OutputFeatures::create_coinbase(1, None, RangeProofType::RevealedValue),
&TariScript::default(),
&Covenant::default(),
coinbase_value,
);
let (pk, sig) = create_random_signature_from_secret_key(
&key_manager,
coinbase_key_id,
0.into(),
0,
KernelFeatures::create_coinbase(),
TxoStage::Output,
);
let excess = CompressedCommitment::from_compressed_key(pk);
let kernel = KernelBuilder::new()
.with_signature(sig)
.with_excess(&excess)
.with_features(KernelFeatures::COINBASE_KERNEL)
.build()
.unwrap();
let (burned, burned_key_id, _) = create_utxo(
100.into(),
&key_manager,
&OutputFeatures::create_burn_output(),
&TariScript::default(),
&Covenant::default(),
MicroMinotari::zero(),
);
let (pk2, sig2) = create_random_signature_from_secret_key(
&key_manager,
burned_key_id,
0.into(),
0,
KernelFeatures::create_burn(),
TxoStage::Output,
);
let excess2 = CompressedCommitment::from_compressed_key(pk2);
let kernel2 = KernelBuilder::new()
.with_signature(sig2)
.with_excess(&excess2)
.with_features(KernelFeatures::create_burn())
.with_burn_commitment(Some(burned.commitment.clone()))
.build()
.unwrap();
burned_sum = &burned_sum + &kernel2.get_burn_commitment().unwrap().to_commitment().unwrap();
let mut header1 = BlockHeader::from_previous(genesis.header());
header1.kernel_mmr_size += 2;
header1.output_smt_size += 2;
let achieved_difficulty = AchievedTargetDifficulty::try_construct(
genesis.header().pow_algo(),
genesis.accumulated_data().target_difficulty,
genesis.accumulated_data().achieved_difficulty,
)
.unwrap();
let accumulated_data = BlockHeaderAccumulatedDataBuilder::from_previous(genesis.accumulated_data())
.with_hash(header1.hash())
.with_achieved_target_difficulty(achieved_difficulty)
.with_total_kernel_offset(header1.total_kernel_offset.clone())
.build(consensus_manager.consensus_constants(header1.height))
.unwrap();
let header1 = ChainHeader::try_construct(header1, accumulated_data).unwrap();
txn.insert_chain_header(header1.clone());
let mut mmr_position = 4;
txn.insert_kernel(kernel.clone(), *header1.hash(), mmr_position);
txn.insert_utxo(coinbase.clone(), *header1.hash(), 1, 0);
mmr_position = 5;
txn.insert_kernel(kernel2.clone(), *header1.hash(), mmr_position);
db.commit(txn).unwrap();
utxo_sum = &coinbase.commitment.to_commitment().unwrap() + &utxo_sum;
kernel_sum = &(&kernel.excess.to_commitment().unwrap() + &kernel_sum) + &kernel2.excess.to_commitment().unwrap();
validator
.validate(
&*db.db_read_access().unwrap(),
1,
&CompressedCommitment::from_commitment(utxo_sum),
&CompressedCommitment::from_commitment(kernel_sum),
&CompressedCommitment::from_commitment(burned_sum),
)
.unwrap();
}
mod transaction_validator {
use std::convert::TryFrom;
use tari_transaction_components::{
transaction_components::{CoinBaseExtra, OutputType, TransactionError},
validation::{AggregatedBodyValidationError, transaction::TransactionInternalConsistencyValidator},
};
use super::*;
use crate::consensus::BaseNodeConsensusManagerBuilder;
#[tokio::test]
async fn it_rejects_coinbase_outputs() {
let key_manager = KeyManager::new_random().unwrap();
let consensus_manager = BaseNodeConsensusManagerBuilder::new(Network::LocalNet).build().unwrap();
let db = create_store_with_consensus(consensus_manager.clone());
let factories = CryptoFactories::default();
let validator =
TransactionInternalConsistencyValidator::new(true, consensus_manager.consensus_manager(), factories);
let features = OutputFeatures::create_coinbase(0, None, RangeProofType::BulletProofPlus);
let tx = match tx!(MicroMinotari(100_000), fee: MicroMinotari(5), inputs: 1, outputs: 1, features: features, &key_manager)
{
Ok((tx, _, _)) => tx,
Err(e) => panic!("Error found: {e}"),
};
let tip = db.get_chain_metadata().unwrap();
let err = validator.validate_with_current_tip(&tx, tip).unwrap_err();
unpack_enum!(
AggregatedBodyValidationError::OutputTypeNotPermitted {
output_type: OutputType::Coinbase
} = err
);
}
#[tokio::test]
async fn coinbase_extra_must_be_empty() {
let key_manager = KeyManager::new_random().unwrap();
let consensus_manager = BaseNodeConsensusManagerBuilder::new(Network::LocalNet).build().unwrap();
let db = create_store_with_consensus(consensus_manager.clone());
let factories = CryptoFactories::default();
let validator =
TransactionInternalConsistencyValidator::new(true, consensus_manager.consensus_manager(), factories);
let mut features = OutputFeatures { ..Default::default() };
features.coinbase_extra = CoinBaseExtra::try_from(b"deadbeef".to_vec()).unwrap();
let tx = match tx!(MicroMinotari(100_000), fee: MicroMinotari(5), inputs: 1, outputs: 1, features: features, &key_manager)
{
Ok((tx, _, _)) => tx,
Err(e) => panic!("Error found: {e}"),
};
let tip = db.get_chain_metadata().unwrap();
let err = validator.validate_with_current_tip(&tx, tip).unwrap_err();
assert!(matches!(
err,
AggregatedBodyValidationError::TransactionError(
TransactionError::NonCoinbaseHasOutputFeaturesCoinbaseExtra
)
));
}
}
pub struct HeaderIter<'a, B> {
chunk: Vec<BlockHeader>,
chunk_size: usize,
cursor: usize,
is_error: bool,
height: u64,
db: &'a BlockchainDatabase<B>,
}
impl<'a, B> HeaderIter<'a, B> {
#[allow(dead_code)]
pub fn new(db: &'a BlockchainDatabase<B>, height: u64, chunk_size: usize) -> Self {
Self {
db,
chunk_size,
cursor: 0,
is_error: false,
height,
chunk: Vec::with_capacity(chunk_size),
}
}
fn get_next_chunk(&self) -> (u64, u64) {
#[allow(clippy::cast_possible_truncation)]
let upper_bound = cmp::min(self.cursor + self.chunk_size, self.height as usize);
(self.cursor as u64, upper_bound as u64)
}
}
impl<B: BlockchainBackend> Iterator for HeaderIter<'_, B> {
type Item = Result<BlockHeader, ChainStorageError>;
fn next(&mut self) -> Option<Self::Item> {
if self.is_error {
return None;
}
if self.chunk.is_empty() {
let (start, end) = self.get_next_chunk();
if start > end {
return None;
}
match self.db.fetch_headers(start..=end) {
Ok(headers) => {
if headers.is_empty() {
return None;
}
self.cursor += headers.len();
self.chunk.extend(headers);
},
Err(err) => {
self.is_error = true;
return Some(Err(err));
},
}
}
Some(Ok(self.chunk.remove(0)))
}
}