Skip to content

Commit

Permalink
feat(base_layer): validate duplicated acceptances (#4233)
Browse files Browse the repository at this point in the history
Description
---
* Added new validation for contract acceptances: validator nodes can only accept a contract once
* Added a unit test to check that the constitution must exist beforehand 
* Refactored some redundant helper code for validations

Motivation and Context
---
The base layer should check that no duplicated acceptances (same contract and validator node) are published.

How Has This Been Tested?
---
* New unit test to check the new validation
* The existing unit and integration tests pass
  • Loading branch information
mrnaveira committed Jun 28, 2022
1 parent c2efd5e commit 3d8a3b2
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,15 @@
// 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::PublicKey;

use super::helpers::{fetch_contract_constitution, get_sidechain_features, validate_output_type};
use tari_common_types::types::{FixedHash, PublicKey};
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::{
Expand Down Expand Up @@ -50,12 +56,12 @@ pub fn validate_acceptance<B: BlockchainBackend>(

let constitution = fetch_contract_constitution(db, contract_id)?;

validate_duplication(db, contract_id, validator_node_public_key)?;
validate_public_key(constitution, validator_node_public_key)?;

// TODO: check that the signature of the transaction is valid
// TODO: check that the acceptance is inside the accpentance window of the constiution
// TODO: check that the stake of the transaction is at least the minimum specified in the constitution
// TODO: check for duplicated acceptances

Ok(())
}
Expand All @@ -70,6 +76,30 @@ fn get_contract_acceptance(sidechain_feature: &SideChainFeatures) -> Result<&Con
}
}

