Skip to content

Commit

Permalink
feat(base_layer): basic validations for proposals, proposal acceptanc…
Browse files Browse the repository at this point in the history
…es 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
  • Loading branch information
mrnaveira authored Jun 30, 2022
1 parent ed39913 commit 64f8972
Show file tree
Hide file tree
Showing 10 changed files with 776 additions and 37 deletions.
42 changes: 22 additions & 20 deletions base_layer/core/src/chain_storage/lmdb_db/contract_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,17 @@ where T: Deref<Target = ConstTransaction<'a>>
) -> Result<Vec<FixedHash>, 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()
Expand All @@ -108,14 +107,13 @@ where T: Deref<Target = ConstTransaction<'a>>
) -> Result<Vec<FixedHash>, 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()
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ impl OutputType {
ContractValidatorAcceptance |
ContractCheckpoint |
ContractConstitutionProposal |
ContractConstitutionChangeAcceptance
ContractConstitutionChangeAcceptance |
ContractAmendment
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)),
}
}
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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");
}

Expand All @@ -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");
}
}
211 changes: 211 additions & 0 deletions base_layer/core/src/validation/dan_validators/amendment_validator.rs
Original file line number Diff line number Diff line change
@@ -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<B: BlockchainBackend>(
db: &BlockchainDatabase<B>,
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<B: BlockchainBackend>(
db: &BlockchainDatabase<B>,
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",
);
}
}
28 changes: 27 additions & 1 deletion base_layer/core/src/validation/dan_validators/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -98,6 +104,26 @@ pub fn fetch_contract_constitution<B: BlockchainBackend>(
Ok(constitution.clone())
}

pub fn fetch_contract_update_proposal<B: BlockchainBackend>(
db: &BlockchainDatabase<B>,
contract_id: FixedHash,
proposal_id: u64,
) -> Result<ContractUpdateProposal, 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(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),
Expand Down
Loading

0 comments on commit 64f8972

Please sign in to comment.