Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(base_layer): validation of committee membership in contract acceptances #4221

Merged
merged 25 commits into from
Jun 24, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f17e3f3
create tx validation for acceptances
mrnaveira Jun 21, 2022
1fdad86
create unit test for acceptances validation
mrnaveira Jun 21, 2022
e0b1512
refactor dan validators into a separated module
mrnaveira Jun 22, 2022
33193c8
create a test_helper module
mrnaveira Jun 22, 2022
c6e4b29
refactor dan validator into a separated struct
mrnaveira Jun 22, 2022
bbfc4a3
clean up the acceptance validator code
mrnaveira Jun 22, 2022
5acdbe0
create a helper with common validation functions
mrnaveira Jun 22, 2022
5843431
parameterize test helpers
mrnaveira Jun 22, 2022
e5148c7
create a custom dan layer error variant
mrnaveira Jun 22, 2022
c697bfc
add comments
mrnaveira Jun 22, 2022
d7f979f
fix integration test of contract acceptance
mrnaveira Jun 22, 2022
66845d1
fix clipply waning in mempool benches
mrnaveira Jun 22, 2022
d6656ac
Merge branch 'development' into validate-acceptances
mrnaveira Jun 22, 2022
42ab386
increase acceptance timeout in integration tests
mrnaveira Jun 23, 2022
8585858
pass the validator public key as a reference
mrnaveira Jun 23, 2022
e86d27b
Merge branch 'development' into validate-acceptances
aviator-app[bot] Jun 23, 2022
0c53e79
try to fix the indentity file problem in circleci
mrnaveira Jun 23, 2022
7ef5924
Merge branch 'validate-acceptances' of https://github.com/mrnaveira/t…
mrnaveira Jun 23, 2022
0d381c4
make the validator identity file writable in tests
mrnaveira Jun 23, 2022
1bfd180
Merge branch 'development' into validate-acceptances
mrnaveira Jun 23, 2022
eea28b2
Merge branch 'development' of https://github.com/mrnaveira/tari into …
mrnaveira Jun 23, 2022
0efafe6
fix js code format
mrnaveira Jun 23, 2022
53aeb31
remove console log
mrnaveira Jun 23, 2022
a10237b
fix eslint warnings in integration tests
mrnaveira Jun 23, 2022
e5d6cb2
Merge branch 'development' into validate-acceptances
mrnaveira Jun 24, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions applications/tari_base_node/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use tari_core::{
transactions::CryptoFactories,
validation::{
block_validators::{BodyOnlyValidator, OrphanBlockValidator},
dan_validators::TxDanLayerValidator,
header_validator::HeaderValidator,
transaction_validators::{
MempoolValidator,
Expand Down Expand Up @@ -246,6 +247,7 @@ async fn build_node_context(
)),
Box::new(TxInputAndMaturityValidator::new(blockchain_db.clone())),
Box::new(TxConsensusValidator::new(blockchain_db.clone())),
Box::new(TxDanLayerValidator::new(blockchain_db.clone())),
]);
let mempool = Mempool::new(
app_config.base_node.mempool.clone(),
Expand Down
16 changes: 10 additions & 6 deletions base_layer/core/benches/mempool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,14 @@ mod benches {
CryptoFactories,
},
tx,
validation::transaction_validators::{
MempoolValidator,
TxConsensusValidator,
TxInputAndMaturityValidator,
TxInternalConsistencyValidator,
validation::{
dan_validators::TxDanLayerValidator,
transaction_validators::{
MempoolValidator,
TxConsensusValidator,
TxInputAndMaturityValidator,
TxInternalConsistencyValidator,
},
},
};
use tokio::{runtime::Runtime, task};
Expand Down Expand Up @@ -85,7 +88,8 @@ mod benches {
db.clone(),
)),
Box::new(TxInputAndMaturityValidator::new(db.clone())),
Box::new(TxConsensusValidator::new(db)),
Box::new(TxConsensusValidator::new(db.clone())),
Box::new(TxDanLayerValidator::new(db)),
]);
let mempool = Mempool::new(config, rules, Box::new(mempool_validator));
const NUM_TXNS: usize = 100;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ use tari_utilities::ByteArray;
use super::{
ContractAcceptance,
ContractAmendment,
ContractConstitution,
ContractDefinition,
ContractUpdateProposal,
ContractUpdateProposalAcceptance,
Expand Down Expand Up @@ -302,6 +303,18 @@ impl OutputFeatures {
}
}

