diff --git a/base_layer/core/src/validation/dan_validators/checkpoint_validator.rs b/base_layer/core/src/validation/dan_validators/checkpoint_validator.rs index 5705e0ac4a..e26c092079 100644 --- a/base_layer/core/src/validation/dan_validators/checkpoint_validator.rs +++ b/base_layer/core/src/validation/dan_validators/checkpoint_validator.rs @@ -20,7 +20,9 @@ // 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::{Commitment, FixedHash}; +use std::{collections::HashSet, iter::FromIterator}; + +use tari_common_types::types::{Commitment, FixedHash, PublicKey}; use super::helpers::{fetch_contract_constitution, get_sidechain_features}; use crate::{ @@ -50,7 +52,7 @@ pub fn validate_contract_checkpoint( let checkpoint = get_checkpoint(sidechain_features)?; let constitution = fetch_contract_constitution(db, contract_id)?; - validate_committee(&constitution, &checkpoint.signatures)?; + validate_committee(checkpoint, &constitution)?; let prev_cp = fetch_current_contract_checkpoint(db, contract_id)?; validate_checkpoint_number(prev_cp.as_ref(), checkpoint)?; @@ -85,19 +87,43 @@ fn validate_checkpoint_number( } } +#[allow(clippy::mutable_key_type)] fn validate_committee( + checkpoint: &ContractCheckpoint, constitution: &ContractConstitution, - signatures: &CommitteeSignatures, ) -> Result<(), DanLayerValidationError> { - let committee = &constitution.validator_committee; - let are_all_signers_in_committee = signatures.into_iter().all(|s| committee.contains(s.signer())); - if !are_all_signers_in_committee { + // retrieve the list of commitee member keys of the constiution and the checkpoint + let checkpoint_members = get_commitee_members(&checkpoint.signatures); + let constitution_members = constitution.validator_committee.members(); + + // we use HashSets to avoid dealing with duplicated members and to easily compare elements + let checkpoint_member_set = HashSet::<&PublicKey>::from_iter(checkpoint_members); + let constitution_member_set = HashSet::<&PublicKey>::from_iter(constitution_members.iter()); + + // an non-empty difference (calculated from the checkpoint) means that there are non-constitution members + let are_invalid_members = checkpoint_member_set.difference(&constitution_member_set).count() > 0; + if are_invalid_members { return Err(DanLayerValidationError::InconsistentCommittee); } + // the intersection allow us to calculate the effective quorum of the checkpoint + let checkpoint_quorum = checkpoint_member_set.intersection(&constitution_member_set).count() as u32; + let required_quorum = constitution.checkpoint_params.minimum_quorum_required; + let is_quorum_met = checkpoint_quorum >= required_quorum; + if !is_quorum_met { + return Err(DanLayerValidationError::InsufficientQuorum { + got: checkpoint_quorum, + minimum: required_quorum, + }); + } + Ok(()) } +fn get_commitee_members(signatures: &CommitteeSignatures) -> Vec<&PublicKey> { + signatures.into_iter().map(|s| s.signer()).collect::>() +} + pub fn validate_signatures(checkpoint: &ContractCheckpoint, contract_id: &FixedHash) -> Result<(), ValidationError> { let challenge = create_checkpoint_challenge(checkpoint, contract_id); let signatures = &checkpoint.signatures; @@ -127,22 +153,31 @@ pub fn create_checkpoint_challenge(checkpoint: &ContractCheckpoint, contract_id: #[cfg(test)] mod test { + use std::convert::TryInto; + + use tari_common_types::types::FixedHash; + use super::create_checkpoint_challenge; - use crate::validation::dan_validators::{ - test_helpers::{ - assert_dan_validator_err, - assert_dan_validator_success, - create_committee_signatures, - create_contract_checkpoint, - create_contract_checkpoint_schema, - create_random_key_pair, - init_test_blockchain, - publish_checkpoint, - publish_contract, - publish_definition, - schema_to_transaction, + use crate::{ + consensus::ConsensusHashWriter, + validation::dan_validators::{ + test_helpers::{ + assert_dan_validator_err, + assert_dan_validator_success, + create_committee_signatures, + create_contract_checkpoint, + create_contract_checkpoint_schema, + create_contract_constitution, + create_random_key_pair, + init_test_blockchain, + publish_checkpoint, + publish_constitution, + publish_contract, + publish_definition, + schema_to_transaction, + }, + DanLayerValidationError, }, - DanLayerValidationError, }; #[test] @@ -273,25 +308,81 @@ mod test { // Publish a new contract specifying a committee with two members let alice = create_random_key_pair(); - let mut bob = create_random_key_pair(); + let bob = create_random_key_pair(); let contract_id = publish_contract(&mut blockchain, &utxos, vec![alice.1.clone(), bob.1.clone()]); - // To generate an invalid signature, lets swap bob private key for a random private key but keep the public key - let (altered_private_key, _) = create_random_key_pair(); - bob.0 = altered_private_key; - - // Create a checkpoint with the altered key pair, - // bob private key is altered compared to the one use in the contract constitution + // To create an invalid signature, let's use a challenge from a different checkpoint let mut checkpoint = create_contract_checkpoint(0); - let challenge = create_checkpoint_challenge(&checkpoint, &contract_id); + let challenge: FixedHash = ConsensusHashWriter::default() + .chain(&"invalid data".as_bytes()) + .finalize() + .into(); checkpoint.signatures = create_committee_signatures(vec![alice, bob], challenge.as_ref()); // Create the invalid transaction - let schema = create_contract_checkpoint_schema(contract_id, utxos[1].clone(), checkpoint); + let schema = create_contract_checkpoint_schema(contract_id, utxos[2].clone(), checkpoint); let (tx, _) = schema_to_transaction(&schema); // try to validate the checkpoint transaction and check that we get the error let err = assert_dan_validator_err(&blockchain, &tx); assert!(matches!(err, DanLayerValidationError::InvalidSignature { .. })); } + + #[test] + fn it_rejects_checkpoints_with_insufficient_quorum() { + // initialise a blockchain with enough funds to spend at contract transactions + let (mut blockchain, utxos) = init_test_blockchain(); + + // publish the contract definition into a block + let contract_id = publish_definition(&mut blockchain, utxos[0].clone()); + + // Publish a new contract constitution specifying a minimum quorum of 2 + let mut constitution = create_contract_constitution(); + let alice = create_random_key_pair(); + let bob = create_random_key_pair(); + let carol = create_random_key_pair(); + constitution.validator_committee = vec![alice.1.clone(), bob.1, carol.1].try_into().unwrap(); + constitution.checkpoint_params.minimum_quorum_required = 2; + publish_constitution(&mut blockchain, utxos[1].clone(), contract_id, constitution); + + // create a checkpoint with an insufficient quorum + let mut checkpoint = create_contract_checkpoint(0); + let challenge = create_checkpoint_challenge(&checkpoint, &contract_id); + checkpoint.signatures = create_committee_signatures(vec![alice], challenge.as_ref()); + let schema = create_contract_checkpoint_schema(contract_id, utxos[2].clone(), checkpoint); + let (tx, _) = schema_to_transaction(&schema); + + // try to validate the acceptance transaction and check that we get the error + let err = assert_dan_validator_err(&blockchain, &tx); + assert!(matches!(err, DanLayerValidationError::InsufficientQuorum { + got: 1, + minimum: 2 + })); + } + + #[test] + fn it_accepts_checkpoints_with_sufficient_quorum() { + // initialise a blockchain with enough funds to spend at contract transactions + let (mut blockchain, utxos) = init_test_blockchain(); + + // publish the contract definition into a block + let contract_id = publish_definition(&mut blockchain, utxos[0].clone()); + + // Publish a new contract constitution specifying a minimum quorum of 2 + let mut constitution = create_contract_constitution(); + let alice = create_random_key_pair(); + let bob = create_random_key_pair(); + constitution.validator_committee = vec![alice.1.clone(), bob.1.clone()].try_into().unwrap(); + constitution.checkpoint_params.minimum_quorum_required = 2; + publish_constitution(&mut blockchain, utxos[1].clone(), contract_id, constitution); + + // create a checkpoint with an enough quorum + let mut checkpoint = create_contract_checkpoint(0); + let challenge = create_checkpoint_challenge(&checkpoint, &contract_id); + checkpoint.signatures = create_committee_signatures(vec![alice, bob], challenge.as_ref()); + let schema = create_contract_checkpoint_schema(contract_id, utxos[2].clone(), checkpoint); + let (tx, _) = schema_to_transaction(&schema); + + assert_dan_validator_success(&blockchain, &tx); + } } diff --git a/base_layer/core/src/validation/dan_validators/error.rs b/base_layer/core/src/validation/dan_validators/error.rs index 98e30d5c54..6e35b1217b 100644 --- a/base_layer/core/src/validation/dan_validators/error.rs +++ b/base_layer/core/src/validation/dan_validators/error.rs @@ -67,4 +67,6 @@ pub enum DanLayerValidationError { CheckpointNonSequentialNumber { got: u64, expected: u64 }, #[error("Validator committee not consistent with contract constitution")] InconsistentCommittee, + #[error("Validator committee quorum not met: Got: {got}, minimum expected: {minimum} ")] + InsufficientQuorum { got: u32, minimum: u32 }, } 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 c75ab98def..f98a606b20 100644 --- a/base_layer/core/src/validation/dan_validators/test_helpers.rs +++ b/base_layer/core/src/validation/dan_validators/test_helpers.rs @@ -187,7 +187,7 @@ pub fn create_contract_constitution() -> ContractConstitution { }, consensus: SideChainConsensus::MerkleRoot, checkpoint_params: CheckpointParameters { - minimum_quorum_required: 5, + minimum_quorum_required: 0, abandoned_interval: 100, }, constitution_change_rules: ConstitutionChangeRules { @@ -341,11 +341,7 @@ pub fn assert_dan_validator_success(blockchain: &TestBlockchain, transaction: &T pub fn create_committee_signatures(keys: Vec<(PrivateKey, PublicKey)>, challenge: &[u8]) -> CommitteeSignatures { let signer_signatures: Vec = keys .into_iter() - .map(|(pri_k, pub_k)| { - let (nonce, _) = create_random_key_pair(); - let signature = Signature::sign(pri_k, nonce, challenge).unwrap(); - SignerSignature::new(pub_k, signature) - }) + .map(|(pri_k, _)| SignerSignature::sign(&pri_k, challenge)) .collect(); CommitteeSignatures::new(signer_signatures.try_into().unwrap())