Skip to content

Commit

Permalink
feat(base_layer): checkpoint quorum validation (#4303)
Browse files Browse the repository at this point in the history
Description
---
Refactored the base layer committee validation for checkpoints:
* Now it's based on `HashSet` of VNC public keys: one for the constitution and other for the checkpoint
    * Surprisingly, clippy warns about `mutable_key_type` for using `PublicKey` types as the keys in the `HashSet`. I'm not sure where the mutability comes from. I decided to ignore that rule for the validation function
* The existing validation of members is now done by checking the _difference_ of the public key sets
* The new quorum validation is done by checking the _intersection_ of the public key sets

Motivation and Context
---
The base layer need to validate if the quorum of an incoming checkpoint is met. The minimum quorum for checkpoints is specified in the `ContractConstitution`, in the `checkpoint_params.minimum_quorum_required field`

How Has This Been Tested?
---
New unit test to check both successful and failing scenarios regarding quorum conditions
  • Loading branch information
mrnaveira committed Jul 12, 2022
1 parent e42085a commit e1704f4
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 35 deletions.
149 changes: 120 additions & 29 deletions base_layer/core/src/validation/dan_validators/checkpoint_validator.rs
Expand Up @@ -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::{
Expand Down Expand Up @@ -50,7 +52,7 @@ pub fn validate_contract_checkpoint<B: BlockchainBackend>(
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)?;
Expand Down Expand Up @@ -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::<Vec<&PublicKey>>()
}

pub fn validate_signatures(checkpoint: &ContractCheckpoint, contract_id: &FixedHash) -> Result<(), ValidationError> {
let challenge = create_checkpoint_challenge(checkpoint, contract_id);
let signatures = &checkpoint.signatures;
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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);
}
}
2 changes: 2 additions & 0 deletions base_layer/core/src/validation/dan_validators/error.rs
Expand Up @@ -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 },
}
8 changes: 2 additions & 6 deletions base_layer/core/src/validation/dan_validators/test_helpers.rs
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<SignerSignature> = 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())
Expand Down

0 comments on commit e1704f4

Please sign in to comment.