pub fn for_contract_constitution(contract_id: FixedHash, constitution: ContractConstitution) -> OutputFeatures {
Self {
output_type: OutputType::ContractConstitution,
sidechain_features: Some(
SideChainFeaturesBuilder::new(contract_id)
.with_contract_constitution(constitution)
.finish(),
),
..Default::default()
}
}

pub fn for_contract_acceptance(
contract_id: FixedHash,
validator_node_public_key: PublicKey,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// 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::PublicKey;

use super::helpers::{get_contract_constitution, get_sidechain_features, validate_output_type};
use crate::{
chain_storage::{BlockchainBackend, BlockchainDatabase},
transactions::transaction_components::{
ContractAcceptance,
ContractConstitution,
OutputType,
SideChainFeatures,
TransactionOutput,
},
validation::ValidationError,
};

/// This validator checks that the provided output corresponds to a valid Contract Acceptance in the DAN layer
pub fn validate_acceptance<B: BlockchainBackend>(
db: &BlockchainDatabase<B>,
output: &TransactionOutput,
) -> Result<(), ValidationError> {
validate_output_type(output, OutputType::ContractValidatorAcceptance)?;

let sidechain_features = get_sidechain_features(output)?;
let contract_id = sidechain_features.contract_id;

let acceptance_features = get_contract_acceptance(sidechain_features)?;
let validator_node_public_key = &acceptance_features.validator_node_public_key;

let constitution = get_contract_constitution(db, contract_id)?;

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(())
}

/// Retrieves a contract acceptance object from the sidechain features, returns an error if not present
fn get_contract_acceptance(sidechain_feature: &SideChainFeatures) -> Result<&ContractAcceptance, ValidationError> {
match sidechain_feature.acceptance.as_ref() {
Some(acceptance) => Ok(acceptance),
None => Err(ValidationError::DanLayerError(
"Invalid contract acceptance: acceptance features not found".to_string(),
)),
}
}

/// Checks that the validator public key is present as part of the proposed committee in the constitution
fn validate_public_key(
constitution: ContractConstitution,
validator_node_public_key: &PublicKey,
) -> Result<(), ValidationError> {
let is_validator_in_committee = constitution
.validator_committee
.members()
.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
);
return Err(ValidationError::DanLayerError(msg));
}

Ok(())
}

