Skip to content

Commit

Permalink
feat: add stateless vn reg utxo validation rules and merkle root
Browse files Browse the repository at this point in the history
  • Loading branch information
sdbondi committed Nov 16, 2022
1 parent 161c17d commit 14d8010
Show file tree
Hide file tree
Showing 12 changed files with 93 additions and 120 deletions.
4 changes: 4 additions & 0 deletions base_layer/core/src/chain_storage/blockchain_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,12 @@ pub trait BlockchainBackend: Send + Sync {
/// Fetches all tracked reorgs
fn fetch_all_reorgs(&self) -> Result<Vec<Reorg>, 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<Vec<(PublicKey, [u8; 32])>, 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<Option<[u8; 32]>, ChainStorageError>;
/// Returns all template registrations within (inclusive) the given height range.
fn fetch_template_registrations(
&self,
start_height: u64,
Expand Down
37 changes: 30 additions & 7 deletions base_layer/core/src/chain_storage/blockchain_database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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;
Expand All @@ -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))
}

Expand Down Expand Up @@ -1240,7 +1241,11 @@ impl std::fmt::Display for MmrRoots {

#[allow(clippy::too_many_lines)]
#[allow(clippy::similar_names)]
pub fn calculate_mmr_roots<T: BlockchainBackend>(db: &T, block: &Block) -> Result<MmrRoots, ChainStorageError> {
pub fn calculate_mmr_roots<T: BlockchainBackend>(
db: &T,
rules: &ConsensusManager,
block: &Block,
) -> Result<MmrRoots, ChainStorageError> {
let header = &block.header;
let body = &block.body;

Expand Down Expand Up @@ -1341,9 +1346,27 @@ pub fn calculate_mmr_roots<T: BlockchainBackend>(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<u8> {
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()?)?,
Expand All @@ -1352,7 +1375,7 @@ pub fn calculate_mmr_roots<T: BlockchainBackend>(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)
}
Expand Down
108 changes: 0 additions & 108 deletions base_layer/core/src/chain_storage/lmdb_db/cursors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<(Vec<u8>, 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]) {
Expand Down Expand Up @@ -116,63 +96,6 @@ where V: DeserializeOwned
}
}

// pub struct LmdbRangeCursor<'a, K, V> {
// cursor: LmdbReadCursor<'a, V>,
// start_key: Option<K>,
// end_key: Option<K>,
// inclusive: bool,
// }
//
// impl<'a, K: Copy, V> LmdbRangeCursor<'a, K, V> {
// pub(super) fn new<R: RangeBounds<K>>(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<Option<(K, V)>, ChainStorageError>
// where K: DeserializeOwned {
// let (k, v) = match self.cursor.next()? {
// Some(r) => r,
// None => return Ok(None),
// };
// let key = deserialize::<K>(&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<Option<(K, V)>, ChainStorageError>
// where K: DeserializeOwned {
// let (k, v) = match self.cursor.prev()? {
// Some(r) => r,
// None => return Ok(None),
// };
// let key = deserialize::<K>(&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<V>,
Expand Down Expand Up @@ -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::<str, str>(&access, "Veggie").is_err());
}
txn.commit().unwrap();
fs::remove_dir_all(&temp_path).expect("Could not delete temporary file");
}
}
3 changes: 2 additions & 1 deletion base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ impl<'a, Txn: Deref<Target = ConstTransaction<'a>>> 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::<ShardIdIndexKey>(&key.as_bytes())? {
Expand Down
8 changes: 8 additions & 0 deletions base_layer/core/src/consensus/consensus_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ impl<B: BlockchainBackend> PostOrphanBodyValidation<B> 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,
Expand Down
11 changes: 10 additions & 1 deletion base_layer/core/src/validation/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions base_layer/core/src/validation/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions base_layer/core/src/validation/transaction_validators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use crate::{
check_outputs,
check_permitted_output_types,
check_total_burned,
check_validator_node_registration_utxo,
validate_versions,
},
MempoolTransactionValidation,
Expand Down Expand Up @@ -138,6 +139,7 @@ impl<B: BlockchainBackend> 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(())
}
Expand Down

0 comments on commit 14d8010

Please sign in to comment.