Skip to content

Commit

Permalink
feat!: add checkpoint_number to checkpoint with basic base layer vali…
Browse files Browse the repository at this point in the history
…dations (#4258)

Description
---
- feat(validator-node): specify checkpoint number in create checkpoint request
- feat(base_layer/wallet): adds checkpoint_number to asset manager
- feat(base_layer/core): adds checkpoint number validation
- feat(base_layer/wallet): adds checkpoint number to proto files
- feat(base_layer/core): adds checkpoint_number to checkpoint

Motivation and Context
---
A checkpoint number provides a sequential ordering of checkpoints that is validated by the base layer. 
This should make dan layer coordination of checkpointing easier and provides an indication of how much checkpoint history has existed on chain.

How Has This Been Tested?
---
New unit tests for the base layer validations
Manually (In progress)
  • Loading branch information
sdbondi authored Jul 1, 2022
1 parent 6ce6b89 commit 7b76141
Show file tree
Hide file tree
Showing 30 changed files with 677 additions and 310 deletions.
5 changes: 3 additions & 2 deletions applications/tari_app_grpc/proto/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,9 @@ message CommitteeMembers {
}

message ContractCheckpoint {
bytes merkle_root = 1;
CommitteeSignatures signatures = 2;
uint64 checkpoint_number = 1;
bytes merkle_root = 2;
CommitteeSignatures signatures = 3;
}

message CheckpointParameters {
Expand Down
5 changes: 3 additions & 2 deletions applications/tari_app_grpc/proto/wallet.proto
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,9 @@ message CreateInitialAssetCheckpointResponse {
}

message CreateFollowOnAssetCheckpointRequest {
bytes contract_id = 1;
bytes merkle_root = 2;
uint64 checkpoint_number = 1;
bytes contract_id = 2;
bytes merkle_root = 3;
}

message CreateFollowOnAssetCheckpointResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ impl TryFrom<grpc::ContractConstitution> for ContractConstitution {
impl From<ContractCheckpoint> for grpc::ContractCheckpoint {
fn from(value: ContractCheckpoint) -> Self {
Self {
checkpoint_number: value.checkpoint_number,
merkle_root: value.merkle_root.to_vec(),
signatures: Some(value.signatures.into()),
}
Expand All @@ -320,6 +321,7 @@ impl TryFrom<grpc::ContractCheckpoint> for ContractCheckpoint {
let merkle_root = value.merkle_root.try_into().map_err(|_| "Invalid merkle root")?;
let signatures = value.signatures.map(TryInto::try_into).transpose()?.unwrap_or_default();
Ok(Self {
checkpoint_number: value.checkpoint_number,
merkle_root,
signatures,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -858,13 +858,15 @@ impl wallet_server::Wallet for WalletGrpcServer {
.try_into()
.map_err(|e| Status::invalid_argument(format!("Contract ID was not valid :{}", e)))?;

let checkpoint_number = message.checkpoint_number;

let merkle_root = message
.merkle_root
.try_into()
.map_err(|_| Status::invalid_argument("Incorrect merkle root length"))?;

let (tx_id, transaction) = asset_manager
.create_follow_on_asset_checkpoint(contract_id, merkle_root)
.create_follow_on_asset_checkpoint(contract_id, checkpoint_number, merkle_root)
.await
.map_err(|e| Status::internal(e.to_string()))?;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ impl WalletClient for GrpcWalletClient {
&mut self,
contract_id: &FixedHash,
state_root: &StateRoot,
is_initial: bool,
checkpoint_number: u64,
) -> Result<(), DigitalAssetError> {
let inner = self.connection().await?;

if is_initial {
if checkpoint_number == 0 {
let request = CreateInitialAssetCheckpointRequest {
contract_id: contract_id.to_vec(),
merkle_root: state_root.as_bytes().to_vec(),
Expand All @@ -84,6 +84,7 @@ impl WalletClient for GrpcWalletClient {
.map_err(|e| DigitalAssetError::FatalError(format!("Could not create checkpoint:{}", e)))?;
} else {
let request = CreateFollowOnAssetCheckpointRequest {
checkpoint_number,
contract_id: contract_id.to_vec(),
merkle_root: state_root.as_bytes().to_vec(),
};
Expand Down
5 changes: 3 additions & 2 deletions base_layer/core/src/proto/transaction.proto
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,9 @@ message SideChainFeatures {
}

message ContractCheckpoint {
bytes merkle_root = 1;
CommitteeSignatures signatures = 2;
uint64 checkpoint_number = 1;
bytes merkle_root = 2;
CommitteeSignatures signatures = 3;
}

message ContractConstitution {
Expand Down
2 changes: 2 additions & 0 deletions base_layer/core/src/proto/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ impl TryFrom<proto::types::ContractConstitution> for ContractConstitution {
impl From<ContractCheckpoint> for proto::types::ContractCheckpoint {
fn from(value: ContractCheckpoint) -> Self {
Self {
checkpoint_number: value.checkpoint_number,
merkle_root: value.merkle_root.to_vec(),
signatures: Some(value.signatures.into()),
}
Expand All @@ -477,6 +478,7 @@ impl TryFrom<proto::types::ContractCheckpoint> for ContractCheckpoint {
let merkle_root = value.merkle_root.try_into().map_err(|_| "Invalid merkle root")?;
let signatures = value.signatures.map(TryInto::try_into).transpose()?.unwrap_or_default();
Ok(Self {
checkpoint_number: value.checkpoint_number,
merkle_root,
signatures,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ use crate::{
AssetOutputFeatures,
CommitteeDefinitionFeatures,
CommitteeMembers,
CommitteeSignatures,
ContractCheckpoint,
MintNonFungibleFeatures,
OutputType,
Expand Down Expand Up @@ -247,16 +246,9 @@ impl OutputFeatures {
}
}

pub fn for_checkpoint(
contract_id: FixedHash,
merkle_root: FixedHash,
signatures: CommitteeSignatures,
) -> OutputFeatures {
pub fn for_contract_checkpoint(contract_id: FixedHash, checkpoint: ContractCheckpoint) -> OutputFeatures {
let features = SideChainFeatures::builder(contract_id)
.with_contract_checkpoint(ContractCheckpoint {
merkle_root,
signatures,
})
.with_contract_checkpoint(checkpoint)
.finish();

Self {
Expand Down Expand Up @@ -540,11 +532,7 @@ impl Display for OutputFeatures {

#[cfg(test)]
mod test {
use std::{
convert::{TryFrom, TryInto},
io::ErrorKind,
iter,
};
use std::{convert::TryInto, io::ErrorKind, iter};

use tari_common_types::types::Signature;

Expand Down Expand Up @@ -666,6 +654,7 @@ mod test {
activation_window: 0_u64,
}),
checkpoint: Some(ContractCheckpoint {
checkpoint_number: u64::MAX,
merkle_root: FixedHash::zero(),
signatures: vec![Signature::default(); 512].try_into().unwrap(),
}),
Expand Down Expand Up @@ -801,22 +790,17 @@ mod test {
fn test_for_checkpoint() {
let contract_id = FixedHash::hash_bytes("CONTRACT");
let hash = FixedHash::hash_bytes("MERKLE");
let signatures = CommitteeSignatures::try_from(vec![Signature::default()]).unwrap();
assert_eq!(
OutputFeatures {
output_type: OutputType::ContractCheckpoint,
sidechain_features: Some(
SideChainFeatures::builder(contract_id)
.with_contract_checkpoint(ContractCheckpoint {
merkle_root: hash,
signatures: signatures.clone()
})
.finish()
),
..Default::default()
},
OutputFeatures::for_checkpoint(contract_id, hash, signatures)
);
let checkpoint = ContractCheckpoint {
checkpoint_number: 123,
merkle_root: hash,
signatures: vec![Signature::default()].try_into().unwrap(),
};

let features = OutputFeatures::for_contract_checkpoint(contract_id, checkpoint.clone());
let sidechain_features = features.sidechain_features.as_ref().unwrap();
assert_eq!(features.output_type, OutputType::ContractCheckpoint);
assert_eq!(sidechain_features.contract_id, contract_id);
assert_eq!(*sidechain_features.checkpoint.as_ref().unwrap(), checkpoint);
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ use crate::{

#[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq)]
pub struct ContractCheckpoint {
pub checkpoint_number: u64,
pub merkle_root: FixedHash,
pub signatures: CommitteeSignatures,
}

impl ConsensusEncoding for ContractCheckpoint {
fn consensus_encode<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
self.checkpoint_number.consensus_encode(writer)?;
self.merkle_root.consensus_encode(writer)?;
self.signatures.consensus_encode(writer)?;
Ok(())
Expand All @@ -49,6 +51,7 @@ impl ConsensusEncodingSized for ContractCheckpoint {}
impl ConsensusDecoding for ContractCheckpoint {
fn consensus_decode<R: Read>(reader: &mut R) -> Result<Self, Error> {
Ok(Self {
checkpoint_number: u64::consensus_decode(reader)?,
merkle_root: ConsensusDecoding::consensus_decode(reader)?,
signatures: ConsensusDecoding::consensus_decode(reader)?,
})
Expand All @@ -67,6 +70,7 @@ mod tests {
#[test]
fn it_encodes_and_decodes_correctly() {
let subject = ContractCheckpoint {
checkpoint_number: u64::MAX,
merkle_root: FixedHash::zero(),
signatures: vec![Signature::default(); 512].try_into().unwrap(),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ mod tests {
activation_window: 0_u64,
}),
checkpoint: Some(ContractCheckpoint {
checkpoint_number: u64::MAX,
merkle_root: FixedHash::zero(),
signatures: vec![Signature::default(); 512].try_into().unwrap(),
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ use crate::{
SideChainFeatures,
TransactionOutput,
},
validation::ValidationError,
validation::{dan_validators::DanLayerValidationError, ValidationError},
};

/// This validator checks that the provided output corresponds to a valid Contract Acceptance in the DAN layer
Expand Down Expand Up @@ -68,12 +68,12 @@ pub fn validate_acceptance<B: BlockchainBackend>(
}

/// 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> {
fn get_contract_acceptance(
sidechain_feature: &SideChainFeatures,
) -> Result<&ContractAcceptance, DanLayerValidationError> {
match sidechain_feature.acceptance.as_ref() {
Some(acceptance) => Ok(acceptance),
None => Err(ValidationError::DanLayerError(
"Contract acceptance features not found".to_string(),
)),
None => Err(DanLayerValidationError::ContractAcceptanceNotFound),
}
}

Expand All @@ -89,14 +89,14 @@ fn validate_uniqueness<B: BlockchainBackend>(
.filter_map(|feature| feature.acceptance)
.find(|feature| feature.validator_node_public_key == *validator_node_public_key)
{
Some(_) => {
let msg = format!(
"Duplicated contract acceptance for contract_id ({:?}) and validator_node_public_key ({:?})",
contract_id.to_hex(),
validator_node_public_key,
);
Err(ValidationError::DanLayerError(msg))
},
Some(_) => Err(ValidationError::DanLayerError(DanLayerValidationError::DuplicateUtxo {
contract_id,
output_type: OutputType::ContractValidatorAcceptance,
details: format!(
"Validator ({}) sent duplicate acceptance UTXO",
validator_node_public_key.to_hex(),
),
})),
None => Ok(()),
}
}
Expand All @@ -105,17 +105,12 @@ fn validate_uniqueness<B: BlockchainBackend>(
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);
) -> Result<(), DanLayerValidationError> {
let is_validator_in_committee = constitution.validator_committee.contains(validator_node_public_key);
if !is_validator_in_committee {
let msg = format!(
"Validator node public key is not in committee ({:?})",
validator_node_public_key
);
return Err(ValidationError::DanLayerError(msg));
return Err(DanLayerValidationError::ValidatorNotInCommittee {
public_key: validator_node_public_key.to_hex(),
});
}

Ok(())
Expand All @@ -133,11 +128,9 @@ fn validate_acceptance_window<B: BlockchainBackend>(

let window_has_expired = current_height > max_allowed_absolute_height;
if window_has_expired {
let msg = format!(
"Acceptance window has expired for contract_id ({})",
contract_id.to_hex()
);
return Err(ValidationError::DanLayerError(msg));
return Err(ValidationError::DanLayerError(
DanLayerValidationError::AcceptanceWindowHasExpired { contract_id },
));
}

Ok(())
Expand All @@ -151,13 +144,9 @@ pub fn fetch_constitution_height<B: BlockchainBackend>(
// Only one constitution should be stored for a particular contract_id
match utxos.first() {
Some(utxo) => Ok(utxo.mined_height),
None => {
let msg = format!(
"Could not find constitution UTXO for contract_id ({})",
contract_id.to_hex(),
);
Err(ValidationError::DanLayerError(msg))
},
None => Err(ValidationError::DanLayerError(
DanLayerValidationError::ContractConstitutionNotFound { contract_id },
)),
}
}

Expand Down Expand Up @@ -247,7 +236,7 @@ mod test {
let (tx, _) = schema_to_transaction(&schema);

// try to validate the duplicated acceptance transaction and check that we get the error
assert_dan_validator_fail(&blockchain, &tx, "Duplicated contract acceptance");
assert_dan_validator_fail(&blockchain, &tx, "sent duplicate acceptance UTXO");
}

#[test]
Expand Down
Loading

0 comments on commit 7b76141

Please sign in to comment.