/// Checks that the validator node has not already published the acceptance for the contract
fn validate_duplication<B: BlockchainBackend>(
db: &BlockchainDatabase<B>,
contract_id: FixedHash,
validator_node_public_key: &PublicKey,
) -> Result<(), ValidationError> {
let features = fetch_contract_features(db, contract_id, OutputType::ContractValidatorAcceptance)?;
match features
.into_iter()
.filter_map(|feature| feature.acceptance)
.find(|feature| feature.validator_node_public_key == *validator_node_public_key)
{
Some(_) => {
let msg = format!(
"Duplicated contract acceptance for contract_id ({:?}) and validator_node_public_key ({:?})",
contract_id.to_hex(),
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(
constitution: ContractConstitution,
Expand Down Expand Up @@ -101,10 +131,56 @@ mod test {
create_contract_acceptance_schema,
create_contract_constitution_schema,
init_test_blockchain,
publish_constitution,
publish_definition,
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 acceptance transaction
let validator_node_public_key = PublicKey::default();
let schema = create_contract_acceptance_schema(contract_id, change[1].clone(), 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 constitution 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);

// publish a contract acceptance into a block
let schema =
create_contract_acceptance_schema(contract_id, change[2].clone(), validator_node_public_key.clone());
create_block(&mut blockchain, "acceptance", schema);

// create a (duplicated) contract acceptance transaction
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
assert_dan_error(&blockchain, &tx, "Duplicated contract acceptance");
}

#[test]
fn it_rejects_contract_acceptances_of_non_committee_members() {
// initialise a blockchain with enough funds to spend at contract transactions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,32 +49,33 @@ fn validate_definition_existence<B: BlockchainBackend>(
db: &BlockchainDatabase<B>,
contract_id: FixedHash,
) -> Result<(), ValidationError> {
match fetch_contract_features(db, contract_id, OutputType::ContractDefinition)? {
Some(_) => Ok(()),
None => {
let msg = format!(
"Contract definition not found for contract_id ({:?})",
contract_id.to_hex()
);
Err(ValidationError::DanLayerError(msg))
},
let features = fetch_contract_features(db, contract_id, OutputType::ContractDefinition)?;
if features.is_empty() {
let msg = format!(
"Contract definition not found for contract_id ({:?})",
contract_id.to_hex()
);
return Err(ValidationError::DanLayerError(msg));
}

Ok(())
}

fn validate_duplication<B: BlockchainBackend>(
db: &BlockchainDatabase<B>,
contract_id: FixedHash,
) -> Result<(), ValidationError> {
match fetch_contract_features(db, contract_id, OutputType::ContractConstitution)? {
Some(_) => {
let msg = format!(
"Duplicated contract constitution for contract_id ({:?})",
contract_id.to_hex()
);
Err(ValidationError::DanLayerError(msg))
},
None => Ok(()),
let features = fetch_contract_features(db, contract_id, OutputType::ContractConstitution)?;
let is_duplicated = !features.is_empty();
if is_duplicated {
let msg = format!(
"Duplicated contract constitution for contract_id ({:?})",
contract_id.to_hex()
);
return Err(ValidationError::DanLayerError(msg));
}

Ok(())
}

#[cfg(test)]
Expand Down Expand Up @@ -111,7 +112,7 @@ mod test {

// publish the contract definition and constitution into a block
let contract_id = publish_definition(&mut blockchain, change[0].clone());
publish_constitution(&mut blockchain, change[1].clone(), contract_id);
publish_constitution(&mut blockchain, change[1].clone(), contract_id, vec![]);

// construct a transaction for the duplicated contract constitution
let schema = create_contract_constitution_schema(contract_id, change[2].clone(), Vec::new());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,17 @@ fn validate_duplication<B: BlockchainBackend>(
db: &BlockchainDatabase<B>,
contract_id: FixedHash,
) -> Result<(), ValidationError> {
match fetch_contract_features(db, contract_id, OutputType::ContractDefinition)? {
Some(_) => {
let msg = format!(
"Duplicated contract definition for contract_id ({:?})",
contract_id.to_hex()
);
Err(ValidationError::DanLayerError(msg))
},
None => Ok(()),
let features = fetch_contract_features(db, contract_id, OutputType::ContractDefinition)?;
let is_duplicated = !features.is_empty();
if is_duplicated {
let msg = format!(
"Duplicated contract definition for contract_id ({:?})",
contract_id.to_hex()
);
return Err(ValidationError::DanLayerError(msg));
}

Ok(())
}

#[cfg(test)]
Expand Down
59 changes: 28 additions & 31 deletions base_layer/core/src/validation/dan_validators/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use tari_common_types::types::FixedHash;
use tari_utilities::hex::Hex;

use crate::{
chain_storage::{BlockchainBackend, BlockchainDatabase},
chain_storage::{BlockchainBackend, BlockchainDatabase, UtxoMinedInfo},
transactions::transaction_components::{ContractConstitution, OutputType, SideChainFeatures, TransactionOutput},
validation::ValidationError,
};
Expand All @@ -49,47 +49,44 @@ pub fn fetch_contract_features<B: BlockchainBackend>(
db: &BlockchainDatabase<B>,
contract_id: FixedHash,
output_type: OutputType,
) -> Result<Option<SideChainFeatures>, ValidationError> {
let outputs = db
.fetch_contract_outputs_by_contract_id_and_type(contract_id, output_type)
.map_err(|err| ValidationError::DanLayerError(format!("Could not search outputs: {}", err)))?;
if outputs.is_empty() {
return Ok(None);
}
) -> Result<Vec<SideChainFeatures>, ValidationError> {
let features = fetch_contract_utxos(db, contract_id, output_type)?
.iter()
.filter_map(|utxo| utxo.output.as_transaction_output())
.filter_map(|output| output.features.sidechain_features.as_ref())
.cloned()
.collect();

let utxo_info = match outputs.first() {
Some(value) => value,
None => return Ok(None),
};
Ok(features)
}

let transaction_output = match utxo_info.output.as_transaction_output() {
Some(value) => value,
None => return Ok(None),
};
pub fn fetch_contract_utxos<B: BlockchainBackend>(
db: &BlockchainDatabase<B>,
contract_id: FixedHash,
output_type: OutputType,
) -> Result<Vec<UtxoMinedInfo>, ValidationError> {
let utxos = db
.fetch_contract_outputs_by_contract_id_and_type(contract_id, output_type)
.map_err(|err| ValidationError::DanLayerError(format!("Could not search outputs: {}", err)))?;

match transaction_output.features.sidechain_features.as_ref() {
Some(value) => Ok(Some(value.clone())),
None => Ok(None),
}
Ok(utxos)
}

pub fn fetch_contract_constitution<B: BlockchainBackend>(
db: &BlockchainDatabase<B>,
contract_id: FixedHash,
) -> Result<ContractConstitution, ValidationError> {
let features_result = fetch_contract_features(db, contract_id, OutputType::ContractConstitution)?;
let features = fetch_contract_features(db, contract_id, OutputType::ContractConstitution)?;
if features.is_empty() {
return Err(ValidationError::DanLayerError(format!(
"Contract constitution not found for contract_id {}",
contract_id.to_hex()
)));
}

let features = match features_result {
Some(value) => value,
None => {
return Err(ValidationError::DanLayerError(format!(
"Contract constitution not found for contract_id {}",
contract_id.to_hex()
)))
},
};
let feature = &features[0];

let constitution = match features.constitution.as_ref() {
let constitution = match feature.constitution.as_ref() {
Some(value) => value,
None => {
return Err(ValidationError::DanLayerError(
Expand Down
9 changes: 7 additions & 2 deletions base_layer/core/src/validation/dan_validators/test_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,13 @@ pub fn publish_definition(blockchain: &mut TestBlockchain, change: UnblindedOutp
contract_id
}

pub fn publish_constitution(blockchain: &mut TestBlockchain, change: UnblindedOutput, contract_id: FixedHash) {
let schema = create_contract_constitution_schema(contract_id, change, Vec::new());
pub fn publish_constitution(
blockchain: &mut TestBlockchain,
change: UnblindedOutput,
contract_id: FixedHash,
committee: Vec<PublicKey>,
) {
let schema = create_contract_constitution_schema(contract_id, change, committee);
create_block(blockchain, "constitution", schema);
}

Expand Down

0 comments on commit 3d8a3b2

Please sign in to comment.