From 64f89724896c6ddd7b21efc8c8ff605cbc373f70 Mon Sep 17 00:00:00 2001 From: Miguel Naveira <47919901+mrnaveira@users.noreply.github.com> Date: Thu, 30 Jun 2022 15:10:30 +0100 Subject: [PATCH] feat(base_layer): basic validations for proposals, proposal acceptances and amendments (#4238) Description --- * Added new base layer validations for: * Contract update proposals: * The corresponding constitution must be already published * No duplicated proposals (same `contract_id` and `proposal_id`) can be published * Contract update proposal acceptances: * The corresponding update proposal must be already published * No duplicated acceptances (same proposal and validator node) can be published * Contract amendments: * The corresponding update proposal must be already published * No duplicated amendments can be published for the same proposal * The `updated_constitution` ratified in the amendment must match exactly the one in the update proposal * Added new unit tests for all the new validations * Adapted an existing integration test for amendments due to the new validations * Added `ContractAmendment` to the base layer database index (`lmdb`) * Refactored some redundant helper code for validations Motivation and Context --- The base layer should perform basic validations (required prior data, no duplications and consistency) on update proposals, proposal acceptances and amendments How Has This Been Tested? --- * New unit test to check the new validation * The existing unit and integration tests pass --- .../chain_storage/lmdb_db/contract_index.rs | 42 +-- .../transaction_components/output_type.rs | 3 +- .../dan_validators/acceptance_validator.rs | 8 +- .../dan_validators/amendment_validator.rs | 211 +++++++++++++++ .../src/validation/dan_validators/helpers.rs | 28 +- .../core/src/validation/dan_validators/mod.rs | 14 + .../validation/dan_validators/test_helpers.rs | 94 ++++++- .../update_proposal_acceptance_validator.rs | 251 ++++++++++++++++++ .../update_proposal_validator.rs | 154 +++++++++++ integration_tests/features/WalletCli.feature | 8 +- 10 files changed, 776 insertions(+), 37 deletions(-) create mode 100644 base_layer/core/src/validation/dan_validators/amendment_validator.rs create mode 100644 base_layer/core/src/validation/dan_validators/update_proposal_acceptance_validator.rs create mode 100644 base_layer/core/src/validation/dan_validators/update_proposal_validator.rs diff --git a/base_layer/core/src/chain_storage/lmdb_db/contract_index.rs b/base_layer/core/src/chain_storage/lmdb_db/contract_index.rs index 52fdd5cdd4..d128c980b7 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/contract_index.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/contract_index.rs @@ -77,18 +77,17 @@ where T: Deref> ) -> Result, ChainStorageError> { let key = ContractIndexKey::new(contract_id, output_type); match output_type { - OutputType::ContractAmendment | - OutputType::ContractDefinition | - OutputType::ContractCheckpoint | - OutputType::ContractConstitution => Ok(self - .get::<_, ContractIndexValue>(&key)? - .into_iter() - .map(|v| v.output_hash) - .collect()), - + OutputType::ContractDefinition | OutputType::ContractCheckpoint | OutputType::ContractConstitution => { + Ok(self + .get::<_, ContractIndexValue>(&key)? + .into_iter() + .map(|v| v.output_hash) + .collect()) + }, OutputType::ContractValidatorAcceptance | OutputType::ContractConstitutionProposal | - OutputType::ContractConstitutionChangeAcceptance => Ok(self + OutputType::ContractConstitutionChangeAcceptance | + OutputType::ContractAmendment => Ok(self .get::<_, ContractValueHashSet>(&key)? .into_iter() .flatten() @@ -108,14 +107,13 @@ where T: Deref> ) -> Result, ChainStorageError> { let key = BlockContractIndexKey::prefixed(block_hash, output_type); match output_type { - OutputType::ContractDefinition | - OutputType::ContractCheckpoint | - OutputType::ContractConstitution | - OutputType::ContractAmendment => self.get_all_matching::<_, FixedHash>(&key), - + OutputType::ContractDefinition | OutputType::ContractCheckpoint | OutputType::ContractConstitution => { + self.get_all_matching::<_, FixedHash>(&key) + }, OutputType::ContractValidatorAcceptance | OutputType::ContractConstitutionProposal | - OutputType::ContractConstitutionChangeAcceptance => Ok(self + OutputType::ContractConstitutionChangeAcceptance | + OutputType::ContractAmendment => Ok(self .get_all_matching::<_, FixedHashSet>(&key)? .into_iter() .flatten() @@ -232,7 +230,7 @@ impl<'a> ContractIndex<'a, WriteTransaction<'a>> { }, // Only one contract checkpoint and constitution can exist at a time and can be overwritten. Consensus rules // decide whether this is valid but we just assume this is valid here. - OutputType::ContractAmendment | OutputType::ContractConstitution | OutputType::ContractCheckpoint => { + OutputType::ContractConstitution | OutputType::ContractCheckpoint => { self.assert_definition_exists(contract_id)?; self.set(&contract_key, &ContractIndexValue { block_hash, @@ -244,7 +242,8 @@ impl<'a> ContractIndex<'a, WriteTransaction<'a>> { // These are collections of output hashes OutputType::ContractValidatorAcceptance | OutputType::ContractConstitutionProposal | - OutputType::ContractConstitutionChangeAcceptance => { + OutputType::ContractConstitutionChangeAcceptance | + OutputType::ContractAmendment => { self.assert_definition_exists(contract_id)?; self.add_to_set(&contract_key, ContractIndexValue { block_hash, @@ -285,13 +284,16 @@ impl<'a> ContractIndex<'a, WriteTransaction<'a>> { self.delete(&block_key)?; Ok(()) }, - OutputType::ContractAmendment | OutputType::ContractConstitution | OutputType::ContractCheckpoint => { + OutputType::ContractConstitution | OutputType::ContractCheckpoint => { let contract = self.get_and_delete::<_, ContractIndexValue>(&contract_key)?; let block_key = BlockContractIndexKey::new(contract.block_hash, output_type, contract_id); self.delete(&block_key)?; Ok(()) }, - OutputType::ContractValidatorAcceptance | OutputType::ContractConstitutionProposal => { + OutputType::ContractValidatorAcceptance | + OutputType::ContractConstitutionProposal | + OutputType::ContractConstitutionChangeAcceptance | + OutputType::ContractAmendment => { let contract = self.remove_from_contract_index(&contract_key, &output_hash)?; let block_key = BlockContractIndexKey::new(contract.block_hash, output_type, contract_id); self.remove_from_set(&block_key, &output_hash)?; diff --git a/base_layer/core/src/transactions/transaction_components/output_type.rs b/base_layer/core/src/transactions/transaction_components/output_type.rs index 4a61cbcaac..50b02969b1 100644 --- a/base_layer/core/src/transactions/transaction_components/output_type.rs +++ b/base_layer/core/src/transactions/transaction_components/output_type.rs @@ -90,7 +90,8 @@ impl OutputType { ContractValidatorAcceptance | ContractCheckpoint | ContractConstitutionProposal | - ContractConstitutionChangeAcceptance + ContractConstitutionChangeAcceptance | + ContractAmendment ) } } diff --git a/base_layer/core/src/validation/dan_validators/acceptance_validator.rs b/base_layer/core/src/validation/dan_validators/acceptance_validator.rs index 9a2f779aa8..865b029346 100644 --- a/base_layer/core/src/validation/dan_validators/acceptance_validator.rs +++ b/base_layer/core/src/validation/dan_validators/acceptance_validator.rs @@ -71,7 +71,7 @@ fn get_contract_acceptance(sidechain_feature: &SideChainFeatures) -> Result<&Con match sidechain_feature.acceptance.as_ref() { Some(acceptance) => Ok(acceptance), None => Err(ValidationError::DanLayerError( - "Invalid contract acceptance: acceptance features not found".to_string(), + "Contract acceptance features not found".to_string(), )), } } @@ -111,7 +111,7 @@ fn validate_public_key( .contains(validator_node_public_key); if !is_validator_in_committee { let msg = format!( - "Invalid contract acceptance: validator node public key is not in committee ({:?})", + "Validator node public key is not in committee ({:?})", validator_node_public_key ); return Err(ValidationError::DanLayerError(msg)); @@ -177,7 +177,7 @@ mod test { let schema = create_contract_acceptance_schema(contract_id, change[3].clone(), validator_node_public_key); let (tx, _) = schema_to_transaction(&schema); - // try to validate the duplicated accepntace transaction and check that we get the error + // try to validate the duplicated acceptance transaction and check that we get the error assert_dan_error(&blockchain, &tx, "Duplicated contract acceptance"); } @@ -203,6 +203,6 @@ mod test { let (tx, _) = schema_to_transaction(&schema); // try to validate the acceptance transaction and check that we get the committee error - assert_dan_error(&blockchain, &tx, "validator node public key is not in committee"); + assert_dan_error(&blockchain, &tx, "Validator node public key is not in committee"); } } diff --git a/base_layer/core/src/validation/dan_validators/amendment_validator.rs b/base_layer/core/src/validation/dan_validators/amendment_validator.rs new file mode 100644 index 0000000000..6a98eeca0a --- /dev/null +++ b/base_layer/core/src/validation/dan_validators/amendment_validator.rs @@ -0,0 +1,211 @@ +// Copyright 2022, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use tari_common_types::types::FixedHash; +use tari_utilities::hex::Hex; + +use super::helpers::{ + fetch_contract_features, + fetch_contract_update_proposal, + get_sidechain_features, + validate_output_type, +}; +use crate::{ + chain_storage::{BlockchainBackend, BlockchainDatabase}, + transactions::transaction_components::{ + ContractAmendment, + ContractUpdateProposal, + OutputType, + SideChainFeatures, + TransactionOutput, + }, + validation::ValidationError, +}; + +pub fn validate_amendment( + db: &BlockchainDatabase, + output: &TransactionOutput, +) -> Result<(), ValidationError> { + validate_output_type(output, OutputType::ContractAmendment)?; + + let sidechain_features = get_sidechain_features(output)?; + let contract_id = sidechain_features.contract_id; + + let amendment = get_contract_amendment(sidechain_features)?; + let proposal_id = amendment.proposal_id; + let proposal = fetch_contract_update_proposal(db, contract_id, proposal_id)?; + + validate_duplication(db, contract_id, proposal_id)?; + validate_updated_constiution(amendment, &proposal)?; + + Ok(()) +} + +fn get_contract_amendment(sidechain_feature: &SideChainFeatures) -> Result<&ContractAmendment, ValidationError> { + match sidechain_feature.amendment.as_ref() { + Some(amendment) => Ok(amendment), + None => Err(ValidationError::DanLayerError( + "Contract amendment features not found".to_string(), + )), + } +} + +fn validate_duplication( + db: &BlockchainDatabase, + contract_id: FixedHash, + proposal_id: u64, +) -> Result<(), ValidationError> { + let features = fetch_contract_features(db, contract_id, OutputType::ContractAmendment)?; + match features + .into_iter() + .filter_map(|feature| feature.amendment) + .find(|amendment| amendment.proposal_id == proposal_id) + { + Some(_) => { + let msg = format!( + "Duplicated amendment for contract_id ({:?}) and proposal_id ({:?})", + contract_id.to_hex(), + proposal_id, + ); + Err(ValidationError::DanLayerError(msg)) + }, + None => Ok(()), + } +} + +fn validate_updated_constiution( + amendment: &ContractAmendment, + proposal: &ContractUpdateProposal, +) -> Result<(), ValidationError> { + if amendment.updated_constitution != proposal.updated_constitution { + return Err(ValidationError::DanLayerError( + "The updated_constitution of the amendment does not match the one in the update proposal".to_string(), + )); + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use tari_common_types::types::PublicKey; + + use crate::validation::dan_validators::test_helpers::{ + assert_dan_error, + create_block, + create_contract_amendment_schema, + init_test_blockchain, + publish_constitution, + publish_definition, + publish_update_proposal, + schema_to_transaction, + }; + + #[test] + fn proposal_must_exist() { + // initialise a blockchain with enough funds to spend at contract transactions + let (mut blockchain, change) = init_test_blockchain(); + + // publish the contract definition into a block + let contract_id = publish_definition(&mut blockchain, change[0].clone()); + + // publish the contract constitution into a block + let validator_node_public_key = PublicKey::default(); + let committee = vec![validator_node_public_key]; + publish_constitution(&mut blockchain, change[1].clone(), contract_id, committee.clone()); + + // skip the publication of the contract update proposal + + // create an amendment transaction + let proposal_id = 1; + let schema = create_contract_amendment_schema(contract_id, change[1].clone(), proposal_id, committee); + let (tx, _) = schema_to_transaction(&schema); + + // try to validate the acceptance transaction and check that we get the error + assert_dan_error(&blockchain, &tx, "Contract update proposal not found"); + } + + #[test] + fn it_rejects_duplicated_amendments() { + // initialise a blockchain with enough funds to spend at contract transactions + let (mut blockchain, change) = init_test_blockchain(); + + // publish the contract definition into a block + let contract_id = publish_definition(&mut blockchain, change[0].clone()); + + // publish the contract constitution into a block + let validator_node_public_key = PublicKey::default(); + let committee = vec![validator_node_public_key]; + publish_constitution(&mut blockchain, change[1].clone(), contract_id, committee.clone()); + + // publish a contract update proposal into a block + let proposal_id: u64 = 1; + publish_update_proposal( + &mut blockchain, + change[2].clone(), + contract_id, + proposal_id, + committee.clone(), + ); + + // publish the contract amendment into a block + let schema = create_contract_amendment_schema(contract_id, change[3].clone(), proposal_id, committee.clone()); + create_block(&mut blockchain, "amendment", schema); + + // create a (duplicated) contract amendment transaction + let schema = create_contract_amendment_schema(contract_id, change[4].clone(), proposal_id, committee); + let (tx, _) = schema_to_transaction(&schema); + + // try to validate the duplicated amendment transaction and check that we get the error + assert_dan_error(&blockchain, &tx, "Duplicated amendment"); + } + + #[test] + fn it_rejects_altered_updated_constitution() { + // initialise a blockchain with enough funds to spend at contract transactions + let (mut blockchain, change) = init_test_blockchain(); + + // publish the contract definition into a block + let contract_id = publish_definition(&mut blockchain, change[0].clone()); + + // publish the contract constitution into a block + let validator_node_public_key = PublicKey::default(); + let committee = vec![validator_node_public_key]; + publish_constitution(&mut blockchain, change[1].clone(), contract_id, committee.clone()); + + // publish a contract update proposal into a block + let proposal_id: u64 = 1; + publish_update_proposal(&mut blockchain, change[2].clone(), contract_id, proposal_id, committee); + + // create an amendment with an altered committee (compared to the proposal) + let altered_committee = vec![]; + let schema = create_contract_amendment_schema(contract_id, change[4].clone(), proposal_id, altered_committee); + let (tx, _) = schema_to_transaction(&schema); + + // try to validate the amendment transaction and check that we get the error + assert_dan_error( + &blockchain, + &tx, + "The updated_constitution of the amendment does not match the one in the update proposal", + ); + } +} diff --git a/base_layer/core/src/validation/dan_validators/helpers.rs b/base_layer/core/src/validation/dan_validators/helpers.rs index fa5d12311f..16df978a6a 100644 --- a/base_layer/core/src/validation/dan_validators/helpers.rs +++ b/base_layer/core/src/validation/dan_validators/helpers.rs @@ -25,7 +25,13 @@ use tari_utilities::hex::Hex; use crate::{ chain_storage::{BlockchainBackend, BlockchainDatabase, UtxoMinedInfo}, - transactions::transaction_components::{ContractConstitution, OutputType, SideChainFeatures, TransactionOutput}, + transactions::transaction_components::{ + ContractConstitution, + ContractUpdateProposal, + OutputType, + SideChainFeatures, + TransactionOutput, + }, validation::ValidationError, }; @@ -98,6 +104,26 @@ pub fn fetch_contract_constitution( Ok(constitution.clone()) } +pub fn fetch_contract_update_proposal( + db: &BlockchainDatabase, + contract_id: FixedHash, + proposal_id: u64, +) -> Result { + let features = fetch_contract_features(db, contract_id, OutputType::ContractConstitutionProposal)?; + match features + .into_iter() + .filter_map(|feature| feature.update_proposal) + .find(|proposal| proposal.proposal_id == proposal_id) + { + Some(proposal) => Ok(proposal), + None => Err(ValidationError::DanLayerError(format!( + "Contract update proposal not found for contract_id {} and proposal_id {}", + contract_id.to_hex(), + proposal_id + ))), + } +} + pub fn get_sidechain_features(output: &TransactionOutput) -> Result<&SideChainFeatures, ValidationError> { match output.features.sidechain_features.as_ref() { Some(features) => Ok(features), diff --git a/base_layer/core/src/validation/dan_validators/mod.rs b/base_layer/core/src/validation/dan_validators/mod.rs index 19daf722d9..7cc36845fe 100644 --- a/base_layer/core/src/validation/dan_validators/mod.rs +++ b/base_layer/core/src/validation/dan_validators/mod.rs @@ -35,6 +35,15 @@ use constitution_validator::validate_constitution; mod definition_validator; use definition_validator::validate_definition; +mod update_proposal_validator; +use update_proposal_validator::validate_update_proposal; + +mod update_proposal_acceptance_validator; +use update_proposal_acceptance_validator::validate_update_proposal_acceptance; + +mod amendment_validator; +use amendment_validator::validate_amendment; + mod helpers; #[cfg(test)] @@ -59,6 +68,11 @@ impl MempoolTransactionValidation for TxDanLayerValidator< OutputType::ContractDefinition => validate_definition(&self.db, output)?, OutputType::ContractConstitution => validate_constitution(&self.db, output)?, OutputType::ContractValidatorAcceptance => validate_acceptance(&self.db, output)?, + OutputType::ContractConstitutionProposal => validate_update_proposal(&self.db, output)?, + OutputType::ContractConstitutionChangeAcceptance => { + validate_update_proposal_acceptance(&self.db, output)? + }, + OutputType::ContractAmendment => validate_amendment(&self.db, output)?, _ => continue, } } diff --git a/base_layer/core/src/validation/dan_validators/test_helpers.rs b/base_layer/core/src/validation/dan_validators/test_helpers.rs index c91c44ddea..5fcc54d0c3 100644 --- a/base_layer/core/src/validation/dan_validators/test_helpers.rs +++ b/base_layer/core/src/validation/dan_validators/test_helpers.rs @@ -36,13 +36,14 @@ use crate::{ transaction_components::{ vec_into_fixed_string, CheckpointParameters, - CommitteeMembers, ConstitutionChangeFlags, ConstitutionChangeRules, ContractAcceptanceRequirements, + ContractAmendment, ContractConstitution, ContractDefinition, ContractSpecification, + ContractUpdateProposal, OutputFeatures, RequirementsForConstitutionChange, SideChainConsensus, @@ -84,6 +85,17 @@ pub fn publish_constitution( create_block(blockchain, "constitution", schema); } +pub fn publish_update_proposal( + blockchain: &mut TestBlockchain, + change: UnblindedOutput, + contract_id: FixedHash, + proposal_id: u64, + committee: Vec, +) { + let schema = create_contract_proposal_schema(contract_id, change, proposal_id, committee); + create_block(blockchain, "proposal", schema); +} + pub fn schema_to_transaction(schema: &TransactionSchema) -> (Transaction, Vec) { let mut utxos = Vec::new(); @@ -129,9 +141,15 @@ pub fn create_contract_constitution_schema( input: UnblindedOutput, committee: Vec, ) -> TransactionSchema { - let validator_committee: CommitteeMembers = vec![PublicKey::default()].try_into().unwrap(); - let constitution = ContractConstitution { - validator_committee, + let constitution = create_contract_constitution(committee); + let constitution_features = OutputFeatures::for_contract_constitution(contract_id, constitution); + + txn_schema!(from: vec![input], to: vec![0.into()], fee: 5.into(), lock: 0, features: constitution_features) +} + +pub fn create_contract_constitution(validator_keys: Vec) -> ContractConstitution { + ContractConstitution { + validator_committee: validator_keys.clone().try_into().unwrap(), acceptance_requirements: ContractAcceptanceRequirements { acceptance_period_expiry: 100, minimum_quorum_required: 5, @@ -145,14 +163,11 @@ pub fn create_contract_constitution_schema( change_flags: ConstitutionChangeFlags::all(), requirements_for_constitution_change: Some(RequirementsForConstitutionChange { minimum_constitution_committee_signatures: 5, - constitution_committee: Some(committee.try_into().unwrap()), + constitution_committee: Some(validator_keys.try_into().unwrap()), }), }, initial_reward: 100.into(), - }; - let constitution_features = OutputFeatures::for_contract_constitution(contract_id, constitution); - - txn_schema!(from: vec![input], to: vec![0.into()], fee: 5.into(), lock: 0, features: constitution_features) + } } pub fn create_contract_acceptance_schema( @@ -168,12 +183,71 @@ pub fn create_contract_acceptance_schema( txn_schema!(from: vec![input], to: vec![0.into()], fee: 5.into(), lock: 0, features: acceptance_features) } +pub fn create_contract_proposal_schema( + contract_id: FixedHash, + input: UnblindedOutput, + proposal_id: u64, + committee: Vec, +) -> TransactionSchema { + let proposal = ContractUpdateProposal { + proposal_id, + signature: Signature::default(), + updated_constitution: create_contract_constitution(committee), + }; + + let proposal_features = OutputFeatures::for_contract_update_proposal(contract_id, proposal); + + txn_schema!(from: vec![input], to: vec![0.into()], fee: 5.into(), lock: 0, features: proposal_features) +} + +pub fn create_contract_update_proposal_acceptance_schema( + contract_id: FixedHash, + input: UnblindedOutput, + proposal_id: u64, + validator_node_public_key: PublicKey, +) -> TransactionSchema { + let signature = Signature::default(); + + let acceptance_features = OutputFeatures::for_contract_update_proposal_acceptance( + contract_id, + proposal_id, + validator_node_public_key, + signature, + ); + + txn_schema!(from: vec![input], to: vec![0.into()], fee: 5.into(), lock: 0, features: acceptance_features) +} + +pub fn create_contract_amendment_schema( + contract_id: FixedHash, + input: UnblindedOutput, + proposal_id: u64, + committee: Vec, +) -> TransactionSchema { + let amendment = ContractAmendment { + proposal_id, + updated_constitution: create_contract_constitution(committee.clone()), + validator_committee: committee.try_into().unwrap(), + validator_signatures: vec![Signature::default()].try_into().unwrap(), + activation_window: 100, + }; + + let amendment_features = OutputFeatures::for_contract_amendment(contract_id, amendment); + + txn_schema!(from: vec![input], to: vec![0.into()], fee: 5.into(), lock: 0, features: amendment_features) +} + pub fn assert_dan_error(blockchain: &TestBlockchain, transaction: &Transaction, expected_message: &str) { let validator = TxDanLayerValidator::new(blockchain.db().clone()); let err = validator.validate(transaction).unwrap_err(); match err { ValidationError::DanLayerError(message) => { - assert!(message.contains(expected_message)) + assert!( + message.contains(expected_message), + "Message \"{}\" does not contain \"{}\"", + message, + expected_message + ) }, _ => panic!("Expected a consensus error"), } diff --git a/base_layer/core/src/validation/dan_validators/update_proposal_acceptance_validator.rs b/base_layer/core/src/validation/dan_validators/update_proposal_acceptance_validator.rs new file mode 100644 index 0000000000..6cdcae977a --- /dev/null +++ b/base_layer/core/src/validation/dan_validators/update_proposal_acceptance_validator.rs @@ -0,0 +1,251 @@ +// Copyright 2022, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use tari_common_types::types::{FixedHash, PublicKey}; +use tari_utilities::hex::Hex; + +use super::helpers::{ + fetch_contract_features, + fetch_contract_update_proposal, + get_sidechain_features, + validate_output_type, +}; +use crate::{ + chain_storage::{BlockchainBackend, BlockchainDatabase}, + transactions::transaction_components::{ + ContractUpdateProposal, + ContractUpdateProposalAcceptance, + OutputType, + SideChainFeatures, + TransactionOutput, + }, + validation::ValidationError, +}; + +pub fn validate_update_proposal_acceptance( + db: &BlockchainDatabase, + output: &TransactionOutput, +) -> Result<(), ValidationError> { + validate_output_type(output, OutputType::ContractConstitutionChangeAcceptance)?; + + let sidechain_features = get_sidechain_features(output)?; + let contract_id = sidechain_features.contract_id; + + let acceptance_features = get_contract_update_proposal_acceptance(sidechain_features)?; + let proposal_id = acceptance_features.proposal_id; + let validator_node_public_key = &acceptance_features.validator_node_public_key; + + let proposal = fetch_contract_update_proposal(db, contract_id, proposal_id)?; + + validate_duplication(db, contract_id, proposal_id, validator_node_public_key)?; + validate_public_key(proposal, validator_node_public_key)?; + + // TODO: check that the signature of the transaction is valid + // TODO: check that the acceptance is inside the acceptance window of the proposal + // TODO: check that the stake of the transaction is at least the minimum specified in the constitution + + Ok(()) +} + +/// Retrieves a contract update proposal acceptance object from the sidechain features, returns an error if not present +fn get_contract_update_proposal_acceptance( + sidechain_feature: &SideChainFeatures, +) -> Result<&ContractUpdateProposalAcceptance, ValidationError> { + match sidechain_feature.update_proposal_acceptance.as_ref() { + Some(acceptance) => Ok(acceptance), + None => Err(ValidationError::DanLayerError( + "Contract update proposal acceptance features not found".to_string(), + )), + } +} + +/// Checks that the validator node has not already published the acceptance for the contract +fn validate_duplication( + db: &BlockchainDatabase, + contract_id: FixedHash, + proposal_id: u64, + validator_node_public_key: &PublicKey, +) -> Result<(), ValidationError> { + let features = fetch_contract_features(db, contract_id, OutputType::ContractConstitutionChangeAcceptance)?; + match features + .into_iter() + .filter_map(|feature| feature.update_proposal_acceptance) + .find(|feature| { + feature.validator_node_public_key == *validator_node_public_key && feature.proposal_id == proposal_id + }) { + Some(_) => { + let msg = format!( + "Duplicated contract update proposal acceptance for contract_id ({:?}), proposal_id ({}) and \ + validator_node_public_key ({:?})", + contract_id.to_hex(), + proposal_id, + validator_node_public_key, + ); + Err(ValidationError::DanLayerError(msg)) + }, + None => Ok(()), + } +} + +/// Checks that the validator public key is present as part of the proposed committee in the constitution +fn validate_public_key( + proposal: ContractUpdateProposal, + validator_node_public_key: &PublicKey, +) -> Result<(), ValidationError> { + let is_validator_in_committee = proposal + .updated_constitution + .validator_committee + .members() + .contains(validator_node_public_key); + if !is_validator_in_committee { + let msg = format!( + "Validator node public key is not in committee ({:?})", + validator_node_public_key + ); + return Err(ValidationError::DanLayerError(msg)); + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use tari_common_types::types::PublicKey; + use tari_utilities::hex::Hex; + + use crate::validation::dan_validators::test_helpers::{ + assert_dan_error, + create_block, + create_contract_constitution_schema, + create_contract_update_proposal_acceptance_schema, + init_test_blockchain, + publish_constitution, + publish_definition, + publish_update_proposal, + schema_to_transaction, + }; + + #[test] + fn proposal_must_exist() { + // initialise a blockchain with enough funds to spend at contract transactions + let (mut blockchain, change) = init_test_blockchain(); + + // publish the contract definition into a block + let contract_id = publish_definition(&mut blockchain, change[0].clone()); + + // publish the contract constitution into a block + let validator_node_public_key = PublicKey::default(); + let committee = vec![validator_node_public_key.clone()]; + publish_constitution(&mut blockchain, change[1].clone(), contract_id, committee); + + // skip the publication of the contract update proposal + + // create a contract update proposal acceptance transaction + let proposal_id = 1; + let schema = create_contract_update_proposal_acceptance_schema( + contract_id, + change[1].clone(), + proposal_id, + validator_node_public_key, + ); + let (tx, _) = schema_to_transaction(&schema); + + // try to validate the acceptance transaction and check that we get the error + assert_dan_error(&blockchain, &tx, "Contract update proposal not found"); + } + + #[test] + fn it_rejects_duplicated_acceptances() { + // initialise a blockchain with enough funds to spend at contract transactions + let (mut blockchain, change) = init_test_blockchain(); + + // publish the contract definition into a block + let contract_id = publish_definition(&mut blockchain, change[0].clone()); + + // publish the contract constitution into a block + let validator_node_public_key = PublicKey::default(); + let committee = vec![validator_node_public_key.clone()]; + publish_constitution(&mut blockchain, change[1].clone(), contract_id, committee.clone()); + + // publish the contract update proposal into a block + let proposal_id: u64 = 1; + publish_update_proposal(&mut blockchain, change[2].clone(), contract_id, proposal_id, committee); + + // publish the contract update proposal acceptance into a block + let proposal_id = 1; + let schema = create_contract_update_proposal_acceptance_schema( + contract_id, + change[3].clone(), + proposal_id, + validator_node_public_key.clone(), + ); + create_block(&mut blockchain, "proposal-acceptance", schema); + + // create a (duplicated) contract acceptance transaction + let proposal_id = 1; + let schema = create_contract_update_proposal_acceptance_schema( + contract_id, + change[4].clone(), + proposal_id, + validator_node_public_key, + ); + let (tx, _) = schema_to_transaction(&schema); + + // try to validate the (duplicated) proposal acceptance transaction and check that we get the error + assert_dan_error(&blockchain, &tx, "Duplicated contract update proposal acceptance"); + } + + #[test] + fn it_rejects_acceptances_of_non_committee_members() { + // initialise a blockchain with enough funds to spend at contract transactions + let (mut blockchain, change) = init_test_blockchain(); + + // publish the contract definition into a block + let contract_id = publish_definition(&mut blockchain, change[0].clone()); + + // publish the contract constitution into a block + let schema = create_contract_constitution_schema(contract_id, change[1].clone(), vec![]); + create_block(&mut blockchain, "constitution", schema); + + // publish the contract update proposal into a block + // we deliberately use a committee with only a defult public key to be able to trigger the committee error later + let proposal_id: u64 = 1; + let committee = vec![PublicKey::default()]; + publish_update_proposal(&mut blockchain, change[2].clone(), contract_id, proposal_id, committee); + + // publish the contract update proposal acceptance into a block + // we use a public key that is not included in the proposal committee, to trigger the error + let validator_node_public_key = + PublicKey::from_hex("70350e09c474809209824c6e6888707b7dd09959aa227343b5106382b856f73a").unwrap(); + let proposal_id = 1; + let schema = create_contract_update_proposal_acceptance_schema( + contract_id, + change[3].clone(), + proposal_id, + validator_node_public_key, + ); + let (tx, _) = schema_to_transaction(&schema); + + // try to validate the proposal acceptance transaction and check that we get the committee error + assert_dan_error(&blockchain, &tx, "Validator node public key is not in committee"); + } +} diff --git a/base_layer/core/src/validation/dan_validators/update_proposal_validator.rs b/base_layer/core/src/validation/dan_validators/update_proposal_validator.rs new file mode 100644 index 0000000000..c88bd2e933 --- /dev/null +++ b/base_layer/core/src/validation/dan_validators/update_proposal_validator.rs @@ -0,0 +1,154 @@ +// Copyright 2022, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use tari_common_types::types::FixedHash; +use tari_utilities::hex::Hex; + +use super::helpers::{ + fetch_contract_constitution, + fetch_contract_features, + get_sidechain_features, + validate_output_type, +}; +use crate::{ + chain_storage::{BlockchainBackend, BlockchainDatabase}, + transactions::transaction_components::{ContractUpdateProposal, OutputType, SideChainFeatures, TransactionOutput}, + validation::ValidationError, +}; + +pub fn validate_update_proposal( + db: &BlockchainDatabase, + output: &TransactionOutput, +) -> Result<(), ValidationError> { + validate_output_type(output, OutputType::ContractConstitutionProposal)?; + + let sidechain_features = get_sidechain_features(output)?; + let contract_id = sidechain_features.contract_id; + + let proposal_features = get_update_proposal(sidechain_features)?; + let proposal_id = proposal_features.proposal_id; + + fetch_contract_constitution(db, contract_id)?; + + validate_duplication(db, contract_id, proposal_id)?; + + Ok(()) +} + +fn get_update_proposal(sidechain_feature: &SideChainFeatures) -> Result<&ContractUpdateProposal, ValidationError> { + match sidechain_feature.update_proposal.as_ref() { + Some(proposal) => Ok(proposal), + None => Err(ValidationError::DanLayerError( + "Contract update proposal features not found".to_string(), + )), + } +} + +fn validate_duplication( + db: &BlockchainDatabase, + contract_id: FixedHash, + proposal_id: u64, +) -> Result<(), ValidationError> { + let features = fetch_contract_features(db, contract_id, OutputType::ContractConstitutionProposal)?; + match features + .into_iter() + .filter_map(|feature| feature.update_proposal) + .find(|proposal| proposal.proposal_id == proposal_id) + { + Some(_) => { + let msg = format!( + "Duplicated contract update proposal for contract_id ({:?}) and proposal_id ({:?})", + contract_id.to_hex(), + proposal_id, + ); + Err(ValidationError::DanLayerError(msg)) + }, + None => Ok(()), + } +} + +#[cfg(test)] +mod test { + use tari_common_types::types::PublicKey; + + use crate::validation::dan_validators::test_helpers::{ + assert_dan_error, + create_contract_proposal_schema, + init_test_blockchain, + publish_constitution, + publish_definition, + publish_update_proposal, + schema_to_transaction, + }; + + #[test] + fn constitution_must_exist() { + // initialise a blockchain with enough funds to spend at contract transactions + let (mut blockchain, change) = init_test_blockchain(); + + // publish the contract definition into a block + let contract_id = publish_definition(&mut blockchain, change[0].clone()); + + // skip the contract constitution publication + + // create a contract proposal transaction + let validator_node_public_key = PublicKey::default(); + let committee = vec![validator_node_public_key]; + let proposal_id: u64 = 1; + let schema = create_contract_proposal_schema(contract_id, change[3].clone(), proposal_id, committee); + let (tx, _) = schema_to_transaction(&schema); + + // try to validate the acceptance transaction and check that we get the error + assert_dan_error(&blockchain, &tx, "Contract constitution not found"); + } + + #[test] + fn it_rejects_duplicated_proposals() { + // initialise a blockchain with enough funds to spend at contract transactions + let (mut blockchain, change) = init_test_blockchain(); + + // publish the contract definition into a block + let contract_id = publish_definition(&mut blockchain, change[0].clone()); + + // publish the contract constitution into a block + let validator_node_public_key = PublicKey::default(); + let committee = vec![validator_node_public_key]; + publish_constitution(&mut blockchain, change[1].clone(), contract_id, committee.clone()); + + // publish a contract update proposal into a block + let proposal_id: u64 = 1; + publish_update_proposal( + &mut blockchain, + change[2].clone(), + contract_id, + proposal_id, + committee.clone(), + ); + + // create a (duplicated) contract proposal transaction + let schema = create_contract_proposal_schema(contract_id, change[3].clone(), proposal_id, committee); + let (tx, _) = schema_to_transaction(&schema); + + // try to validate the duplicated proposal transaction and check that we get the error + assert_dan_error(&blockchain, &tx, "Duplicated contract update proposal"); + } +} diff --git a/integration_tests/features/WalletCli.feature b/integration_tests/features/WalletCli.feature index 903ca552ba..a11ce1dfc6 100644 --- a/integration_tests/features/WalletCli.feature +++ b/integration_tests/features/WalletCli.feature @@ -195,7 +195,13 @@ Feature: Wallet CLI And I publish a contract definition DEF1 from file "fixtures/contract_definition.json" on wallet WALLET via command line And mining node MINE mines 8 blocks Then wallet WALLET has at least 1 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled - And I publish a contract amendment from file "fixtures/contract_amendment.json" on wallet WALLET via command line + When I publish a contract constitution from file "fixtures/contract_constitution.json" on wallet WALLET via command line And mining node MINE mines 8 blocks Then wallet WALLET has at least 2 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled + And I publish a contract update proposal from file "fixtures/contract_update_proposal.json" on wallet WALLET via command line + And mining node MINE mines 8 blocks + Then wallet WALLET has at least 3 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled + And I publish a contract amendment from file "fixtures/contract_amendment.json" on wallet WALLET via command line + And mining node MINE mines 8 blocks + Then wallet WALLET has at least 4 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled Then WALLET is connected to BASE