#[cfg(test)]
mod test {
use tari_common_types::types::PublicKey;
use tari_p2p::Network;
use tari_utilities::hex::Hex;

use crate::{
block_spec,
consensus::ConsensusManagerBuilder,
test_helpers::blockchain::TestBlockchain,
transactions::tari_amount::T,
txn_schema,
validation::{
dan_validators::{
test_helpers::{
create_block,
create_contract_acceptance_schema,
create_contract_constitution_schema,
create_contract_definition_schema,
schema_to_transaction,
},
TxDanLayerValidator,
},
MempoolTransactionValidation,
ValidationError,
},
};

#[test]
fn it_rejects_contract_acceptances_of_non_committee_members() {
// initialize a brand new taest blockchain with a genesis block
let consensus_manager = ConsensusManagerBuilder::new(Network::LocalNet).build();
let mut blockchain = TestBlockchain::create(consensus_manager);
let (_, coinbase_a) = blockchain.add_next_tip(block_spec!("1")).unwrap();

// create a block with some UTXOs to spend later at contract transactions
let schema = txn_schema!(from: vec![coinbase_a], to: vec![50 * T, 50 * T, 50 * T]);
let change_outputs = create_block(&mut blockchain, "2", schema);

// publish the contract definition into a block
let (contract_id, schema) = create_contract_definition_schema(change_outputs[0].clone());
create_block(&mut blockchain, "3", schema);

// publish the contract constitution into a block
// we deliberately use a committee with only a defult public key to be able to trigger the committee error later
let committee = vec![PublicKey::default()];
let schema = create_contract_constitution_schema(contract_id, change_outputs[1].clone(), committee);
create_block(&mut blockchain, "4", schema);

// create a contract acceptance transaction
// we use a public key that is not included in the constitution committee, to trigger the error
let validator_node_public_key =
PublicKey::from_hex("70350e09c474809209824c6e6888707b7dd09959aa227343b5106382b856f73a").unwrap();
let schema =
create_contract_acceptance_schema(contract_id, change_outputs[2].clone(), validator_node_public_key);
let (txs, _) = schema_to_transaction(&[schema]);

// try to validate the acceptance transaction and check that we get the committee error
let validator = TxDanLayerValidator::new(blockchain.db().clone());
let err = validator.validate(txs.first().unwrap()).unwrap_err();
match err {
ValidationError::DanLayerError(message) => {
assert!(message.contains("Invalid contract acceptance: validator node public key is not in committee"))
},
_ => panic!("Expected a consensus error"),
}
}
}
109 changes: 109 additions & 0 deletions base_layer/core/src/validation/dan_validators/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// 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 crate::{
chain_storage::{BlockchainBackend, BlockchainDatabase},
transactions::transaction_components::{ContractConstitution, OutputType, SideChainFeatures, TransactionOutput},
validation::ValidationError,
};

pub fn validate_output_type(
output: &TransactionOutput,
expected_output_type: OutputType,
) -> Result<(), ValidationError> {
let output_type = output.features.output_type;
if output_type != expected_output_type {
let msg = format!(
"Invalid output type: expected {:?} but got {:?}",
expected_output_type, output_type
);
return Err(ValidationError::DanLayerError(msg));
}

Ok(())
}

pub fn get_sidechain_features(output: &TransactionOutput) -> Result<&SideChainFeatures, ValidationError> {
match output.features.sidechain_features.as_ref() {
Some(features) => Ok(features),
None => Err(ValidationError::DanLayerError(
"Sidechain features not found".to_string(),
)),
}
}

pub fn get_contract_constitution<B: BlockchainBackend>(
db: &BlockchainDatabase<B>,
contract_id: FixedHash,
) -> Result<ContractConstitution, ValidationError> {
let contract_outputs = db
.fetch_contract_outputs_by_contract_id_and_type(contract_id, OutputType::ContractConstitution)
.unwrap();

if contract_outputs.is_empty() {
return Err(ValidationError::DanLayerError(
"Contract constitution not found".to_string(),
));
}

// we assume that only one constitution should be present in the blockchain for any given contract
// TODO: create a validation to avoid duplicated constitution publishing
let utxo_info = match contract_outputs.first() {
Some(value) => value,
None => {
return Err(ValidationError::DanLayerError(
"Contract constitution UtxoMindInfo not found".to_string(),
))
},
};

let constitution_output = match utxo_info.output.as_transaction_output() {
Some(value) => value,
None => {
return Err(ValidationError::DanLayerError(
"Contract constitution output not found".to_string(),
))
},
};

let constitution_features = match constitution_output.features.sidechain_features.as_ref() {
Some(value) => value,
None => {
return Err(ValidationError::DanLayerError(
"Contract constitution output features not found".to_string(),
))
},
};

let constitution = match constitution_features.constitution.as_ref() {
Some(value) => value,
None => {
return Err(ValidationError::DanLayerError(
"Contract constitution data not found in the output features".to_string(),
))
},
};

Ok(constitution.clone())
}
Loading