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): basic checkpoint validation #4293

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
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use std::{
convert::{TryFrom, TryInto},
io,
slice,
};

use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -70,6 +71,15 @@ impl IntoIterator for CommitteeSignatures {
}
}

impl<'a> IntoIterator for &'a CommitteeSignatures {
type IntoIter = slice::Iter<'a, SignerSignature>;
type Item = <Self::IntoIter as Iterator>::Item;

fn into_iter(self) -> Self::IntoIter {
self.signatures.iter()
}
}

impl TryFrom<Vec<SignerSignature>> for CommitteeSignatures {
type Error = TransactionError;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@
// 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 super::helpers::get_sidechain_features;
use super::helpers::{fetch_contract_constitution, get_sidechain_features};
use crate::{
chain_storage::{BlockchainBackend, BlockchainDatabase},
transactions::transaction_components::{ContractCheckpoint, OutputType, SideChainFeatures, TransactionOutput},
transactions::transaction_components::{
CommitteeSignatures,
ContractCheckpoint,
ContractConstitution,
OutputType,
SideChainFeatures,
TransactionOutput,
},
validation::{
dan_validators::{
constitution_validator::validate_definition_existence,
helpers::fetch_current_contract_checkpoint,
DanLayerValidationError,
},
dan_validators::{helpers::fetch_current_contract_checkpoint, DanLayerValidationError},
ValidationError,
},
};
Expand All @@ -40,48 +43,71 @@ pub fn validate_contract_checkpoint<B: BlockchainBackend>(
) -> Result<(), ValidationError> {
let sidechain_features = get_sidechain_features(output)?;
let contract_id = sidechain_features.contract_id;
validate_definition_existence(db, contract_id)?;
let checkpoint = get_checkpoint(sidechain_features)?;

let constitution = fetch_contract_constitution(db, contract_id)?;
validate_committee(&constitution, &checkpoint.signatures)?;

let prev_cp = fetch_current_contract_checkpoint(db, contract_id)?;
validate_checkpoint_number(prev_cp.as_ref(), sidechain_features)?;
validate_checkpoint_number(prev_cp.as_ref(), checkpoint)?;

Ok(())
}

fn validate_checkpoint_number(
prev_checkpoint: Option<&ContractCheckpoint>,
sidechain_features: &SideChainFeatures,
) -> Result<(), DanLayerValidationError> {
let checkpoint = sidechain_features
.checkpoint
.as_ref()
.ok_or(DanLayerValidationError::MissingContractData {
fn get_checkpoint(sidechain_features: &SideChainFeatures) -> Result<&ContractCheckpoint, DanLayerValidationError> {
match sidechain_features.checkpoint.as_ref() {
Some(checkpoint) => Ok(checkpoint),
None => Err(DanLayerValidationError::MissingContractData {
contract_id: sidechain_features.contract_id,
output_type: OutputType::ContractCheckpoint,
})?;
}),
}
}

fn validate_checkpoint_number(
prev_checkpoint: Option<&ContractCheckpoint>,
current_checkpoint: &ContractCheckpoint,
) -> Result<(), DanLayerValidationError> {
let expected_number = prev_checkpoint.map(|cp| cp.checkpoint_number + 1).unwrap_or(0);
if checkpoint.checkpoint_number == expected_number {
if current_checkpoint.checkpoint_number == expected_number {
Ok(())
} else {
Err(DanLayerValidationError::CheckpointNonSequentialNumber {
got: checkpoint.checkpoint_number,
got: current_checkpoint.checkpoint_number,
expected: expected_number,
})
}
}

fn validate_committee(
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 {
return Err(DanLayerValidationError::InconsistentCommittee);
}

Ok(())
}

#[cfg(test)]
mod test {
use tari_common_types::types::Signature;

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,
},
DanLayerValidationError,
Expand Down Expand Up @@ -162,4 +188,49 @@ mod test {
expected: 2
}))
}

#[test]
fn constitution_must_exist() {
// 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());

// skip the contract constitution publication

// Create a checkpoint
let checkpoint = create_contract_checkpoint(0);
let schema = create_contract_checkpoint_schema(contract_id, utxos[1].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::ContractConstitutionNotFound { .. }
));
}

#[test]
fn it_rejects_checkpoints_with_non_committee_members() {
// initialise a blockchain with enough funds to spend at contract transactions
let (mut blockchain, utxos) = init_test_blockchain();

// Publish a new contract specifying a committee with only one member ("alice")
let (_, alice) = create_random_key_pair();
let contract_id = publish_contract(&mut blockchain, &utxos, vec![alice.clone()]);

// Create a checkpoint, with a committe that has an extra member ("bob") not present in the constiution
let mut checkpoint = create_contract_checkpoint(0);
let (_, bob) = create_random_key_pair();
checkpoint.signatures =
create_committee_signatures(vec![(alice, Signature::default()), (bob, Signature::default())]);
let schema = create_contract_checkpoint_schema(contract_id, utxos[1].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::InconsistentCommittee { .. }));
}
}
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 @@ -65,4 +65,6 @@ pub enum DanLayerValidationError {
ProposalAcceptanceWindowHasExpired { contract_id: FixedHash, proposal_id: u64 },
#[error("Checkpoint has non-sequential number. Got: {got}, expected: {expected}")]
CheckpointNonSequentialNumber { got: u64, expected: u64 },
#[error("Validator committee not consistent with contract constitution")]
InconsistentCommittee,
}
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,12 @@ pub fn assert_dan_validator_fail(blockchain: &TestBlockchain, transaction: &Tran
pub fn assert_dan_validator_success(blockchain: &TestBlockchain, transaction: &Transaction) {
perform_validation(blockchain, transaction).unwrap()
}

pub fn create_committee_signatures(key_signature_pairs: Vec<(PublicKey, Signature)>) -> CommitteeSignatures {
let signer_signatures: Vec<SignerSignature> = key_signature_pairs
.iter()
.map(|(k, s)| SignerSignature::new(k.clone(), s.clone()))
.collect();

CommitteeSignatures::new(signer_signatures.try_into().unwrap())
}