From 7b761410cd1dde2c47fd209d4b5e2a77f51aed96 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Fri, 1 Jul 2022 12:29:29 +0200 Subject: [PATCH] feat!: add checkpoint_number to checkpoint with basic base layer validations (#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) --- applications/tari_app_grpc/proto/types.proto | 5 +- applications/tari_app_grpc/proto/wallet.proto | 5 +- .../src/conversions/sidechain_features.rs | 2 + .../src/grpc/wallet_grpc_server.rs | 4 +- .../src/grpc/services/wallet_client.rs | 5 +- base_layer/core/src/proto/transaction.proto | 5 +- base_layer/core/src/proto/transaction.rs | 2 + .../transaction_components/output_features.rs | 46 ++--- .../side_chain/contract_checkpoint.rs | 4 + .../side_chain/sidechain_features.rs | 1 + .../dan_validators/acceptance_validator.rs | 61 +++---- .../dan_validators/amendment_validator.rs | 76 ++++---- .../dan_validators/checkpoint_validator.rs | 165 ++++++++++++++++++ .../dan_validators/constitution_validator.rs | 73 +++++--- .../dan_validators/definition_validator.rs | 48 +++-- .../src/validation/dan_validators/error.rs | 66 +++++++ .../src/validation/dan_validators/helpers.rs | 99 ++++++----- .../core/src/validation/dan_validators/mod.rs | 7 + .../validation/dan_validators/test_helpers.rs | 81 +++++++-- .../update_proposal_acceptance_validator.rs | 98 ++++++----- .../update_proposal_validator.rs | 68 +++++--- base_layer/core/src/validation/error.rs | 3 +- base_layer/wallet/src/assets/asset_manager.rs | 20 ++- .../wallet/src/assets/asset_manager_handle.rs | 2 + .../infrastructure/asset_manager_service.rs | 3 +- .../wallet/src/assets/infrastructure/mod.rs | 1 + .../output_manager_service_tests/service.rs | 16 +- .../core/src/services/checkpoint_manager.rs | 17 +- dan_layer/core/src/services/mocks/mod.rs | 2 +- dan_layer/core/src/services/wallet_client.rs | 2 +- 30 files changed, 677 insertions(+), 310 deletions(-) create mode 100644 base_layer/core/src/validation/dan_validators/checkpoint_validator.rs create mode 100644 base_layer/core/src/validation/dan_validators/error.rs diff --git a/applications/tari_app_grpc/proto/types.proto b/applications/tari_app_grpc/proto/types.proto index 6bbad58347..16071bd3ae 100644 --- a/applications/tari_app_grpc/proto/types.proto +++ b/applications/tari_app_grpc/proto/types.proto @@ -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 { diff --git a/applications/tari_app_grpc/proto/wallet.proto b/applications/tari_app_grpc/proto/wallet.proto index a67008ea45..3fdda3d1c7 100644 --- a/applications/tari_app_grpc/proto/wallet.proto +++ b/applications/tari_app_grpc/proto/wallet.proto @@ -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 { diff --git a/applications/tari_app_grpc/src/conversions/sidechain_features.rs b/applications/tari_app_grpc/src/conversions/sidechain_features.rs index 2166f7735c..abfa8dc32f 100644 --- a/applications/tari_app_grpc/src/conversions/sidechain_features.rs +++ b/applications/tari_app_grpc/src/conversions/sidechain_features.rs @@ -307,6 +307,7 @@ impl TryFrom for ContractConstitution { impl From 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()), } @@ -320,6 +321,7 @@ impl TryFrom 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, }) diff --git a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs index 8bc09b6235..c72622e803 100644 --- a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -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()))?; diff --git a/applications/tari_validator_node/src/grpc/services/wallet_client.rs b/applications/tari_validator_node/src/grpc/services/wallet_client.rs index 208fdc7077..c8a1bf9056 100644 --- a/applications/tari_validator_node/src/grpc/services/wallet_client.rs +++ b/applications/tari_validator_node/src/grpc/services/wallet_client.rs @@ -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(), @@ -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(), }; diff --git a/base_layer/core/src/proto/transaction.proto b/base_layer/core/src/proto/transaction.proto index c7bc405b09..fe30e00632 100644 --- a/base_layer/core/src/proto/transaction.proto +++ b/base_layer/core/src/proto/transaction.proto @@ -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 { diff --git a/base_layer/core/src/proto/transaction.rs b/base_layer/core/src/proto/transaction.rs index 75d02a2d08..0f4115a168 100644 --- a/base_layer/core/src/proto/transaction.rs +++ b/base_layer/core/src/proto/transaction.rs @@ -464,6 +464,7 @@ impl TryFrom for ContractConstitution { impl From 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()), } @@ -477,6 +478,7 @@ impl TryFrom 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, }) diff --git a/base_layer/core/src/transactions/transaction_components/output_features.rs b/base_layer/core/src/transactions/transaction_components/output_features.rs index 165411b49c..b667752fb5 100644 --- a/base_layer/core/src/transactions/transaction_components/output_features.rs +++ b/base_layer/core/src/transactions/transaction_components/output_features.rs @@ -55,7 +55,6 @@ use crate::{ AssetOutputFeatures, CommitteeDefinitionFeatures, CommitteeMembers, - CommitteeSignatures, ContractCheckpoint, MintNonFungibleFeatures, OutputType, @@ -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 { @@ -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; @@ -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(), }), @@ -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] diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/contract_checkpoint.rs b/base_layer/core/src/transactions/transaction_components/side_chain/contract_checkpoint.rs index 18560922b5..b43d00c199 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/contract_checkpoint.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/contract_checkpoint.rs @@ -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(&self, writer: &mut W) -> Result<(), Error> { + self.checkpoint_number.consensus_encode(writer)?; self.merkle_root.consensus_encode(writer)?; self.signatures.consensus_encode(writer)?; Ok(()) @@ -49,6 +51,7 @@ impl ConsensusEncodingSized for ContractCheckpoint {} impl ConsensusDecoding for ContractCheckpoint { fn consensus_decode(reader: &mut R) -> Result { Ok(Self { + checkpoint_number: u64::consensus_decode(reader)?, merkle_root: ConsensusDecoding::consensus_decode(reader)?, signatures: ConsensusDecoding::consensus_decode(reader)?, }) @@ -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(), }; diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_features.rs b/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_features.rs index 1775d12293..feaf964f32 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_features.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_features.rs @@ -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(), }), diff --git a/base_layer/core/src/validation/dan_validators/acceptance_validator.rs b/base_layer/core/src/validation/dan_validators/acceptance_validator.rs index be287a5819..3100758009 100644 --- a/base_layer/core/src/validation/dan_validators/acceptance_validator.rs +++ b/base_layer/core/src/validation/dan_validators/acceptance_validator.rs @@ -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 @@ -68,12 +68,12 @@ pub fn validate_acceptance( } /// 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), } } @@ -89,14 +89,14 @@ fn validate_uniqueness( .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(()), } } @@ -105,17 +105,12 @@ fn validate_uniqueness( 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(()) @@ -133,11 +128,9 @@ fn validate_acceptance_window( 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(()) @@ -151,13 +144,9 @@ pub fn fetch_constitution_height( // 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 }, + )), } } @@ -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] diff --git a/base_layer/core/src/validation/dan_validators/amendment_validator.rs b/base_layer/core/src/validation/dan_validators/amendment_validator.rs index 74e96dc83b..4f6816e675 100644 --- a/base_layer/core/src/validation/dan_validators/amendment_validator.rs +++ b/base_layer/core/src/validation/dan_validators/amendment_validator.rs @@ -21,7 +21,6 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use tari_common_types::types::FixedHash; -use tari_utilities::hex::Hex; use super::helpers::{ fetch_contract_features, @@ -38,7 +37,7 @@ use crate::{ SideChainFeatures, TransactionOutput, }, - validation::ValidationError, + validation::{dan_validators::DanLayerValidationError, ValidationError}, }; pub fn validate_amendment( @@ -60,12 +59,14 @@ pub fn validate_amendment( Ok(()) } -fn get_contract_amendment(sidechain_feature: &SideChainFeatures) -> Result<&ContractAmendment, ValidationError> { +fn get_contract_amendment( + sidechain_feature: &SideChainFeatures, +) -> Result<&ContractAmendment, DanLayerValidationError> { match sidechain_feature.amendment.as_ref() { Some(amendment) => Ok(amendment), - None => Err(ValidationError::DanLayerError( - "Contract amendment features not found".to_string(), - )), + None => Err(DanLayerValidationError::SideChainFeaturesDataNotProvided { + field_name: "amendment", + }), } } @@ -80,14 +81,11 @@ fn validate_uniqueness( .filter_map(|feature| feature.amendment) .find(|amendment| amendment.proposal_id == proposal_id) { - Some(_) => { - let msg = format!( - "Duplicated amendment for contract_id ({:?}) and proposal_id ({:?})", - contract_id.to_hex(), - proposal_id, - ); - Err(ValidationError::DanLayerError(msg)) - }, + Some(_) => Err(ValidationError::DanLayerError(DanLayerValidationError::DuplicateUtxo { + contract_id, + output_type: OutputType::ContractAmendment, + details: format!("proposal_id = {}", proposal_id), + })), None => Ok(()), } } @@ -95,11 +93,9 @@ fn validate_uniqueness( fn validate_updated_constiution( amendment: &ContractAmendment, proposal: &ContractUpdateProposal, -) -> Result<(), ValidationError> { +) -> Result<(), DanLayerValidationError> { if amendment.updated_constitution != proposal.updated_constitution { - return Err(ValidationError::DanLayerError( - "The updated_constitution of the amendment does not match the one in the update proposal".to_string(), - )); + return Err(DanLayerValidationError::UpdatedConstitutionAmendmentMismatch); } Ok(()) @@ -110,18 +106,26 @@ mod test { use std::convert::TryInto; use tari_common_types::types::PublicKey; - - use crate::validation::dan_validators::test_helpers::{ - assert_dan_validator_fail, - assert_dan_validator_success, - create_block, - create_contract_amendment_schema, - create_contract_constitution, - init_test_blockchain, - publish_constitution, - publish_definition, - publish_update_proposal, - schema_to_transaction, + use tari_test_utils::unpack_enum; + + use crate::{ + transactions::transaction_components::OutputType, + validation::dan_validators::{ + test_helpers::{ + assert_dan_validator_err, + assert_dan_validator_fail, + assert_dan_validator_success, + create_block, + create_contract_amendment_schema, + create_contract_constitution, + init_test_blockchain, + publish_constitution, + publish_definition, + publish_update_proposal, + schema_to_transaction, + }, + DanLayerValidationError, + }, }; #[test] @@ -217,7 +221,17 @@ mod test { let (tx, _) = schema_to_transaction(&schema); // try to validate the duplicated amendment transaction and check that we get the error - assert_dan_validator_fail(&blockchain, &tx, "Duplicated amendment"); + let err = assert_dan_validator_err(&blockchain, &tx); + let expected_contract_id = contract_id; + unpack_enum!( + DanLayerValidationError::DuplicateUtxo { + output_type, + contract_id, + .. + } = err + ); + assert_eq!(output_type, OutputType::ContractAmendment); + assert_eq!(contract_id, expected_contract_id); } #[test] diff --git a/base_layer/core/src/validation/dan_validators/checkpoint_validator.rs b/base_layer/core/src/validation/dan_validators/checkpoint_validator.rs new file mode 100644 index 0000000000..ff1c386ca4 --- /dev/null +++ b/base_layer/core/src/validation/dan_validators/checkpoint_validator.rs @@ -0,0 +1,165 @@ +// Copyright 2022, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 crate::{ + chain_storage::{BlockchainBackend, BlockchainDatabase}, + transactions::transaction_components::{ContractCheckpoint, OutputType, SideChainFeatures, TransactionOutput}, + validation::{ + dan_validators::{ + constitution_validator::validate_definition_existence, + helpers::fetch_current_contract_checkpoint, + DanLayerValidationError, + }, + ValidationError, + }, +}; + +pub fn validate_contract_checkpoint( + db: &BlockchainDatabase, + output: &TransactionOutput, +) -> Result<(), ValidationError> { + let sidechain_features = get_sidechain_features(output)?; + let contract_id = sidechain_features.contract_id; + validate_definition_existence(db, contract_id)?; + + let prev_cp = fetch_current_contract_checkpoint(db, contract_id)?; + validate_checkpoint_number(prev_cp.as_ref(), sidechain_features)?; + + 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 { + contract_id: sidechain_features.contract_id, + output_type: OutputType::ContractCheckpoint, + })?; + + let expected_number = prev_checkpoint.map(|cp| cp.checkpoint_number + 1).unwrap_or(0); + if checkpoint.checkpoint_number == expected_number { + Ok(()) + } else { + Err(DanLayerValidationError::CheckpointNonSequentialNumber { + got: checkpoint.checkpoint_number, + expected: expected_number, + }) + } +} + +#[cfg(test)] +mod test { + use crate::validation::dan_validators::{ + test_helpers::{ + assert_dan_validator_err, + assert_dan_validator_success, + create_contract_checkpoint, + create_contract_checkpoint_schema, + init_test_blockchain, + publish_checkpoint, + publish_contract, + schema_to_transaction, + }, + DanLayerValidationError, + }; + + #[test] + fn it_allows_initial_checkpoint_output_with_zero_checkpoint_number() { + // initialise a blockchain with enough funds to spend at contract transactions + let (mut blockchain, utxos) = init_test_blockchain(); + + // Publish a new contract + let contract_id = publish_contract(&mut blockchain, &utxos, vec![]); + + // Create checkpoint 0 with no prior checkpoints + let checkpoint = create_contract_checkpoint(0); + let schema = create_contract_checkpoint_schema(contract_id, utxos[2].clone(), checkpoint); + let (tx, _) = schema_to_transaction(&schema); + + assert_dan_validator_success(&blockchain, &tx); + } + + #[test] + fn it_allows_checkpoint_output_with_correct_sequential_checkpoint_number() { + // initialise a blockchain with enough funds to spend at contract transactions + let (mut blockchain, utxos) = init_test_blockchain(); + + // Publish a new contract + let contract_id = publish_contract(&mut blockchain, &utxos, vec![]); + + publish_checkpoint(&mut blockchain, "cp0", utxos[2].clone(), contract_id, 0); + publish_checkpoint(&mut blockchain, "cp1", utxos[3].clone(), contract_id, 1); + // Create checkpoint 0 with no prior checkpoints + let checkpoint = create_contract_checkpoint(2); + let schema = create_contract_checkpoint_schema(contract_id, utxos[4].clone(), checkpoint); + let (tx, _) = schema_to_transaction(&schema); + + assert_dan_validator_success(&blockchain, &tx); + } + + #[test] + fn it_rejects_initial_checkpoint_output_with_non_zero_checkpoint_number() { + // initialise a blockchain with enough funds to spend at contract transactions + let (mut blockchain, utxos) = init_test_blockchain(); + + // Publish a new contract + let contract_id = publish_contract(&mut blockchain, &utxos, vec![]); + + // Create checkpoint 1 with no prior checkpoints + let checkpoint = create_contract_checkpoint(1); + let schema = create_contract_checkpoint_schema(contract_id, utxos[2].clone(), checkpoint); + let (tx, _) = schema_to_transaction(&schema); + + let err = assert_dan_validator_err(&blockchain, &tx); + assert!(matches!(err, DanLayerValidationError::CheckpointNonSequentialNumber { + got: 1, + expected: 0 + })) + } + + #[test] + fn it_rejects_checkpoint_output_with_non_sequential_checkpoint_number() { + // initialise a blockchain with enough funds to spend at contract transactions + let (mut blockchain, utxos) = init_test_blockchain(); + + // Publish a new contract + let contract_id = publish_contract(&mut blockchain, &utxos, vec![]); + + publish_checkpoint(&mut blockchain, "cp0", utxos[2].clone(), contract_id, 0); + publish_checkpoint(&mut blockchain, "cp1", utxos[3].clone(), contract_id, 1); + // Create checkpoint 0 with no prior checkpoints + let checkpoint = create_contract_checkpoint(3); + let schema = create_contract_checkpoint_schema(contract_id, utxos[2].clone(), checkpoint); + let (tx, _) = schema_to_transaction(&schema); + + let err = assert_dan_validator_err(&blockchain, &tx); + assert!(matches!(err, DanLayerValidationError::CheckpointNonSequentialNumber { + got: 3, + expected: 2 + })) + } +} diff --git a/base_layer/core/src/validation/dan_validators/constitution_validator.rs b/base_layer/core/src/validation/dan_validators/constitution_validator.rs index 9bc293f626..dd31f813b5 100644 --- a/base_layer/core/src/validation/dan_validators/constitution_validator.rs +++ b/base_layer/core/src/validation/dan_validators/constitution_validator.rs @@ -21,13 +21,12 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use tari_common_types::types::FixedHash; -use tari_utilities::hex::Hex; use super::helpers::{fetch_contract_features, get_sidechain_features, validate_output_type}; use crate::{ chain_storage::{BlockchainBackend, BlockchainDatabase}, transactions::transaction_components::{OutputType, TransactionOutput}, - validation::ValidationError, + validation::{dan_validators::DanLayerValidationError, ValidationError}, }; pub fn validate_constitution( @@ -45,17 +44,15 @@ pub fn validate_constitution( Ok(()) } -fn validate_definition_existence( +pub fn validate_definition_existence( db: &BlockchainDatabase, contract_id: FixedHash, ) -> Result<(), ValidationError> { let features = fetch_contract_features(db, contract_id, OutputType::ContractDefinition)?; if features.is_empty() { - let msg = format!( - "Contract definition not found for contract_id ({:?})", - contract_id.to_hex() - ); - return Err(ValidationError::DanLayerError(msg)); + return Err(ValidationError::DanLayerError( + DanLayerValidationError::ContractDefnintionNotFound { contract_id }, + )); } Ok(()) @@ -68,11 +65,11 @@ fn validate_uniqueness( let features = fetch_contract_features(db, contract_id, OutputType::ContractConstitution)?; let is_duplicated = !features.is_empty(); if is_duplicated { - let msg = format!( - "Duplicated contract constitution for contract_id ({:?})", - contract_id.to_hex() - ); - return Err(ValidationError::DanLayerError(msg)); + return Err(ValidationError::DanLayerError(DanLayerValidationError::DuplicateUtxo { + contract_id, + output_type: OutputType::ContractConstitution, + details: String::new(), + })); } Ok(()) @@ -81,16 +78,24 @@ fn validate_uniqueness( #[cfg(test)] mod test { use tari_common_types::types::FixedHash; - - use crate::validation::dan_validators::test_helpers::{ - assert_dan_validator_fail, - assert_dan_validator_success, - create_contract_constitution, - create_contract_constitution_schema, - init_test_blockchain, - publish_constitution, - publish_definition, - schema_to_transaction, + use tari_test_utils::unpack_enum; + + use crate::{ + transactions::transaction_components::OutputType, + validation::dan_validators::{ + test_helpers::{ + assert_dan_validator_err, + assert_dan_validator_fail, + assert_dan_validator_success, + create_contract_constitution, + create_contract_constitution_schema, + init_test_blockchain, + publish_constitution, + publish_definition, + schema_to_transaction, + }, + DanLayerValidationError, + }, }; #[test] @@ -130,17 +135,31 @@ mod test { let (mut blockchain, change) = init_test_blockchain(); // publish the contract definition into a block - let contract_id = publish_definition(&mut blockchain, change[0].clone()); + let expected_contract_id = publish_definition(&mut blockchain, change[0].clone()); // publish the contract constitution into a block let constitution = create_contract_constitution(); - publish_constitution(&mut blockchain, change[1].clone(), contract_id, constitution.clone()); + publish_constitution( + &mut blockchain, + change[1].clone(), + expected_contract_id, + constitution.clone(), + ); // construct a transaction for the duplicated contract constitution - let schema = create_contract_constitution_schema(contract_id, change[2].clone(), constitution); + let schema = create_contract_constitution_schema(expected_contract_id, change[2].clone(), constitution); let (tx, _) = schema_to_transaction(&schema); // try to validate the duplicated constitution transaction and check that we get the error - assert_dan_validator_fail(&blockchain, &tx, "Duplicated contract constitution"); + let err = assert_dan_validator_err(&blockchain, &tx); + unpack_enum!( + DanLayerValidationError::DuplicateUtxo { + output_type, + contract_id, + .. + } = err + ); + assert_eq!(output_type, OutputType::ContractConstitution); + assert_eq!(contract_id, expected_contract_id); } } diff --git a/base_layer/core/src/validation/dan_validators/definition_validator.rs b/base_layer/core/src/validation/dan_validators/definition_validator.rs index 4fa316e6dc..d47fa9115d 100644 --- a/base_layer/core/src/validation/dan_validators/definition_validator.rs +++ b/base_layer/core/src/validation/dan_validators/definition_validator.rs @@ -21,13 +21,12 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use tari_common_types::types::FixedHash; -use tari_utilities::hex::Hex; use super::helpers::{fetch_contract_features, get_sidechain_features, validate_output_type}; use crate::{ chain_storage::{BlockchainBackend, BlockchainDatabase}, transactions::transaction_components::{OutputType, TransactionOutput}, - validation::ValidationError, + validation::{dan_validators::DanLayerValidationError, ValidationError}, }; pub fn validate_definition( @@ -51,11 +50,11 @@ fn validate_uniqueness( let features = fetch_contract_features(db, contract_id, OutputType::ContractDefinition)?; let is_duplicated = !features.is_empty(); if is_duplicated { - let msg = format!( - "Duplicated contract definition for contract_id ({:?})", - contract_id.to_hex() - ); - return Err(ValidationError::DanLayerError(msg)); + return Err(ValidationError::DanLayerError(DanLayerValidationError::DuplicateUtxo { + contract_id, + output_type: OutputType::ContractDefinition, + details: String::new(), + })); } Ok(()) @@ -63,13 +62,21 @@ fn validate_uniqueness( #[cfg(test)] mod test { - use crate::validation::dan_validators::test_helpers::{ - assert_dan_validator_fail, - assert_dan_validator_success, - create_contract_definition_schema, - init_test_blockchain, - publish_definition, - schema_to_transaction, + use tari_test_utils::unpack_enum; + + use crate::{ + transactions::transaction_components::OutputType, + validation::dan_validators::{ + test_helpers::{ + assert_dan_validator_err, + assert_dan_validator_success, + create_contract_definition_schema, + init_test_blockchain, + publish_definition, + schema_to_transaction, + }, + DanLayerValidationError, + }, }; #[test] @@ -90,13 +97,22 @@ mod test { let (mut blockchain, change) = init_test_blockchain(); // publish the contract definition into a block - let _contract_id = publish_definition(&mut blockchain, change[0].clone()); + let expected_contract_id = publish_definition(&mut blockchain, change[0].clone()); // construct a transaction for the duplicated contract definition let (_, schema) = create_contract_definition_schema(change[1].clone()); let (tx, _) = schema_to_transaction(&schema); // try to validate the duplicated definition transaction and check that we get the error - assert_dan_validator_fail(&blockchain, &tx, "Duplicated contract definition"); + let err = assert_dan_validator_err(&blockchain, &tx); + unpack_enum!( + DanLayerValidationError::DuplicateUtxo { + output_type, + contract_id, + .. + } = err + ); + assert_eq!(output_type, OutputType::ContractDefinition); + assert_eq!(contract_id, expected_contract_id); } } diff --git a/base_layer/core/src/validation/dan_validators/error.rs b/base_layer/core/src/validation/dan_validators/error.rs new file mode 100644 index 0000000000..cea5ab7d82 --- /dev/null +++ b/base_layer/core/src/validation/dan_validators/error.rs @@ -0,0 +1,66 @@ +// Copyright 2022. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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::FixedHash; + +use crate::transactions::transaction_components::OutputType; + +#[derive(Debug, thiserror::Error)] +pub enum DanLayerValidationError { + #[error("{output_type} output with contract id {contract_id} is missing required feature data")] + MissingContractData { + contract_id: FixedHash, + output_type: OutputType, + }, + #[error("Data inconsistency: {details}")] + DataInconsistency { details: String }, + #[error("Contract constitution not found for contract_id {contract_id}")] + ContractConstitutionNotFound { contract_id: FixedHash }, + #[error("Sidechain features not provided")] + SidechainFeaturesNotProvided, + #[error("Contract update proposal not found for contract_id {contract_id} and proposal_id {proposal_id}")] + ContractUpdateProposalNotFound { contract_id: FixedHash, proposal_id: u64 }, + #[error("Invalid output type: expected {expected} but got {got}")] + UnexpectedOutputType { got: OutputType, expected: OutputType }, + #[error("Contract acceptance features not found")] + ContractAcceptanceNotFound, + #[error("Duplicate {output_type} contract UTXO: {details}")] + DuplicateUtxo { + contract_id: FixedHash, + output_type: OutputType, + details: String, + }, + #[error("Validator node public key is not in committee ({public_key})")] + ValidatorNotInCommittee { public_key: String }, + #[error("Contract definition not found for contract_id ({contract_id})")] + ContractDefnintionNotFound { contract_id: FixedHash }, + #[error("Sidechain features data for {field_name} not provided")] + SideChainFeaturesDataNotProvided { field_name: &'static str }, + #[error("The updated_constitution of the amendment does not match the one in the update proposal")] + UpdatedConstitutionAmendmentMismatch, + #[error("Acceptance window has expired for contract_id ({contract_id})")] + AcceptanceWindowHasExpired { contract_id: FixedHash }, + #[error("Proposal acceptance window has expired for contract_id ({contract_id}) and proposal_id ({proposal_id})")] + ProposalAcceptanceWindowHasExpired { contract_id: FixedHash, proposal_id: u64 }, + #[error("Checkpoint has non-sequential number. Got: {got}, expected: {expected}")] + CheckpointNonSequentialNumber { got: u64, expected: u64 }, +} diff --git a/base_layer/core/src/validation/dan_validators/helpers.rs b/base_layer/core/src/validation/dan_validators/helpers.rs index 16df978a6a..bb64c754b6 100644 --- a/base_layer/core/src/validation/dan_validators/helpers.rs +++ b/base_layer/core/src/validation/dan_validators/helpers.rs @@ -21,46 +21,51 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use tari_common_types::types::FixedHash; -use tari_utilities::hex::Hex; use crate::{ chain_storage::{BlockchainBackend, BlockchainDatabase, UtxoMinedInfo}, transactions::transaction_components::{ + ContractCheckpoint, ContractConstitution, ContractUpdateProposal, OutputType, SideChainFeatures, TransactionOutput, }, - validation::ValidationError, + validation::{dan_validators::DanLayerValidationError, ValidationError}, }; pub fn validate_output_type( output: &TransactionOutput, expected_output_type: OutputType, -) -> Result<(), ValidationError> { +) -> Result<(), DanLayerValidationError> { let output_type = output.features.output_type; if output_type != expected_output_type { - let msg = format!( - "Invalid output type: expected {:?} but got {:?}", - expected_output_type, output_type - ); - return Err(ValidationError::DanLayerError(msg)); + return Err(DanLayerValidationError::UnexpectedOutputType { + got: output_type, + expected: expected_output_type, + }); } Ok(()) } +pub fn get_sidechain_features(output: &TransactionOutput) -> Result<&SideChainFeatures, DanLayerValidationError> { + match output.features.sidechain_features.as_ref() { + Some(features) => Ok(features), + None => Err(DanLayerValidationError::SidechainFeaturesNotProvided), + } +} + pub fn fetch_contract_features( db: &BlockchainDatabase, contract_id: FixedHash, output_type: OutputType, ) -> Result, ValidationError> { let features = fetch_contract_utxos(db, contract_id, output_type)? - .iter() - .filter_map(|utxo| utxo.output.as_transaction_output()) - .filter_map(|output| output.features.sidechain_features.as_ref()) - .cloned() + .into_iter() + .filter_map(|utxo| utxo.output.into_unpruned_output()) + .filter_map(|output| output.features.sidechain_features) .collect(); Ok(features) @@ -71,10 +76,7 @@ pub fn fetch_contract_utxos( contract_id: FixedHash, output_type: OutputType, ) -> Result, ValidationError> { - let utxos = db - .fetch_contract_outputs_by_contract_id_and_type(contract_id, output_type) - .map_err(|err| ValidationError::DanLayerError(format!("Could not search outputs: {}", err)))?; - + let utxos = db.fetch_contract_outputs_by_contract_id_and_type(contract_id, output_type)?; Ok(utxos) } @@ -82,26 +84,19 @@ pub fn fetch_contract_constitution( db: &BlockchainDatabase, contract_id: FixedHash, ) -> Result { - let features = fetch_contract_features(db, contract_id, OutputType::ContractConstitution)?; - if features.is_empty() { - return Err(ValidationError::DanLayerError(format!( - "Contract constitution not found for contract_id {}", - contract_id.to_hex() - ))); - } - - let feature = &features[0]; - - let constitution = match feature.constitution.as_ref() { - Some(value) => value, - None => { - return Err(ValidationError::DanLayerError( - "Contract constitution data not found in the output features".to_string(), - )) - }, - }; + let mut features = fetch_contract_features(db, contract_id, OutputType::ContractConstitution)?; + let feature = features + .pop() + .ok_or(DanLayerValidationError::ContractConstitutionNotFound { contract_id })?; - Ok(constitution.clone()) + match feature.constitution { + Some(value) => Ok(value), + None => Err(ValidationError::DanLayerError( + DanLayerValidationError::DataInconsistency { + details: "Contract constitution data not found in the output features".to_string(), + }, + )), + } } pub fn fetch_contract_update_proposal( @@ -116,19 +111,29 @@ pub fn fetch_contract_update_proposal( .find(|proposal| proposal.proposal_id == proposal_id) { Some(proposal) => Ok(proposal), - None => Err(ValidationError::DanLayerError(format!( - "Contract update proposal not found for contract_id {} and proposal_id {}", - contract_id.to_hex(), - proposal_id - ))), - } -} - -pub fn get_sidechain_features(output: &TransactionOutput) -> Result<&SideChainFeatures, ValidationError> { - match output.features.sidechain_features.as_ref() { - Some(features) => Ok(features), None => Err(ValidationError::DanLayerError( - "Sidechain features not found".to_string(), + DanLayerValidationError::ContractUpdateProposalNotFound { + contract_id, + proposal_id, + }, )), } } + +pub fn fetch_current_contract_checkpoint( + db: &BlockchainDatabase, + contract_id: FixedHash, +) -> Result, ValidationError> { + let mut features = fetch_contract_features(db, contract_id, OutputType::ContractCheckpoint)?; + let feature = match features.pop() { + Some(feat) => feat, + None => return Ok(None), + }; + + let checkpoint = feature + .checkpoint + .ok_or_else(|| DanLayerValidationError::DataInconsistency { + details: "DB output marked as checkpoint did not contain checkpoint data".to_string(), + })?; + Ok(Some(checkpoint)) +} diff --git a/base_layer/core/src/validation/dan_validators/mod.rs b/base_layer/core/src/validation/dan_validators/mod.rs index 7cc36845fe..fbaed04b67 100644 --- a/base_layer/core/src/validation/dan_validators/mod.rs +++ b/base_layer/core/src/validation/dan_validators/mod.rs @@ -44,8 +44,14 @@ use update_proposal_acceptance_validator::validate_update_proposal_acceptance; mod amendment_validator; use amendment_validator::validate_amendment; +mod checkpoint_validator; +use checkpoint_validator::validate_contract_checkpoint; + mod helpers; +mod error; +pub use error::DanLayerValidationError; + #[cfg(test)] mod test_helpers; @@ -68,6 +74,7 @@ impl MempoolTransactionValidation for TxDanLayerValidator< OutputType::ContractDefinition => validate_definition(&self.db, output)?, OutputType::ContractConstitution => validate_constitution(&self.db, output)?, OutputType::ContractValidatorAcceptance => validate_acceptance(&self.db, output)?, + OutputType::ContractCheckpoint => validate_contract_checkpoint(&self.db, output)?, OutputType::ContractConstitutionProposal => validate_update_proposal(&self.db, output)?, OutputType::ContractConstitutionChangeAcceptance => { validate_update_proposal_acceptance(&self.db, output)? 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 3f6bc8751f..2a0465bfaf 100644 --- a/base_layer/core/src/validation/dan_validators/test_helpers.rs +++ b/base_layer/core/src/validation/dan_validators/test_helpers.rs @@ -36,10 +36,12 @@ use crate::{ transaction_components::{ vec_into_fixed_string, CheckpointParameters, + CommitteeSignatures, ConstitutionChangeFlags, ConstitutionChangeRules, ContractAcceptanceRequirements, ContractAmendment, + ContractCheckpoint, ContractConstitution, ContractDefinition, ContractSpecification, @@ -52,7 +54,7 @@ use crate::{ }, }, txn_schema, - validation::{MempoolTransactionValidation, ValidationError}, + validation::{dan_validators::DanLayerValidationError, MempoolTransactionValidation, ValidationError}, }; pub fn init_test_blockchain() -> (TestBlockchain, Vec) { @@ -68,6 +70,21 @@ pub fn init_test_blockchain() -> (TestBlockchain, Vec) { (blockchain, change_outputs) } +pub fn publish_contract( + blockchain: &mut TestBlockchain, + inputs: &[UnblindedOutput], + committee: Vec, +) -> FixedHash { + // publish the contract definition into a block + let contract_id = publish_definition(blockchain, inputs[0].clone()); + + // construct a transaction for the duplicated contract definition + let mut constitution = create_contract_constitution(); + constitution.validator_committee = committee.try_into().unwrap(); + publish_constitution(blockchain, inputs[1].clone(), contract_id, constitution); + contract_id +} + pub fn publish_definition(blockchain: &mut TestBlockchain, change: UnblindedOutput) -> FixedHash { let (contract_id, schema) = create_contract_definition_schema(change); create_block(blockchain, "definition", schema); @@ -85,6 +102,19 @@ pub fn publish_constitution( create_block(blockchain, "constitution", schema); } +pub fn publish_checkpoint( + blockchain: &mut TestBlockchain, + block_name: &'static str, + input: UnblindedOutput, + contract_id: FixedHash, + checkpoint_number: u64, +) { + let checkpoint = create_contract_checkpoint(checkpoint_number); + let schema = create_contract_checkpoint_schema(contract_id, input, checkpoint); + // TODO: need to change block spec to accept dynamic strings for name + create_block(blockchain, block_name, schema); +} + pub fn publish_update_proposal( blockchain: &mut TestBlockchain, change: UnblindedOutput, @@ -168,6 +198,23 @@ pub fn create_contract_constitution() -> ContractConstitution { } } +pub fn create_contract_checkpoint(checkpoint_number: u64) -> ContractCheckpoint { + ContractCheckpoint { + checkpoint_number, + merkle_root: FixedHash::zero(), + signatures: CommitteeSignatures::default(), + } +} + +pub fn create_contract_checkpoint_schema( + contract_id: FixedHash, + input: UnblindedOutput, + checkpoint: ContractCheckpoint, +) -> TransactionSchema { + let features = OutputFeatures::for_contract_checkpoint(contract_id, checkpoint); + txn_schema!(from: vec![input], to: vec![0.into()], fee: 5.into(), lock: 0, features: features) +} + pub fn create_contract_acceptance_schema( contract_id: FixedHash, input: UnblindedOutput, @@ -235,23 +282,29 @@ pub fn create_contract_amendment_schema( txn_schema!(from: vec![input], to: vec![0.into()], fee: 5.into(), lock: 0, features: amendment_features) } -pub fn assert_dan_validator_fail(blockchain: &TestBlockchain, transaction: &Transaction, expected_message: &str) { +fn perform_validation(blockchain: &TestBlockchain, transaction: &Transaction) -> Result<(), DanLayerValidationError> { let validator = TxDanLayerValidator::new(blockchain.db().clone()); - let err = validator.validate(transaction).unwrap_err(); - match err { - ValidationError::DanLayerError(message) => { - assert!( - message.contains(expected_message), - "Message \"{}\" does not contain \"{}\"", - message, - expected_message - ) - }, + match validator.validate(transaction) { + Ok(()) => Ok(()), + Err(ValidationError::DanLayerError(err)) => Err(err), _ => panic!("Expected a consensus error"), } } +pub fn assert_dan_validator_err(blockchain: &TestBlockchain, transaction: &Transaction) -> DanLayerValidationError { + perform_validation(blockchain, transaction).unwrap_err() +} + +pub fn assert_dan_validator_fail(blockchain: &TestBlockchain, transaction: &Transaction, expected_message: &str) { + let err = assert_dan_validator_err(blockchain, transaction); + assert!( + err.to_string().contains(expected_message), + "Message \"{}\" does not contain \"{}\"", + err, + expected_message + ); +} + pub fn assert_dan_validator_success(blockchain: &TestBlockchain, transaction: &Transaction) { - let validator = TxDanLayerValidator::new(blockchain.db().clone()); - validator.validate(transaction).unwrap(); + perform_validation(blockchain, transaction).unwrap() } diff --git a/base_layer/core/src/validation/dan_validators/update_proposal_acceptance_validator.rs b/base_layer/core/src/validation/dan_validators/update_proposal_acceptance_validator.rs index ad0ed0a409..621365d6f5 100644 --- a/base_layer/core/src/validation/dan_validators/update_proposal_acceptance_validator.rs +++ b/base_layer/core/src/validation/dan_validators/update_proposal_acceptance_validator.rs @@ -39,7 +39,7 @@ use crate::{ SideChainFeatures, TransactionOutput, }, - validation::ValidationError, + validation::{dan_validators::DanLayerValidationError, ValidationError}, }; pub fn validate_update_proposal_acceptance( @@ -71,12 +71,12 @@ pub fn validate_update_proposal_acceptance( /// Retrieves a contract update proposal acceptance object from the sidechain features, returns an error if not present fn get_contract_update_proposal_acceptance( sidechain_feature: &SideChainFeatures, -) -> Result<&ContractUpdateProposalAcceptance, ValidationError> { +) -> Result<&ContractUpdateProposalAcceptance, DanLayerValidationError> { match sidechain_feature.update_proposal_acceptance.as_ref() { Some(acceptance) => Ok(acceptance), - None => Err(ValidationError::DanLayerError( - "Contract update proposal acceptance features not found".to_string(), - )), + None => Err(DanLayerValidationError::SideChainFeaturesDataNotProvided { + field_name: "update_proposal_acceptance", + }), } } @@ -94,16 +94,11 @@ fn validate_uniqueness( .find(|feature| { feature.validator_node_public_key == *validator_node_public_key && feature.proposal_id == proposal_id }) { - Some(_) => { - let msg = format!( - "Duplicated contract update proposal acceptance for contract_id ({:?}), proposal_id ({}) and \ - validator_node_public_key ({:?})", - contract_id.to_hex(), - proposal_id, - validator_node_public_key, - ); - Err(ValidationError::DanLayerError(msg)) - }, + Some(_) => Err(ValidationError::DanLayerError(DanLayerValidationError::DuplicateUtxo { + contract_id, + output_type: OutputType::ContractConstitutionChangeAcceptance, + details: format!("validator_node_public_key = {}", validator_node_public_key.to_hex()), + })), None => Ok(()), } } @@ -112,18 +107,16 @@ fn validate_uniqueness( fn validate_public_key( proposal: &ContractUpdateProposal, validator_node_public_key: &PublicKey, -) -> Result<(), ValidationError> { +) -> Result<(), DanLayerValidationError> { let is_validator_in_committee = proposal .updated_constitution .validator_committee .members() .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(()) @@ -144,12 +137,12 @@ fn validate_acceptance_window( let window_has_expired = current_height > max_allowed_absolute_height; if window_has_expired { - let msg = format!( - "Proposal acceptance window has expired for contract_id ({}) and proposal_id ({})", - contract_id.to_hex(), - proposal.proposal_id - ); - return Err(ValidationError::DanLayerError(msg)); + return Err(ValidationError::DanLayerError( + DanLayerValidationError::ProposalAcceptanceWindowHasExpired { + contract_id, + proposal_id: proposal.proposal_id, + }, + )); } Ok(()) @@ -180,11 +173,12 @@ pub fn fetch_proposal_height( match proposal_utxo { Some(utxo) => Ok(utxo.mined_height), - None => Err(ValidationError::DanLayerError(format!( - "Contract update proposal not found for contract_id {} and proposal_id {}", - contract_id.to_hex(), - proposal_id - ))), + None => Err(ValidationError::DanLayerError( + DanLayerValidationError::ContractUpdateProposalNotFound { + contract_id, + proposal_id, + }, + )), } } @@ -193,21 +187,27 @@ mod test { use std::convert::TryInto; use tari_common_types::types::PublicKey; + use tari_test_utils::unpack_enum; use tari_utilities::hex::Hex; use crate::{ + transactions::transaction_components::OutputType, txn_schema, - validation::dan_validators::test_helpers::{ - assert_dan_validator_fail, - assert_dan_validator_success, - create_block, - create_contract_constitution, - create_contract_update_proposal_acceptance_schema, - init_test_blockchain, - publish_constitution, - publish_definition, - publish_update_proposal, - schema_to_transaction, + validation::dan_validators::{ + test_helpers::{ + assert_dan_validator_err, + assert_dan_validator_fail, + assert_dan_validator_success, + create_block, + create_contract_constitution, + create_contract_update_proposal_acceptance_schema, + init_test_blockchain, + publish_constitution, + publish_definition, + publish_update_proposal, + schema_to_transaction, + }, + DanLayerValidationError, }, }; @@ -326,7 +326,17 @@ mod test { let (tx, _) = schema_to_transaction(&schema); // try to validate the (duplicated) proposal acceptance transaction and check that we get the error - assert_dan_validator_fail(&blockchain, &tx, "Duplicated contract update proposal acceptance"); + let err = assert_dan_validator_err(&blockchain, &tx); + let expected_contract_id = contract_id; + unpack_enum!( + DanLayerValidationError::DuplicateUtxo { + output_type, + contract_id, + .. + } = err + ); + assert_eq!(output_type, OutputType::ContractConstitutionChangeAcceptance); + assert_eq!(contract_id, expected_contract_id); } #[test] diff --git a/base_layer/core/src/validation/dan_validators/update_proposal_validator.rs b/base_layer/core/src/validation/dan_validators/update_proposal_validator.rs index b5b9bcf209..a731127e8d 100644 --- a/base_layer/core/src/validation/dan_validators/update_proposal_validator.rs +++ b/base_layer/core/src/validation/dan_validators/update_proposal_validator.rs @@ -21,7 +21,6 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use tari_common_types::types::FixedHash; -use tari_utilities::hex::Hex; use super::helpers::{ fetch_contract_constitution, @@ -32,7 +31,7 @@ use super::helpers::{ use crate::{ chain_storage::{BlockchainBackend, BlockchainDatabase}, transactions::transaction_components::{ContractUpdateProposal, OutputType, SideChainFeatures, TransactionOutput}, - validation::ValidationError, + validation::{dan_validators::DanLayerValidationError, ValidationError}, }; pub fn validate_update_proposal( @@ -54,12 +53,14 @@ pub fn validate_update_proposal( Ok(()) } -fn get_update_proposal(sidechain_feature: &SideChainFeatures) -> Result<&ContractUpdateProposal, ValidationError> { +fn get_update_proposal( + sidechain_feature: &SideChainFeatures, +) -> Result<&ContractUpdateProposal, DanLayerValidationError> { match sidechain_feature.update_proposal.as_ref() { Some(proposal) => Ok(proposal), - None => Err(ValidationError::DanLayerError( - "Contract update proposal features not found".to_string(), - )), + None => Err(DanLayerValidationError::SideChainFeaturesDataNotProvided { + field_name: "update_proposal", + }), } } @@ -74,14 +75,11 @@ fn validate_uniqueness( .filter_map(|feature| feature.update_proposal) .find(|proposal| proposal.proposal_id == proposal_id) { - Some(_) => { - let msg = format!( - "Duplicated contract update proposal for contract_id ({:?}) and proposal_id ({:?})", - contract_id.to_hex(), - proposal_id, - ); - Err(ValidationError::DanLayerError(msg)) - }, + Some(_) => Err(ValidationError::DanLayerError(DanLayerValidationError::DuplicateUtxo { + contract_id, + output_type: OutputType::ContractConstitutionProposal, + details: format!("Proposal ID is {}", proposal_id), + })), None => Ok(()), } } @@ -91,17 +89,25 @@ mod test { use std::convert::TryInto; use tari_common_types::types::PublicKey; - - use crate::validation::dan_validators::test_helpers::{ - assert_dan_validator_fail, - assert_dan_validator_success, - create_contract_constitution, - create_contract_proposal_schema, - init_test_blockchain, - publish_constitution, - publish_definition, - publish_update_proposal, - schema_to_transaction, + use tari_test_utils::unpack_enum; + + use crate::{ + transactions::transaction_components::OutputType, + validation::dan_validators::{ + test_helpers::{ + assert_dan_validator_err, + assert_dan_validator_fail, + assert_dan_validator_success, + create_contract_constitution, + create_contract_proposal_schema, + init_test_blockchain, + publish_constitution, + publish_definition, + publish_update_proposal, + schema_to_transaction, + }, + DanLayerValidationError, + }, }; #[test] @@ -180,6 +186,16 @@ mod test { let (tx, _) = schema_to_transaction(&schema); // try to validate the duplicated proposal transaction and check that we get the error - assert_dan_validator_fail(&blockchain, &tx, "Duplicated contract update proposal"); + let err = assert_dan_validator_err(&blockchain, &tx); + let expected_contract_id = contract_id; + unpack_enum!( + DanLayerValidationError::DuplicateUtxo { + output_type, + contract_id, + .. + } = err + ); + assert_eq!(output_type, OutputType::ContractConstitutionProposal); + assert_eq!(contract_id, expected_contract_id); } } diff --git a/base_layer/core/src/validation/error.rs b/base_layer/core/src/validation/error.rs index 7ab54a285b..6ba293a229 100644 --- a/base_layer/core/src/validation/error.rs +++ b/base_layer/core/src/validation/error.rs @@ -30,6 +30,7 @@ use crate::{ covenants::CovenantError, proof_of_work::{monero_rx::MergeMineError, PowError}, transactions::transaction_components::TransactionError, + validation::dan_validators::DanLayerValidationError, }; #[derive(Debug, Error)] @@ -117,7 +118,7 @@ pub enum ValidationError { #[error("Standard transaction contains coinbase output")] ErroneousCoinbaseOutput, #[error("Digital Asset Network Error: {0}")] - DanLayerError(String), + DanLayerError(#[from] DanLayerValidationError), } // ChainStorageError has a ValidationError variant, so to prevent a cyclic dependency we use a string representation in diff --git a/base_layer/wallet/src/assets/asset_manager.rs b/base_layer/wallet/src/assets/asset_manager.rs index 912d7fe33d..1d0b10cd1e 100644 --- a/base_layer/wallet/src/assets/asset_manager.rs +++ b/base_layer/wallet/src/assets/asset_manager.rs @@ -28,6 +28,7 @@ use tari_common_types::{ use tari_core::transactions::transaction_components::{ CommitteeSignatures, ContractAmendment, + ContractCheckpoint, ContractDefinition, ContractUpdateProposal, OutputFeatures, @@ -183,12 +184,12 @@ impl AssetManager { .output_manager .create_output_with_features( 10.into(), - OutputFeatures::for_checkpoint( - contract_id, + OutputFeatures::for_contract_checkpoint(contract_id, ContractCheckpoint { + checkpoint_number: 0, merkle_root, // TODO: add vn signatures - CommitteeSignatures::default(), - ), + signatures: CommitteeSignatures::default(), + }), ) .await?; // TODO: get consensus threshold from somewhere else @@ -228,18 +229,19 @@ impl AssetManager { pub async fn create_follow_on_contract_checkpoint( &mut self, contract_id: FixedHash, + checkpoint_number: u64, merkle_root: FixedHash, ) -> Result<(TxId, Transaction), WalletError> { let output = self .output_manager .create_output_with_features( 10.into(), - OutputFeatures::for_checkpoint( - contract_id, + OutputFeatures::for_contract_checkpoint(contract_id, ContractCheckpoint { + checkpoint_number, merkle_root, - // TODO: Add vn sigs - CommitteeSignatures::default(), - ), + // TODO: add vn signatures + signatures: CommitteeSignatures::default(), + }), ) .await?; // TODO: get consensus threshold from somewhere else diff --git a/base_layer/wallet/src/assets/asset_manager_handle.rs b/base_layer/wallet/src/assets/asset_manager_handle.rs index efbc42122a..b883f9567a 100644 --- a/base_layer/wallet/src/assets/asset_manager_handle.rs +++ b/base_layer/wallet/src/assets/asset_manager_handle.rs @@ -105,12 +105,14 @@ impl AssetManagerHandle { pub async fn create_follow_on_asset_checkpoint( &mut self, contract_id: FixedHash, + checkpoint_number: u64, merkle_root: FixedHash, ) -> Result<(TxId, Transaction), WalletError> { match self .handle .call(AssetManagerRequest::CreateFollowOnCheckpoint { contract_id, + checkpoint_number, merkle_root, committee_public_keys: Vec::new(), }) diff --git a/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs b/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs index 361acee8bd..87a9eba16c 100644 --- a/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs +++ b/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs @@ -150,12 +150,13 @@ impl AssetManagerService { }, AssetManagerRequest::CreateFollowOnCheckpoint { contract_id, + checkpoint_number, merkle_root, committee_public_keys: _pks, } => { let (tx_id, transaction) = self .manager - .create_follow_on_contract_checkpoint(contract_id, merkle_root) + .create_follow_on_contract_checkpoint(contract_id, checkpoint_number, merkle_root) .await?; Ok(AssetManagerResponse::CreateFollowOnCheckpoint { transaction: Box::new(transaction), diff --git a/base_layer/wallet/src/assets/infrastructure/mod.rs b/base_layer/wallet/src/assets/infrastructure/mod.rs index b72c106e00..ebf7b89f3b 100644 --- a/base_layer/wallet/src/assets/infrastructure/mod.rs +++ b/base_layer/wallet/src/assets/infrastructure/mod.rs @@ -67,6 +67,7 @@ pub enum AssetManagerRequest { }, CreateFollowOnCheckpoint { contract_id: FixedHash, + checkpoint_number: u64, merkle_root: FixedHash, committee_public_keys: Vec, }, diff --git a/base_layer/wallet/tests/output_manager_service_tests/service.rs b/base_layer/wallet/tests/output_manager_service_tests/service.rs index ea82a9235b..7219b94f9a 100644 --- a/base_layer/wallet/tests/output_manager_service_tests/service.rs +++ b/base_layer/wallet/tests/output_manager_service_tests/service.rs @@ -43,6 +43,7 @@ use tari_core::{ test_helpers::{create_unblinded_output, TestParams as TestParamsHelpers}, transaction_components::{ CommitteeSignatures, + ContractCheckpoint, EncryptedValue, OutputFeatures, OutputType, @@ -767,10 +768,13 @@ async fn utxo_selection_for_contract_checkpoint() { &mut OsRng.clone(), amount, &factories.commitment, - Some(OutputFeatures::for_checkpoint( + Some(OutputFeatures::for_contract_checkpoint( contract_id, - FixedHash::zero(), - CommitteeSignatures::empty(), + ContractCheckpoint { + checkpoint_number: 0, + merkle_root: FixedHash::zero(), + signatures: CommitteeSignatures::empty(), + }, )), oms.clone(), ) @@ -799,7 +803,11 @@ async fn utxo_selection_for_contract_checkpoint() { // Spend more than the selected contract output, this will cause the other UTXO to be included MicroTari::from(2500), UtxoSelectionCriteria::for_contract(contract_id, OutputType::ContractCheckpoint), - OutputFeatures::for_checkpoint(contract_id, FixedHash::zero(), CommitteeSignatures::empty()), + OutputFeatures::for_contract_checkpoint(contract_id, ContractCheckpoint { + checkpoint_number: 0, + merkle_root: FixedHash::zero(), + signatures: CommitteeSignatures::empty(), + }), fee_per_gram, None, String::new(), diff --git a/dan_layer/core/src/services/checkpoint_manager.rs b/dan_layer/core/src/services/checkpoint_manager.rs index bd5b7a3c04..f7ee1ff7ec 100644 --- a/dan_layer/core/src/services/checkpoint_manager.rs +++ b/dan_layer/core/src/services/checkpoint_manager.rs @@ -41,7 +41,6 @@ pub struct ConcreteCheckpointManager { asset_definition: AssetDefinition, wallet: TWallet, num_calls: u32, - is_initial_checkpoint: bool, checkpoint_interval: u32, } @@ -51,8 +50,6 @@ impl ConcreteCheckpointManager { asset_definition, wallet, num_calls: 0, - // TODO: VNs need to be aware of the checkpoint state - is_initial_checkpoint: true, checkpoint_interval: 100, } } @@ -61,22 +58,18 @@ impl ConcreteCheckpointManager { #[async_trait] impl CheckpointManager for ConcreteCheckpointManager { async fn create_checkpoint(&mut self, state_root: StateRoot) -> Result<(), DigitalAssetError> { - self.num_calls += 1; - if self.num_calls > self.checkpoint_interval { + if self.num_calls == 0 || self.num_calls >= self.checkpoint_interval { + // TODO: fetch and increment checkpoint number + let checkpoint_number = u64::from(self.num_calls / self.checkpoint_interval); info!( target: LOG_TARGET, "Creating checkpoint for contract {}", self.asset_definition.contract_id ); self.wallet - .create_new_checkpoint( - &self.asset_definition.contract_id, - &state_root, - self.is_initial_checkpoint, - ) + .create_new_checkpoint(&self.asset_definition.contract_id, &state_root, checkpoint_number) .await?; - self.is_initial_checkpoint = false; - self.num_calls = 0; } + self.num_calls += 1; Ok(()) } } diff --git a/dan_layer/core/src/services/mocks/mod.rs b/dan_layer/core/src/services/mocks/mod.rs index ccf61e5fd3..3b76fd50bb 100644 --- a/dan_layer/core/src/services/mocks/mod.rs +++ b/dan_layer/core/src/services/mocks/mod.rs @@ -340,7 +340,7 @@ impl WalletClient for MockWalletClient { &mut self, _contract_id: &FixedHash, _state_root: &StateRoot, - _is_initial: bool, + _checkpoint_number: u64, ) -> Result<(), DigitalAssetError> { Ok(()) } diff --git a/dan_layer/core/src/services/wallet_client.rs b/dan_layer/core/src/services/wallet_client.rs index 42d9537a83..f4344aa26e 100644 --- a/dan_layer/core/src/services/wallet_client.rs +++ b/dan_layer/core/src/services/wallet_client.rs @@ -31,7 +31,7 @@ pub trait WalletClient: Send + Sync { &mut self, contract_id: &FixedHash, state_root: &StateRoot, - is_initial: bool, + checkpoint_number: u64, ) -> Result<(), DigitalAssetError>; async fn submit_contract_acceptance(