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): checkpoint quorum validation #4303

Merged
merged 4 commits into from
Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
149 changes: 120 additions & 29 deletions base_layer/core/src/validation/dan_validators/checkpoint_validator.rs
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 },
}
Original file line number Diff line number Diff line change
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