From 14d801099d53ed244aff7dcb9417f6f4c0bdad4e Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Wed, 16 Nov 2022 18:33:02 +0400 Subject: [PATCH] feat: add stateless vn reg utxo validation rules and merkle root --- .../src/chain_storage/blockchain_backend.rs | 4 + .../src/chain_storage/blockchain_database.rs | 37 ++++-- .../core/src/chain_storage/lmdb_db/cursors.rs | 108 ------------------ .../core/src/chain_storage/lmdb_db/lmdb_db.rs | 3 +- .../lmdb_db/validator_node_store.rs | 3 + .../core/src/consensus/consensus_constants.rs | 8 ++ .../transaction_components/output_features.rs | 6 + .../transaction_output.rs | 3 +- .../validation/block_validators/body_only.rs | 2 +- base_layer/core/src/validation/error.rs | 11 +- base_layer/core/src/validation/helpers.rs | 26 +++++ .../src/validation/transaction_validators.rs | 2 + 12 files changed, 93 insertions(+), 120 deletions(-) diff --git a/base_layer/core/src/chain_storage/blockchain_backend.rs b/base_layer/core/src/chain_storage/blockchain_backend.rs index 9bcff456120..4494014701f 100644 --- a/base_layer/core/src/chain_storage/blockchain_backend.rs +++ b/base_layer/core/src/chain_storage/blockchain_backend.rs @@ -193,8 +193,12 @@ pub trait BlockchainBackend: Send + Sync { /// Fetches all tracked reorgs fn fetch_all_reorgs(&self) -> Result, ChainStorageError>; + /// Fetches the validator node set for the given height ordered according to height of registration and canonical + /// block body ordering. fn fetch_active_validator_nodes(&self, height: u64) -> Result, ChainStorageError>; + /// Returns the shard key for the validator node if valid at the given height. fn get_shard_key(&self, height: u64, public_key: PublicKey) -> Result, ChainStorageError>; + /// Returns all template registrations within (inclusive) the given height range. fn fetch_template_registrations( &self, start_height: u64, diff --git a/base_layer/core/src/chain_storage/blockchain_database.rs b/base_layer/core/src/chain_storage/blockchain_database.rs index 8affd89bd61..b8669877e0a 100644 --- a/base_layer/core/src/chain_storage/blockchain_database.rs +++ b/base_layer/core/src/chain_storage/blockchain_database.rs @@ -38,6 +38,7 @@ use tari_common_types::{ chain_metadata::ChainMetadata, types::{BlockHash, Commitment, FixedHash, HashOutput, PublicKey, Signature}, }; +use tari_crypto::hash::blake2::Blake256; use tari_mmr::pruned_hashset::PrunedHashSet; use tari_utilities::{epoch_time::EpochTime, hex::Hex, ByteArray}; @@ -814,7 +815,7 @@ where B: BlockchainBackend header.timestamp = median_timestamp.increase(1); } let mut block = Block { header, body }; - let roots = calculate_mmr_roots(&*db, &block)?; + let roots = calculate_mmr_roots(&*db, self.rules(), &block)?; block.header.kernel_mr = roots.kernel_mr; block.header.kernel_mmr_size = roots.kernel_mmr_size; block.header.input_mr = roots.input_mr; @@ -835,7 +836,7 @@ where B: BlockchainBackend block.body.is_sorted(), "calculate_mmr_roots expected a sorted block body, however the block body was not sorted" ); - let mmr_roots = calculate_mmr_roots(&*db, &block)?; + let mmr_roots = calculate_mmr_roots(&*db, self.rules(), &block)?; Ok((block, mmr_roots)) } @@ -1240,7 +1241,11 @@ impl std::fmt::Display for MmrRoots { #[allow(clippy::too_many_lines)] #[allow(clippy::similar_names)] -pub fn calculate_mmr_roots(db: &T, block: &Block) -> Result { +pub fn calculate_mmr_roots( + db: &T, + rules: &ConsensusManager, + block: &Block, +) -> Result { let header = &block.header; let body = &block.body; @@ -1341,9 +1346,27 @@ pub fn calculate_mmr_roots(db: &T, block: &Block) -> Resul output_mmr.compress(); - let mut validator_nodes = db.fetch_active_validator_nodes(metadata.height_of_longest_chain() + 1)?; - validator_nodes.sort(); - let vn_mmr = ValidatorNodeMmr::new(validator_nodes.iter().map(|vn| vn.1.to_vec()).collect()); + let next_height = metadata.height_of_longest_chain() + 1; + let epoch_len = rules.consensus_constants(next_height).epoch_length(); + let validator_node_mr = if next_height % epoch_len == 0 { + // At epoch boundary, the MR is rebuilt from the current validator set + let validator_nodes = db.fetch_active_validator_nodes(next_height)?; + fn hash_node((pk, s): &(PublicKey, [u8; 32])) -> Vec { + use digest::Digest; + Blake256::new() + .chain(pk.as_bytes()) + .chain(s.as_slice()) + .finalize() + .to_vec() + } + + let vn_mmr = ValidatorNodeMmr::new(validator_nodes.iter().map(hash_node).collect()); + FixedHash::try_from(vn_mmr.get_merkle_root()?)? + } else { + // MR is unchanged except for epoch boundary + let tip_header = db.fetch_tip_header()?; + tip_header.header().validator_node_mr + }; let mmr_roots = MmrRoots { kernel_mr: FixedHash::try_from(kernel_mmr.get_merkle_root()?)?, @@ -1352,7 +1375,7 @@ pub fn calculate_mmr_roots(db: &T, block: &Block) -> Resul output_mr: FixedHash::try_from(output_mmr.get_merkle_root()?)?, output_mmr_size: output_mmr.get_leaf_count() as u64, witness_mr: FixedHash::try_from(witness_mmr.get_merkle_root()?)?, - validator_node_mr: FixedHash::try_from(vn_mmr.get_merkle_root()?)?, + validator_node_mr, }; Ok(mmr_roots) } diff --git a/base_layer/core/src/chain_storage/lmdb_db/cursors.rs b/base_layer/core/src/chain_storage/lmdb_db/cursors.rs index 551361cba82..cba319392e1 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/cursors.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/cursors.rs @@ -65,26 +65,6 @@ where V: DeserializeOwned } } - // /// Returns the item on or before the given seek key, progressing backwards until the key prefix no longer - // matches #[allow(dead_code)] - // pub fn prev(&mut self) -> Result, V)>, ChainStorageError> { - // if !self.has_seeked { - // let prefix_key = self.prefix_key; - // if let Some((k, val)) = self.seek_gte(prefix_key)? { - // // seek_range_k returns the greater key, so we only want to return the current value that was seeked - // to // if it exactly matches the prefix_key - // if k == prefix_key { - // return Ok(Some((k, val))); - // } - // } - // } - // - // match self.cursor.prev(&self.access).to_opt()? { - // Some((k, v)) => Self::deserialize_if_matches(self.prefix_key, k, v), - // None => Ok(None), - // } - // } - // This function could be used later in cases where multiple seeks are required. #[cfg(test)] pub fn reset_to(&mut self, prefix_key: &'a [u8]) { @@ -116,63 +96,6 @@ where V: DeserializeOwned } } -// pub struct LmdbRangeCursor<'a, K, V> { -// cursor: LmdbReadCursor<'a, V>, -// start_key: Option, -// end_key: Option, -// inclusive: bool, -// } -// -// impl<'a, K: Copy, V> LmdbRangeCursor<'a, K, V> { -// pub(super) fn new>(cursor: LmdbReadCursor<'a, V>, range: R) -> Self { -// Self { -// cursor, -// start_key: match range.start_bound() { -// Bound::Included(k) | Bound::Excluded(k) => Some(*k), -// Bound::Unbounded => None, -// }, -// end_key: match range.end_bound() { -// Bound::Included(k) | -// Bound::Excluded(k) => Some -// Bound::Unbounded => {} -// }, -// inclusive, -// } -// } -// -// /// Returns the item on or after the start key, progressing forwards until the end key is reached -// pub fn next(&mut self) -> Result, ChainStorageError> -// where K: DeserializeOwned { -// let (k, v) = match self.cursor.next()? { -// Some(r) => r, -// None => return Ok(None), -// }; -// let key = deserialize::(&k)?; -// if let Some(end_key) = &self.end_key { -// if key > *end_key { -// return Ok(None); -// } -// } -// Ok(Some((key, v))) -// } -// -// /// Returns the item on or before the end key, progressing backwards until the start key is reached -// pub fn prev(&mut self) -> Result, ChainStorageError> -// where K: DeserializeOwned { -// let (k, v) = match self.cursor.prev()? { -// Some(r) => r, -// None => return Ok(None), -// }; -// let key = deserialize::(&k)?; -// if let Some(start_key) = &self.start_key { -// if key < *start_key { -// return Ok(None); -// } -// } -// Ok(Some((key, v))) -// } -// } - pub struct LmdbReadCursor<'a, V> { cursor: Cursor<'a, 'a>, value_type: PhantomData, @@ -296,35 +219,4 @@ mod tests { assert_eq!(cursor.next().unwrap(), None); } } - - #[test] - fn balh() { - let temp_path = create_temporary_data_path(); - - let lmdb_store = LMDBBuilder::new() - .set_path(&temp_path) - .set_env_config(LMDBConfig::default()) - .set_max_number_of_databases(1) - .add_database("test", db::CREATE) - .build() - .unwrap(); - - let db = lmdb_store.get_handle("test").unwrap(); - let txn = WriteTransaction::new(lmdb_store.env()).unwrap(); - { - let mut access = txn.access(); - let f = lmdb_zero::put::Flags::empty(); - access.put(&db.db(), "Fruit4", "Orange", f).unwrap(); - access.put(&db.db(), "Fruit1", "Apple", f).unwrap(); - access.put(&db.db(), "Animal", "Badger", f).unwrap(); - // access.put(&db.db(), "Veggie", "Carrot", f).unwrap(); - - let mut cursor = txn.cursor(db.db()).unwrap(); - assert_eq!(("Fruit1", "Apple"), cursor.seek_range_k(&access, "Banana").unwrap()); - assert_eq!(("Fruit4", "Orange"), cursor.next(&access).unwrap()); - assert!(cursor.seek_range_k::(&access, "Veggie").is_err()); - } - txn.commit().unwrap(); - fs::remove_dir_all(&temp_path).expect("Could not delete temporary file"); - } } diff --git a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs index 9d2be2b109a..a6af31f4a02 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs @@ -1320,7 +1320,7 @@ impl LMDBDatabase { .as_ref() .and_then(|f| f.validator_node_registration()) { - self.insert_validator_node(txn, &block_hash, &vn_reg, mmr_count)?; + self.insert_validator_node(txn, &header, output_hash, &vn_reg)?; } if let Some(template_reg) = output .features @@ -1402,6 +1402,7 @@ impl LMDBDatabase { &self, txn: &WriteTransaction<'_>, header: &BlockHeader, + output_hash: HashOutput, vn_reg: &ValidatorNodeRegistration, ) -> Result<(), ChainStorageError> { let store = self.validator_node_store(txn); diff --git a/base_layer/core/src/chain_storage/lmdb_db/validator_node_store.rs b/base_layer/core/src/chain_storage/lmdb_db/validator_node_store.rs index 7751e4dd937..eb891e727f4 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/validator_node_store.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/validator_node_store.rs @@ -183,6 +183,9 @@ impl<'a, Txn: Deref>> ValidatorNodeStore<'a, Txn> let mut cursor = self.index_read_cursor()?; let key = ShardIdIndexKey::try_from_parts(&[public_key.as_bytes(), &start_height.to_be_bytes()]) .expect("fetch_shard_key: Composite key length is incorrect"); + + // TODO: unused_assignments is a false positive, we might be able to remove this exclusion in future + #[allow(unused_assignments)] let mut shard_key = None; // Find the first entry at or above start_height match cursor.seek_range::(&key.as_bytes())? { diff --git a/base_layer/core/src/consensus/consensus_constants.rs b/base_layer/core/src/consensus/consensus_constants.rs index 72c8a4fc3dd..34d36207ba2 100644 --- a/base_layer/core/src/consensus/consensus_constants.rs +++ b/base_layer/core/src/consensus/consensus_constants.rs @@ -304,6 +304,14 @@ impl ConsensusConstants { self.vn_registration_shuffle_interval } + pub fn validator_node_registration_min_deposit_amount(&self) -> MicroTari { + self.vn_registration_min_deposit_amount + } + + pub fn validator_node_registration_min_lock_height(&self) -> u64 { + self.vn_registration_lock_height + } + /// Returns the current epoch from the given height pub fn block_height_to_current_epoch(&self, height: u64) -> VnEpoch { VnEpoch(height / self.vn_epoch_length) diff --git a/base_layer/core/src/transactions/transaction_components/output_features.rs b/base_layer/core/src/transactions/transaction_components/output_features.rs index e7c75d94de6..2759e0f8731 100644 --- a/base_layer/core/src/transactions/transaction_components/output_features.rs +++ b/base_layer/core/src/transactions/transaction_components/output_features.rs @@ -126,6 +126,12 @@ impl OutputFeatures { } } + pub fn validator_node_registration(&self) -> Option<&ValidatorNodeRegistration> { + self.sidechain_feature + .as_ref() + .and_then(|s| s.validator_node_registration()) + } + pub fn is_coinbase(&self) -> bool { matches!(self.output_type, OutputType::Coinbase) } diff --git a/base_layer/core/src/transactions/transaction_components/transaction_output.rs b/base_layer/core/src/transactions/transaction_components/transaction_output.rs index ff4b9175aad..58182f26f82 100644 --- a/base_layer/core/src/transactions/transaction_components/transaction_output.rs +++ b/base_layer/core/src/transactions/transaction_components/transaction_output.rs @@ -223,8 +223,7 @@ impl TransactionOutput { .as_ref() .and_then(|f| f.validator_node_registration()) { - // TODO: figure out what the validator node should sign - if !validator_node_reg.is_valid_signature_for(b"") { + if !validator_node_reg.is_valid_signature_for(self.commitment.as_bytes()) { return Err(TransactionError::InvalidSignatureError( "Validator node signature is not valid!".to_string(), )); diff --git a/base_layer/core/src/validation/block_validators/body_only.rs b/base_layer/core/src/validation/block_validators/body_only.rs index 0b2fbf0fd05..b04afeeb9da 100644 --- a/base_layer/core/src/validation/block_validators/body_only.rs +++ b/base_layer/core/src/validation/block_validators/body_only.rs @@ -87,7 +87,7 @@ impl PostOrphanBodyValidation for BodyOnlyValidator { "Block validation: All inputs, outputs and kernels are valid for {}", block_id ); - let mmr_roots = chain_storage::calculate_mmr_roots(backend, block.block())?; + let mmr_roots = chain_storage::calculate_mmr_roots(backend, &self.rules, block.block())?; helpers::check_mmr_roots(block.header(), &mmr_roots)?; trace!( target: LOG_TARGET, diff --git a/base_layer/core/src/validation/error.rs b/base_layer/core/src/validation/error.rs index 52a1b09bc1b..765739d1284 100644 --- a/base_layer/core/src/validation/error.rs +++ b/base_layer/core/src/validation/error.rs @@ -30,7 +30,10 @@ use crate::{ chain_storage::ChainStorageError, covenants::CovenantError, proof_of_work::{monero_rx::MergeMineError, PowError}, - transactions::transaction_components::{OutputType, TransactionError}, + transactions::{ + tari_amount::MicroTari, + transaction_components::{OutputType, TransactionError}, + }, }; #[derive(Debug, Error)] @@ -136,6 +139,12 @@ pub enum ValidationError { FixedHashSizeError(#[from] FixedHashSizeError), #[error("Validator node MMR is not correct")] ValidatorNodeMmmrError, + #[error("Validator registration has invalid minimum amount {actual}, must be at least {min}")] + ValidatorNodeRegistrationMinDepositAmount { min: MicroTari, actual: MicroTari }, + #[error("Validator registration has invalid maturity {actual}, must be at least {min}")] + ValidatorNodeRegistrationMinLockHeight { min: u64, actual: u64 }, + #[error("Validator node registration signature failed verification")] + InvalidValidatorNodeSignature, } // ChainStorageError has a ValidationError variant, so to prevent a cyclic dependency we use a string representation in diff --git a/base_layer/core/src/validation/helpers.rs b/base_layer/core/src/validation/helpers.rs index 595a3b53fe8..22d1ad7e16c 100644 --- a/base_layer/core/src/validation/helpers.rs +++ b/base_layer/core/src/validation/helpers.rs @@ -30,6 +30,7 @@ use tari_crypto::{ tari_utilities::{epoch_time::EpochTime, hex::Hex}, }; use tari_script::TariScript; +use tari_utilities::ByteArray; use crate::{ blocks::{Block, BlockHeader, BlockHeaderValidationError, BlockValidationError}, @@ -824,6 +825,31 @@ pub fn validate_versions( Ok(()) } +pub fn check_validator_node_registration_utxo( + consensus_constants: &ConsensusConstants, + utxo: &TransactionOutput, +) -> Result<(), ValidationError> { + if let Some(reg) = utxo.features.validator_node_registration() { + if utxo.minimum_value_promise < consensus_constants.validator_node_registration_min_deposit_amount() { + return Err(ValidationError::ValidatorNodeRegistrationMinDepositAmount { + min: consensus_constants.validator_node_registration_min_deposit_amount(), + actual: utxo.minimum_value_promise, + }); + } + if utxo.features.maturity < consensus_constants.validator_node_registration_min_lock_height() { + return Err(ValidationError::ValidatorNodeRegistrationMinLockHeight { + min: consensus_constants.validator_node_registration_min_lock_height(), + actual: utxo.features.maturity, + }); + } + + if !reg.is_valid_signature_for(utxo.commitment.as_bytes()) { + return Err(ValidationError::InvalidValidatorNodeSignature); + } + } + Ok(()) +} + #[cfg(test)] mod test { use tari_test_utils::unpack_enum; diff --git a/base_layer/core/src/validation/transaction_validators.rs b/base_layer/core/src/validation/transaction_validators.rs index 59da19b2a95..3bf36144d32 100644 --- a/base_layer/core/src/validation/transaction_validators.rs +++ b/base_layer/core/src/validation/transaction_validators.rs @@ -32,6 +32,7 @@ use crate::{ check_outputs, check_permitted_output_types, check_total_burned, + check_validator_node_registration_utxo, validate_versions, }, MempoolTransactionValidation, @@ -138,6 +139,7 @@ impl MempoolTransactionValidation for TxConsensusValidator validate_versions(tx.body(), consensus_constants)?; for output in tx.body.outputs() { check_permitted_output_types(consensus_constants, output)?; + check_validator_node_registration_utxo(consensus_constants, output)?; } Ok(()) }