From ada31432ea2e0ac1591153580b0e2b86475b30e7 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Thu, 26 May 2022 12:38:45 +0400 Subject: [PATCH] feat(core)!: add side-chain features and constitution to UTXOs (#4134) Description --- Based on #4122 #4129 #4121 - adds `SideChainFeatures` to `OutputFeatures` - updates code and tests - updates cucumber output feature consensus encoding - allows MaxSizeVec to be used directly in consensus encoded structs Motivation and Context --- Partial implementation of RFC 312 contract constitution How Has This Been Tested? --- - New consensus de/encoding tests - Manually on igor - cucumbers pass --- Cargo.lock | 7 +- applications/tari_app_grpc/Cargo.toml | 1 + applications/tari_app_grpc/proto/types.proto | 75 ++++- .../tari_app_grpc/src/conversions/mod.rs | 1 + .../src/conversions/output_features.rs | 28 +- .../src/conversions/sidechain_features.rs | 267 ++++++++++++++++++ .../common_types/src/types/fixed_hash.rs | 30 +- base_layer/core/Cargo.toml | 2 + base_layer/core/src/blocks/genesis_block.rs | 3 +- .../tests/blockchain_database.rs | 1 - .../src/consensus/consensus_encoding/vec.rs | 31 ++ base_layer/core/src/covenants/arguments.rs | 19 +- base_layer/core/src/covenants/byte_codes.rs | 8 +- base_layer/core/src/covenants/covenant.rs | 3 +- base_layer/core/src/covenants/decoder.rs | 9 +- base_layer/core/src/covenants/encoder.rs | 3 +- base_layer/core/src/covenants/fields.rs | 90 ++++-- base_layer/core/src/covenants/filters/and.rs | 12 +- .../core/src/covenants/filters/field_eq.rs | 9 +- .../src/covenants/filters/fields_hashed_eq.rs | 10 +- .../src/covenants/filters/fields_preserved.rs | 17 +- base_layer/core/src/covenants/filters/not.rs | 10 +- base_layer/core/src/covenants/filters/or.rs | 10 +- .../src/covenants/filters/output_hash_eq.rs | 4 +- base_layer/core/src/covenants/filters/xor.rs | 10 +- base_layer/core/src/covenants/macros.rs | 19 +- base_layer/core/src/covenants/token.rs | 6 +- base_layer/core/src/proto/transaction.proto | 73 ++++- base_layer/core/src/proto/transaction.rs | 262 ++++++++++++++++- .../transaction_components/error.rs | 2 + .../transaction_components/mod.rs | 2 +- .../transaction_components/output_features.rs | 89 +++++- .../side_chain/committee_members.rs | 29 +- .../side_chain/contract_constitution.rs | 80 ++---- .../transaction_components/side_chain/mod.rs | 13 +- .../side_chain/sidechain_features.rs | 146 ++++++++++ .../transaction_components/test.rs | 33 ++- .../transaction_protocol/single_receiver.rs | 2 +- .../transaction_initializer.rs | 2 +- .../src/validation/block_validators/test.rs | 11 +- base_layer/core/tests/mempool.rs | 1 + .../storage/sqlite_db/new_output_sql.rs | 2 +- .../storage/sqlite_db/output_sql.rs | 15 +- base_layer/wallet_ffi/src/lib.rs | 1 + common/config/presets/base_node.toml | 2 +- .../helpers/transactionBuilder.js | 3 + integration_tests/helpers/util.js | 6 +- 47 files changed, 1219 insertions(+), 240 deletions(-) create mode 100644 applications/tari_app_grpc/src/conversions/sidechain_features.rs create mode 100644 base_layer/core/src/transactions/transaction_components/side_chain/sidechain_features.rs diff --git a/Cargo.lock b/Cargo.lock index 6b8fe2421f..4e6a23e5a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4176,9 +4176,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", "libm 0.2.2", @@ -6519,6 +6519,7 @@ name = "tari_app_grpc" version = "0.32.0" dependencies = [ "chrono", + "num-traits", "prost", "prost-types", "tari_common_types", @@ -6893,7 +6894,9 @@ dependencies = [ "log-mdc", "monero", "newtype-ops", + "num-derive", "num-format", + "num-traits", "once_cell", "prost", "prost-types", diff --git a/applications/tari_app_grpc/Cargo.toml b/applications/tari_app_grpc/Cargo.toml index 5c09ceebac..4c317b67cc 100644 --- a/applications/tari_app_grpc/Cargo.toml +++ b/applications/tari_app_grpc/Cargo.toml @@ -18,6 +18,7 @@ tari_utilities = { git = "https://github.com/tari-project/tari_utilities.git", t chrono = { version = "0.4.19", default-features = false } prost = "0.9" prost-types = "0.9" +num-traits = "0.2.15" tonic = "0.6.2" [build-dependencies] diff --git a/applications/tari_app_grpc/proto/types.proto b/applications/tari_app_grpc/proto/types.proto index ded33a0ba8..1a0be8d0df 100644 --- a/applications/tari_app_grpc/proto/types.proto +++ b/applications/tari_app_grpc/proto/types.proto @@ -206,32 +206,79 @@ message TransactionOutput { // Options for UTXOs message OutputFeatures { + // Version + uint32 version = 1; // Flags are the feature flags that differentiate between outputs, eg Coinbase all of which has different rules - uint32 flags = 1; + uint32 flags = 2; // The maturity of the specific UTXO. This is the min lock height at which an UTXO can be spend. Coinbase UTXO // require a min maturity of the Coinbase_lock_height, this should be checked on receiving new blocks. - uint64 maturity = 2; + uint64 maturity = 3; + bytes metadata = 4; + // The recovery byte - not consensus critical - can help reduce the bandwidth with wallet recovery or in other + // instances when a wallet needs to request the complete UTXO set from a base node. + uint32 recovery_byte = 5; + SideChainFeatures sidechain_features = 6; + bytes unique_id = 7; + + // TODO: deprecated + AssetOutputFeatures asset = 8; + bytes parent_public_key = 9; + MintNonFungibleFeatures mint_non_fungible = 10; + SideChainCheckpointFeatures sidechain_checkpoint = 11; + CommitteeDefinitionFeatures committee_definition = 12; +} - bytes metadata= 3; +message SideChainFeatures { + bytes contract_id = 1; + // ContractDefinition definition = 2; + ContractConstitution constitution = 3; +} + +message ContractConstitution { + CommitteeMembers validator_committee = 1; + ContractAcceptanceRequirements acceptance_requirements = 2; + SideChainConsensus consensus = 3; + CheckpointParameters checkpoint_params = 4; + ConstitutionChangeRules constitution_change_rules = 5; + uint64 initial_reward = 6; +} - AssetOutputFeatures asset = 4; +message ContractAcceptanceRequirements { + uint64 acceptance_period_expiry = 1; + uint32 minimum_quorum_required = 2; +} - bytes unique_id = 5; +message CommitteeMembers { + repeated bytes members = 1; +} - bytes parent_public_key = 6; +message CheckpointParameters { + uint64 abandoned_interval = 1; + uint32 minimum_quorum_required = 2; +} - MintNonFungibleFeatures mint_non_fungible = 7; +message ConstitutionChangeRules { + uint32 change_flags = 1; + RequirementsForConstitutionChange requirements_for_constitution_change = 2; +} - SideChainCheckpointFeatures sidechain_checkpoint = 8; - // Version - uint32 version = 9; - CommitteeDefinitionFeatures committee_definition = 10; +message RequirementsForConstitutionChange { + // The minimum required constitution committee signatures required for a constitution change proposal to pass. + uint32 minimum_constitution_committee_signatures = 1; + // An allowlist of keys that are able to accept and ratify the initial constitution and its amendments. If this is + // None, the constitution cannot be amended. + CommitteeMembers constitution_committee = 2; +} - // The recovery byte - not consensus critical - can help reduce the bandwidth with wallet recovery or in other - // instances when a wallet needs to request the complete UTXO set from a base node. - uint32 recovery_byte = 11; +enum SideChainConsensus { + UNSPECIFIED = 0; + BFT = 1; + PROOF_OF_WORK = 2; + MERKLE_ROOT = 3; } +// TODO: DEPRECATED + message AssetOutputFeatures { bytes public_key = 1; repeated uint32 template_ids_implemented = 2; diff --git a/applications/tari_app_grpc/src/conversions/mod.rs b/applications/tari_app_grpc/src/conversions/mod.rs index 015a960486..c08c3d0cdb 100644 --- a/applications/tari_app_grpc/src/conversions/mod.rs +++ b/applications/tari_app_grpc/src/conversions/mod.rs @@ -32,6 +32,7 @@ mod new_block_template; mod output_features; mod peer; mod proof_of_work; +mod sidechain_features; mod signature; mod transaction; mod transaction_input; diff --git a/applications/tari_app_grpc/src/conversions/output_features.rs b/applications/tari_app_grpc/src/conversions/output_features.rs index 7e98fa62ff..a0473b6a4f 100644 --- a/applications/tari_app_grpc/src/conversions/output_features.rs +++ b/applications/tari_app_grpc/src/conversions/output_features.rs @@ -34,6 +34,7 @@ use tari_core::transactions::transaction_components::{ OutputFeaturesVersion, OutputFlags, SideChainCheckpointFeatures, + SideChainFeatures, TemplateParameter, }; use tari_utilities::ByteArray; @@ -54,7 +55,12 @@ impl TryFrom for OutputFeatures { } else { Some(PublicKey::from_bytes(features.parent_public_key.as_bytes()).map_err(|err| format!("{:?}", err))?) }; - let flags = u16::try_from(features.flags).map_err(|_| "Invalid output flags: overflowed u8")?; + let sidechain_features = features + .sidechain_features + .map(SideChainFeatures::try_from) + .transpose()?; + + let flags = u16::try_from(features.flags).map_err(|_| "Invalid output flags: overflowed u16")?; Ok(OutputFeatures::new( OutputFeaturesVersion::try_from( @@ -65,6 +71,7 @@ impl TryFrom for OutputFeatures { u8::try_from(features.recovery_byte).map_err(|_| "Invalid recovery byte: overflowed u8")?, features.metadata, unique_id, + sidechain_features, parent_public_key, features.asset.map(|a| a.try_into()).transpose()?, features.mint_non_fungible.map(|m| m.try_into()).transpose()?, @@ -77,24 +84,23 @@ impl TryFrom for OutputFeatures { impl From for grpc::OutputFeatures { fn from(features: OutputFeatures) -> Self { Self { + version: features.version as u32, flags: u32::from(features.flags.bits()), maturity: features.maturity, metadata: features.metadata, unique_id: features.unique_id.unwrap_or_default(), - parent_public_key: features - .parent_public_key - .map(|a| a.as_bytes().to_vec()) - .unwrap_or_default(), - asset: features.asset.map(|a| a.into()), - mint_non_fungible: features.mint_non_fungible.map(|m| m.into()), - sidechain_checkpoint: features.sidechain_checkpoint.map(|m| m.into()), - version: features.version as u32, - committee_definition: features.committee_definition.map(|c| c.into()), recovery_byte: u32::from(features.recovery_byte), + sidechain_features: features.sidechain_features.map(Into::into), + + // TODO: Deprecated + asset: None, + parent_public_key: vec![], + mint_non_fungible: None, + sidechain_checkpoint: None, + committee_definition: None, } } } - impl TryFrom for AssetOutputFeatures { type Error = String; diff --git a/applications/tari_app_grpc/src/conversions/sidechain_features.rs b/applications/tari_app_grpc/src/conversions/sidechain_features.rs new file mode 100644 index 0000000000..9c2c7a491b --- /dev/null +++ b/applications/tari_app_grpc/src/conversions/sidechain_features.rs @@ -0,0 +1,267 @@ +// 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 std::convert::{TryFrom, TryInto}; + +use tari_common_types::types::PublicKey; +use tari_core::transactions::transaction_components::{ + CheckpointParameters, + CommitteeMembers, + ConstitutionChangeFlags, + ConstitutionChangeRules, + ContractAcceptanceRequirements, + ContractConstitution, + RequirementsForConstitutionChange, + SideChainConsensus, + SideChainFeatures, +}; +use tari_utilities::ByteArray; + +use crate::tari_rpc as grpc; + +impl From for grpc::SideChainFeatures { + fn from(value: SideChainFeatures) -> Self { + Self { + contract_id: value.contract_id.to_vec(), + constitution: value.constitution.map(Into::into), + } + } +} + +impl TryFrom for SideChainFeatures { + type Error = String; + + fn try_from(features: grpc::SideChainFeatures) -> Result { + let constitution = features.constitution.map(ContractConstitution::try_from).transpose()?; + + Ok(Self { + contract_id: features.contract_id.try_into().map_err(|_| "Invalid contract_id")?, + constitution, + }) + } +} + +//---------------------------------- ContractConstitution --------------------------------------------// +impl From for grpc::ContractConstitution { + fn from(value: ContractConstitution) -> Self { + Self { + validator_committee: Some(value.validator_committee.into()), + acceptance_requirements: Some(value.acceptance_requirements.into()), + consensus: value.consensus.into(), + checkpoint_params: Some(value.checkpoint_params.into()), + constitution_change_rules: Some(value.constitution_change_rules.into()), + initial_reward: value.initial_reward.into(), + } + } +} + +impl TryFrom for ContractConstitution { + type Error = String; + + fn try_from(value: grpc::ContractConstitution) -> Result { + use num_traits::FromPrimitive; + let validator_committee = value + .validator_committee + .map(TryInto::try_into) + .ok_or("validator_committee not provided")??; + let acceptance_requirements = value + .acceptance_requirements + .map(TryInto::try_into) + .ok_or("acceptance_requirements not provided")??; + let consensus = SideChainConsensus::from_i32(value.consensus).ok_or("Invalid SideChainConsensus")?; + let checkpoint_params = value + .checkpoint_params + .map(TryInto::try_into) + .ok_or("checkpoint_params not provided")??; + let constitution_change_rules = value + .constitution_change_rules + .map(TryInto::try_into) + .ok_or("constitution_change_rules not provided")??; + let initial_reward = value.initial_reward.into(); + + Ok(Self { + validator_committee, + acceptance_requirements, + consensus, + checkpoint_params, + constitution_change_rules, + initial_reward, + }) + } +} + +//---------------------------------- ContractAcceptanceRequirements --------------------------------------------// +impl From for grpc::ContractAcceptanceRequirements { + fn from(value: ContractAcceptanceRequirements) -> Self { + Self { + acceptance_period_expiry: value.acceptance_period_expiry, + minimum_quorum_required: value.minimum_quorum_required, + } + } +} + +impl TryFrom for ContractAcceptanceRequirements { + type Error = String; + + fn try_from(value: grpc::ContractAcceptanceRequirements) -> Result { + Ok(Self { + acceptance_period_expiry: value.acceptance_period_expiry, + minimum_quorum_required: value.minimum_quorum_required, + }) + } +} + +//---------------------------------- SideChainConsensus --------------------------------------------// +impl From for grpc::SideChainConsensus { + fn from(value: SideChainConsensus) -> Self { + #[allow(clippy::enum_glob_use)] + use grpc::SideChainConsensus::*; + match value { + SideChainConsensus::Bft => Bft, + SideChainConsensus::ProofOfWork => ProofOfWork, + SideChainConsensus::MerkleRoot => MerkleRoot, + } + } +} + +impl TryFrom for SideChainConsensus { + type Error = String; + + fn try_from(value: grpc::SideChainConsensus) -> Result { + #[allow(clippy::enum_glob_use)] + use grpc::SideChainConsensus::*; + match value { + Unspecified => Err("Side chain consensus not specified or invalid".to_string()), + Bft => Ok(SideChainConsensus::Bft), + ProofOfWork => Ok(SideChainConsensus::ProofOfWork), + MerkleRoot => Ok(SideChainConsensus::MerkleRoot), + } + } +} + +//---------------------------------- CheckpointParameters --------------------------------------------// +impl From for grpc::CheckpointParameters { + fn from(value: CheckpointParameters) -> Self { + Self { + minimum_quorum_required: value.minimum_quorum_required, + abandoned_interval: value.abandoned_interval, + } + } +} + +impl TryFrom for CheckpointParameters { + type Error = String; + + fn try_from(value: grpc::CheckpointParameters) -> Result { + Ok(Self { + minimum_quorum_required: value.minimum_quorum_required, + abandoned_interval: value.abandoned_interval, + }) + } +} + +//---------------------------------- ConstitutionChangeRules --------------------------------------------// +impl From for grpc::ConstitutionChangeRules { + fn from(value: ConstitutionChangeRules) -> Self { + Self { + change_flags: value.change_flags.bits().into(), + requirements_for_constitution_change: value.requirements_for_constitution_change.map(Into::into), + } + } +} + +impl TryFrom for ConstitutionChangeRules { + type Error = String; + + fn try_from(value: grpc::ConstitutionChangeRules) -> Result { + Ok(Self { + change_flags: u8::try_from(value.change_flags) + .ok() + .and_then(ConstitutionChangeFlags::from_bits) + .ok_or("Invalid change_flags")?, + requirements_for_constitution_change: value + .requirements_for_constitution_change + .map(RequirementsForConstitutionChange::try_from) + .transpose()?, + }) + } +} + +//---------------------------------- RequirementsForConstitutionChange --------------------------------------------// +impl From for grpc::RequirementsForConstitutionChange { + fn from(value: RequirementsForConstitutionChange) -> Self { + Self { + minimum_constitution_committee_signatures: value.minimum_constitution_committee_signatures, + constitution_committee: value.constitution_committee.map(Into::into), + } + } +} + +impl TryFrom for RequirementsForConstitutionChange { + type Error = String; + + fn try_from(value: grpc::RequirementsForConstitutionChange) -> Result { + Ok(Self { + minimum_constitution_committee_signatures: value.minimum_constitution_committee_signatures, + constitution_committee: value + .constitution_committee + .map(CommitteeMembers::try_from) + .transpose()?, + }) + } +} + +//---------------------------------- CommitteeMembers --------------------------------------------// +impl From for grpc::CommitteeMembers { + fn from(value: CommitteeMembers) -> Self { + Self { + members: value.members().iter().map(|pk| pk.to_vec()).collect(), + } + } +} + +impl TryFrom for CommitteeMembers { + type Error = String; + + fn try_from(value: grpc::CommitteeMembers) -> Result { + if value.members.len() > CommitteeMembers::MAX_MEMBERS { + return Err(format!( + "Too many committee members: expected {} but got {}", + CommitteeMembers::MAX_MEMBERS, + value.members.len() + )); + } + + let members = value + .members + .iter() + .enumerate() + .map(|(i, c)| { + PublicKey::from_bytes(c) + .map_err(|err| format!("committee member #{} was not a valid public key: {}", i + 1, err)) + }) + .collect::, _>>()?; + + let members = CommitteeMembers::try_from(members).map_err(|e| e.to_string())?; + Ok(members) + } +} diff --git a/base_layer/common_types/src/types/fixed_hash.rs b/base_layer/common_types/src/types/fixed_hash.rs index 652bbe9005..af1d00b276 100644 --- a/base_layer/common_types/src/types/fixed_hash.rs +++ b/base_layer/common_types/src/types/fixed_hash.rs @@ -25,10 +25,12 @@ use std::{ ops::{Deref, DerefMut}, }; -use digest::{consts::U32, generic_array}; +use digest::{consts::U32, generic_array, Digest}; use serde::{Deserialize, Serialize}; use tari_utilities::hex::{Hex, HexError}; +use crate::types::HashDigest; + const ZERO_HASH: [u8; FixedHash::byte_size()] = [0u8; FixedHash::byte_size()]; #[derive(thiserror::Error, Debug)] @@ -50,6 +52,12 @@ impl FixedHash { pub fn as_slice(&self) -> &[u8] { &self.0 } + + /// Hashes the bytes and returns the resulting `FixedHash`. Generally only be used as a convenience function for + /// tests. + pub fn hash_bytes>(bytes: T) -> Self { + HashDigest::default().chain(bytes).finalize().into() + } } impl From<[u8; FixedHash::byte_size()]> for FixedHash { @@ -81,7 +89,7 @@ impl TryFrom<&[u8]> for FixedHash { } impl From> for FixedHash { - fn from(hash: digest::generic_array::GenericArray) -> Self { + fn from(hash: generic_array::GenericArray) -> Self { Self(hash.into()) } } @@ -92,6 +100,24 @@ impl PartialEq<[u8]> for FixedHash { } } +impl PartialEq for [u8] { + fn eq(&self, other: &FixedHash) -> bool { + self[..].eq(&other.0) + } +} + +impl PartialEq> for FixedHash { + fn eq(&self, other: &Vec) -> bool { + self == other.as_slice() + } +} + +impl AsRef<[u8]> for FixedHash { + fn as_ref(&self) -> &[u8] { + self.as_slice() + } +} + impl Hex for FixedHash { fn from_hex(hex: &str) -> Result where Self: Sized { diff --git a/base_layer/core/Cargo.toml b/base_layer/core/Cargo.toml index 7b153a3b26..ebf5233813 100644 --- a/base_layer/core/Cargo.toml +++ b/base_layer/core/Cargo.toml @@ -55,6 +55,8 @@ log = "0.4" log-mdc = "0.1.0" monero = { version = "^0.13.0", features = ["serde_support"], optional = true } newtype-ops = "0.1.4" +num-traits = "0.2.15" +num-derive = "0.3.3" num-format = "0.4.0" once_cell = "1.8.0" prost = "0.9" diff --git a/base_layer/core/src/blocks/genesis_block.rs b/base_layer/core/src/blocks/genesis_block.rs index 720a6e8fdb..81a32776ac 100644 --- a/base_layer/core/src/blocks/genesis_block.rs +++ b/base_layer/core/src/blocks/genesis_block.rs @@ -214,7 +214,7 @@ pub fn get_dibbler_genesis_block() -> ChainBlock { // hardcode the Merkle roots once they've been computed above block.header.kernel_mr = from_hex("5b91bebd33e18798e03e9c5d831d161ee9c3d12560f50b987e1a8c3ec53146df").unwrap(); block.header.witness_mr = from_hex("11227f6ce9ff34349d7dcab606b633f55234d5c8a73696a68c6e9ddc7cd3bc40").unwrap(); - block.header.output_mr = from_hex("5e69274e72f8590e1cf91c189e24368527414aed966de62135d9273a6c14c3ef").unwrap(); + block.header.output_mr = from_hex("12c50da7232c05dc969b388b86eb6f093c8eb358322fee2486ff5ea84975edde").unwrap(); let accumulated_data = BlockHeaderAccumulatedData { hash: block.hash(), @@ -243,6 +243,7 @@ fn get_dibbler_genesis_block_raw() -> Block { recovery_byte: 0, metadata: Vec::new(), unique_id: None, + sidechain_features: None, parent_public_key: None, asset: None, mint_non_fungible: None, diff --git a/base_layer/core/src/chain_storage/tests/blockchain_database.rs b/base_layer/core/src/chain_storage/tests/blockchain_database.rs index 0f7f541cc9..146af7086b 100644 --- a/base_layer/core/src/chain_storage/tests/blockchain_database.rs +++ b/base_layer/core/src/chain_storage/tests/blockchain_database.rs @@ -377,7 +377,6 @@ mod fetch_block_hashes_from_header_tip { } mod add_block { - use super::*; #[test] diff --git a/base_layer/core/src/consensus/consensus_encoding/vec.rs b/base_layer/core/src/consensus/consensus_encoding/vec.rs index f5b650a09b..82955c8094 100644 --- a/base_layer/core/src/consensus/consensus_encoding/vec.rs +++ b/base_layer/core/src/consensus/consensus_encoding/vec.rs @@ -40,6 +40,24 @@ impl MaxSizeVec { pub fn into_vec(self) -> Vec { self.inner } + + pub fn try_from_iter>(iter: I) -> Option { + let iter = iter.into_iter(); + let (lower, upper) = iter.size_hint(); + if lower > MAX { + return None; + } + + let capacity = upper.filter(|u| *u <= MAX).unwrap_or(lower); + let mut inner = Vec::with_capacity(capacity); + for item in iter { + if inner.len() + 1 > MAX { + return None; + } + inner.push(item); + } + Some(Self { inner }) + } } impl Deref for MaxSizeVec { @@ -107,3 +125,16 @@ impl PartialEq> for MaxSizeVec { self.inner.eq(other) } } + +#[cfg(test)] +mod tests { + use std::iter; + + use super::*; + + #[test] + fn try_from_iter() { + MaxSizeVec::<_, 5>::try_from_iter(iter::repeat(1).take(5)).unwrap(); + assert!(MaxSizeVec::<_, 5>::try_from_iter(iter::repeat(1).take(6)).is_none()); + } +} diff --git a/base_layer/core/src/covenants/arguments.rs b/base_layer/core/src/covenants/arguments.rs index 1bb522fd7c..0ec6e82913 100644 --- a/base_layer/core/src/covenants/arguments.rs +++ b/base_layer/core/src/covenants/arguments.rs @@ -26,7 +26,7 @@ use std::{ }; use integer_encoding::VarIntWriter; -use tari_common_types::types::{Commitment, PublicKey}; +use tari_common_types::types::{Commitment, FixedHash, PublicKey}; use tari_script::TariScript; use tari_utilities::hex::{to_hex, Hex}; @@ -45,11 +45,9 @@ use crate::{ const MAX_COVENANT_ARG_SIZE: usize = 4096; const MAX_BYTES_ARG_SIZE: usize = 4096; -pub type Hash = [u8; 32]; - #[derive(Debug, Clone, PartialEq, Eq)] pub enum CovenantArg { - Hash(Hash), + Hash(FixedHash), PublicKey(PublicKey), Commitment(Commitment), TariScript(TariScript), @@ -71,7 +69,7 @@ impl CovenantArg { ARG_HASH => { let mut hash = [0u8; 32]; reader.read_exact(&mut hash)?; - Ok(CovenantArg::Hash(hash)) + Ok(CovenantArg::Hash(hash.into())) }, ARG_PUBLIC_KEY => { let pk = PublicKey::consensus_decode(reader)?; @@ -168,9 +166,9 @@ impl CovenantArg { } macro_rules! require_x_impl { - ($name:ident, $output:ident, $expected: expr) => { + ($name:ident, $output:ident, $expected: expr, $output_type:ident) => { #[allow(dead_code)] - pub(super) fn $name(self) -> Result<$output, CovenantError> { + pub(super) fn $name(self) -> Result<$output_type, CovenantError> { match self { CovenantArg::$output(obj) => Ok(obj), got => Err(CovenantError::UnexpectedArgument { @@ -180,10 +178,13 @@ macro_rules! require_x_impl { } } }; + ($name:ident, $output:ident, $expected:expr) => { + require_x_impl!($name, $output, $expected, $output); + }; } impl CovenantArg { - require_x_impl!(require_hash, Hash, "hash"); + require_x_impl!(require_hash, Hash, "hash", FixedHash); require_x_impl!(require_publickey, PublicKey, "publickey"); @@ -298,7 +299,7 @@ mod test { &from_hex("020000000000000000000000000000000000000000000000000000000000000000").unwrap(), ); test_case( - CovenantArg::Hash([0u8; 32]), + CovenantArg::Hash(FixedHash::zero()), &from_hex("010000000000000000000000000000000000000000000000000000000000000000").unwrap(), ); test_case(CovenantArg::TariScript(script!(Nop)), &[ARG_TARI_SCRIPT, 0x01, 0x73]); diff --git a/base_layer/core/src/covenants/byte_codes.rs b/base_layer/core/src/covenants/byte_codes.rs index 176fd53943..42070595d4 100644 --- a/base_layer/core/src/covenants/byte_codes.rs +++ b/base_layer/core/src/covenants/byte_codes.rs @@ -86,9 +86,11 @@ pub const FIELD_COVENANT: u8 = 0x03; pub const FIELD_FEATURES: u8 = 0x04; pub const FIELD_FEATURES_FLAGS: u8 = 0x05; pub const FIELD_FEATURES_MATURITY: u8 = 0x06; -pub const FIELD_FEATURES_UNIQUE_ID: u8 = 0x07; -pub const FIELD_FEATURES_PARENT_PUBLIC_KEY: u8 = 0x08; -pub const FIELD_FEATURES_METADATA: u8 = 0x09; +pub const FIELD_FEATURES_METADATA: u8 = 0x07; +pub const FIELD_FEATURES_SIDE_CHAIN_FEATURES: u8 = 0x08; +pub const FIELD_FEATURES_SIDE_CHAIN_FEATURES_CONTRACT_ID: u8 = 0x09; +pub const FIELD_FEATURES_UNIQUE_ID: u8 = 0x0a; +pub const FIELD_FEATURES_PARENT_PUBLIC_KEY: u8 = 0x0b; #[cfg(test)] mod tests { diff --git a/base_layer/core/src/covenants/covenant.rs b/base_layer/core/src/covenants/covenant.rs index 33c1a1dbc0..0955170fdc 100644 --- a/base_layer/core/src/covenants/covenant.rs +++ b/base_layer/core/src/covenants/covenant.rs @@ -187,8 +187,7 @@ mod test { let covenant = covenant!(fields_preserved(@fields( @field::features_flags, @field::features_maturity, - @field::features_unique_id, - @field::features_parent_public_key, + @field::features_contract_id, @field::features_metadata)) ); let num_matching_outputs = covenant.execute(0, &input, &outputs).unwrap(); diff --git a/base_layer/core/src/covenants/decoder.rs b/base_layer/core/src/covenants/decoder.rs index becf41cfb2..3f5358fdfd 100644 --- a/base_layer/core/src/covenants/decoder.rs +++ b/base_layer/core/src/covenants/decoder.rs @@ -121,8 +121,9 @@ impl CovenantReadExt for R { #[cfg(test)] mod test { + use tari_common_types::types::FixedHash; use tari_test_utils::unpack_enum; - use tari_utilities::hex::{from_hex, to_hex}; + use tari_utilities::hex::{to_hex, Hex}; use super::*; use crate::{ @@ -165,13 +166,11 @@ mod test { #[test] fn it_decodes_from_well_formed_bytes() { - let hash = from_hex("53563b674ba8e5166adb57afa8355bcf2ee759941eef8f8959b802367c2558bd").unwrap(); - let mut hash_buf = [0u8; 32]; - hash_buf.copy_from_slice(hash.as_slice()); + let hash = FixedHash::from_hex("53563b674ba8e5166adb57afa8355bcf2ee759941eef8f8959b802367c2558bd").unwrap(); let mut bytes = Vec::new(); covenant!(fields_hashed_eq( @fields(@field::commitment, @field::features_metadata), - @hash(hash_buf), + @hash(hash), )) .write_to(&mut bytes) .unwrap(); diff --git a/base_layer/core/src/covenants/encoder.rs b/base_layer/core/src/covenants/encoder.rs index 419e4af1bc..00bca70b3c 100644 --- a/base_layer/core/src/covenants/encoder.rs +++ b/base_layer/core/src/covenants/encoder.rs @@ -54,6 +54,7 @@ impl CovenentWriteExt for W { #[cfg(test)] mod tests { + use tari_common_types::types::FixedHash; use super::*; use crate::{ @@ -83,7 +84,7 @@ mod tests { #[test] fn it_encodes_args_correctly() { - let dummy = [0u8; 32]; + let dummy = FixedHash::zero(); let covenant = covenant!(field_eq(@field::features, @hash(dummy))); let encoder = CovenantTokenEncoder::new(covenant.tokens()); let mut buf = Vec::::new(); diff --git a/base_layer/core/src/covenants/fields.rs b/base_layer/core/src/covenants/fields.rs index 9b99bbf72d..e0fe4f9d1d 100644 --- a/base_layer/core/src/covenants/fields.rs +++ b/base_layer/core/src/covenants/fields.rs @@ -39,7 +39,7 @@ use crate::{ encoder::CovenentWriteExt, error::CovenantError, }, - transactions::transaction_components::{TransactionInput, TransactionOutput}, + transactions::transaction_components::{SideChainFeatures, TransactionInput, TransactionOutput}, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -55,6 +55,8 @@ pub enum OutputField { FeaturesUniqueId = byte_codes::FIELD_FEATURES_UNIQUE_ID, FeaturesParentPublicKey = byte_codes::FIELD_FEATURES_PARENT_PUBLIC_KEY, FeaturesMetadata = byte_codes::FIELD_FEATURES_METADATA, + FeaturesSideChainFeatures = byte_codes::FIELD_FEATURES_SIDE_CHAIN_FEATURES, + FeaturesSideChainFeaturesContractId = byte_codes::FIELD_FEATURES_SIDE_CHAIN_FEATURES_CONTRACT_ID, } impl OutputField { @@ -72,6 +74,8 @@ impl OutputField { FIELD_FEATURES_MATURITY => Ok(FeaturesMaturity), FIELD_FEATURES_UNIQUE_ID => Ok(FeaturesUniqueId), FIELD_FEATURES_PARENT_PUBLIC_KEY => Ok(FeaturesParentPublicKey), + FIELD_FEATURES_SIDE_CHAIN_FEATURES => Ok(FeaturesSideChainFeatures), + FIELD_FEATURES_SIDE_CHAIN_FEATURES_CONTRACT_ID => Ok(FeaturesSideChainFeaturesContractId), FIELD_FEATURES_METADATA => Ok(FeaturesMetadata), _ => Err(CovenantDecodeError::UnknownByteCode { code: byte }), @@ -82,7 +86,7 @@ impl OutputField { self as u8 } - pub fn get_field_value_ref<'a, T: 'static>(self, output: &'a TransactionOutput) -> Option<&'a T> { + pub(super) fn get_field_value_ref<'a, T: 'static>(self, output: &'a TransactionOutput) -> Option<&'a T> { #[allow(clippy::enum_glob_use)] use OutputField::*; let val = match self { @@ -95,6 +99,16 @@ impl OutputField { FeaturesMaturity => &output.features.maturity as &dyn Any, FeaturesUniqueId => &output.features.unique_id as &dyn Any, FeaturesParentPublicKey => &output.features.parent_public_key as &dyn Any, + FeaturesSideChainFeatures => &output.features.sidechain_features as &dyn Any, + FeaturesSideChainFeaturesContractId => { + // This is tricky: in the Some case we return &FixedHash and None case we return ref to None from + // sidechain_features because there is no reference to Option for + // sidechain_features.contract_id to return. + match &output.features.sidechain_features { + Some(SideChainFeatures { ref contract_id, .. }) => contract_id as &dyn Any, + none => none as &dyn Any, + } + }, FeaturesMetadata => &output.features.metadata as &dyn Any, }; val.downcast_ref::() @@ -113,6 +127,8 @@ impl OutputField { FeaturesMaturity => output.features.maturity.to_consensus_bytes(), FeaturesUniqueId => output.features.unique_id.to_consensus_bytes(), FeaturesParentPublicKey => output.features.parent_public_key.to_consensus_bytes(), + FeaturesSideChainFeatures => output.features.sidechain_features.to_consensus_bytes(), + FeaturesSideChainFeaturesContractId => output.features.unique_asset_id().to_consensus_bytes(), FeaturesMetadata => output.features.metadata.to_consensus_bytes(), } } @@ -154,6 +170,14 @@ impl OutputField { .features() .map(|features| features.parent_public_key == output.features.parent_public_key) .unwrap_or(false), + FeaturesSideChainFeatures => input + .features() + .map(|features| features.sidechain_features == output.features.sidechain_features) + .unwrap_or(false), + FeaturesSideChainFeaturesContractId => input + .features() + .map(|features| features.contract_id() == output.features.contract_id()) + .unwrap_or(false), FeaturesMetadata => input .features() .map(|features| features.metadata == output.features.metadata) @@ -162,16 +186,23 @@ impl OutputField { } pub fn is_eq(self, output: &TransactionOutput, val: &T) -> Result { - use OutputField::{FeaturesParentPublicKey, FeaturesUniqueId}; + #[allow(clippy::enum_glob_use)] + use OutputField::*; match self { // Handle edge cases - FeaturesParentPublicKey | FeaturesUniqueId => match self.get_field_value_ref::>(output) { - Some(Some(field_val)) => Ok(field_val == val), - Some(None) => Ok(false), - None => Err(CovenantError::InvalidArgument { - filter: "is_eq", - details: format!("Invalid type for field {}", self), - }), + FeaturesParentPublicKey | FeaturesUniqueId | FeaturesSideChainFeatures => { + match self.get_field_value_ref::>(output) { + Some(Some(field_val)) => Ok(field_val == val), + Some(None) => Ok(false), + None => Err(CovenantError::InvalidArgument { + filter: "is_eq", + details: format!("Invalid type for field {}", self), + }), + } + }, + FeaturesSideChainFeaturesContractId => match self.get_field_value_ref::(output) { + Some(field_val) => Ok(field_val == val), + None => Ok(false), }, _ => match self.get_field_value_ref::(output) { Some(field_val) => Ok(field_val == val), @@ -229,6 +260,16 @@ impl OutputField { OutputField::FeaturesParentPublicKey } + #[allow(dead_code)] + pub fn features_contract_id() -> Self { + OutputField::FeaturesSideChainFeaturesContractId + } + + #[allow(dead_code)] + pub fn features_sidechain_features() -> Self { + OutputField::FeaturesSideChainFeatures + } + #[allow(dead_code)] pub fn features_metadata() -> Self { OutputField::FeaturesMetadata @@ -247,6 +288,8 @@ impl Display for OutputField { Features => write!(f, "field::features"), FeaturesFlags => write!(f, "field::features_flags"), FeaturesUniqueId => write!(f, "field::features_unique_id"), + FeaturesSideChainFeatures => write!(f, "field::features_sidechain_features"), + FeaturesSideChainFeaturesContractId => write!(f, "field::features_contract_id"), FeaturesMetadata => write!(f, "field::features_metadata"), FeaturesParentPublicKey => write!(f, "field::features_parent_public_key"), FeaturesMaturity => write!(f, "field::features_maturity"), @@ -333,7 +376,7 @@ impl FromIterator for OutputFields { #[cfg(test)] mod test { use rand::rngs::OsRng; - use tari_common_types::types::{Commitment, PublicKey}; + use tari_common_types::types::{Commitment, FixedHash, PublicKey}; use tari_crypto::keys::PublicKey as PublicKeyTrait; use tari_script::script; @@ -352,15 +395,15 @@ mod test { use super::*; mod is_eq { - use super::*; + use crate::transactions::transaction_components::SideChainFeatures; #[test] fn it_returns_true_if_eq() { let output = create_outputs(1, UtxoTestParams { features: OutputFeatures { parent_public_key: Some(Default::default()), - unique_id: Some(b"1234".to_vec()), + sidechain_features: Some(SideChainFeatures::new(FixedHash::zero())), ..Default::default() }, script: script![Drop Nop], @@ -378,14 +421,14 @@ mod test { assert!(OutputField::FeaturesFlags .is_eq(&output, &output.features.flags) .unwrap()); - assert!(OutputField::FeaturesParentPublicKey - .is_eq(&output, output.features.parent_public_key.as_ref().unwrap()) + assert!(OutputField::FeaturesSideChainFeatures + .is_eq(&output, output.features.sidechain_features.as_ref().unwrap()) .unwrap()); assert!(OutputField::FeaturesMetadata .is_eq(&output, &output.features.metadata) .unwrap()); - assert!(OutputField::FeaturesUniqueId - .is_eq(&output, output.features.unique_id.as_ref().unwrap()) + assert!(OutputField::FeaturesSideChainFeaturesContractId + .is_eq(&output, &output.features.contract_id().unwrap()) .unwrap()); assert!(OutputField::SenderOffsetPublicKey .is_eq(&output, &output.sender_offset_public_key) @@ -398,7 +441,7 @@ mod test { let output = create_outputs(1, UtxoTestParams { features: OutputFeatures { parent_public_key: Some(parent_pk), - unique_id: Some(b"1234".to_vec()), + sidechain_features: Some(SideChainFeatures::new(FixedHash::hash_bytes(b"A"))), ..Default::default() }, script: script![Drop Nop], @@ -418,8 +461,11 @@ mod test { assert!(!OutputField::FeaturesFlags .is_eq(&output, &OutputFlags::COINBASE_OUTPUT) .unwrap()); - assert!(!OutputField::FeaturesParentPublicKey - .is_eq(&output, &PublicKey::default()) + assert!(!OutputField::FeaturesSideChainFeatures + .is_eq(&output, &SideChainFeatures::new(FixedHash::hash_bytes(b"B"))) + .unwrap()); + assert!(!OutputField::FeaturesSideChainFeaturesContractId + .is_eq(&output, &FixedHash::hash_bytes(b"B")) .unwrap()); assert!(!OutputField::FeaturesMetadata.is_eq(&output, &vec![123u8]).unwrap()); assert!(!OutputField::FeaturesUniqueId.is_eq(&output, &vec![123u8]).unwrap()); @@ -467,6 +513,8 @@ mod test { assert!(OutputField::FeaturesMaturity.is_eq_input(&input, &output)); assert!(OutputField::FeaturesFlags.is_eq_input(&input, &output)); assert!(OutputField::FeaturesParentPublicKey.is_eq_input(&input, &output)); + assert!(OutputField::FeaturesSideChainFeatures.is_eq_input(&input, &output)); + assert!(OutputField::FeaturesSideChainFeaturesContractId.is_eq_input(&input, &output)); assert!(OutputField::FeaturesMetadata.is_eq_input(&input, &output)); assert!(OutputField::FeaturesUniqueId.is_eq_input(&input, &output)); assert!(OutputField::SenderOffsetPublicKey.is_eq_input(&input, &output)); @@ -480,6 +528,8 @@ mod test { OutputField::Features, OutputField::FeaturesFlags, OutputField::FeaturesUniqueId, + OutputField::FeaturesSideChainFeatures, + OutputField::FeaturesSideChainFeaturesContractId, OutputField::FeaturesMetadata, OutputField::FeaturesMaturity, OutputField::FeaturesParentPublicKey, diff --git a/base_layer/core/src/covenants/filters/and.rs b/base_layer/core/src/covenants/filters/and.rs index 4c32a704f1..b292863358 100644 --- a/base_layer/core/src/covenants/filters/and.rs +++ b/base_layer/core/src/covenants/filters/and.rs @@ -38,26 +38,28 @@ impl Filter for AndFilter { #[cfg(test)] mod test { + use tari_common_types::types::FixedHash; use super::*; use crate::{ covenant, covenants::{filters::test::setup_filter_test, test::create_input}, + transactions::transaction_components::SideChainFeatures, }; #[test] fn it_filters_outputset_using_intersection() { - let bytes = vec![0xab, 0xcd, 0xef]; - let covenant = covenant!(and(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::features_unique_id, @bytes(bytes.clone())))); + let hash = FixedHash::hash_bytes("A"); + let covenant = covenant!(and(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::features_contract_id, @hash(hash)))); let input = create_input(); let (mut context, outputs) = setup_filter_test(&covenant, &input, 0, |outputs| { outputs[5].features.maturity = 42; - outputs[5].features.unique_id = Some(bytes.clone()); + outputs[5].features.sidechain_features = Some(SideChainFeatures::new(hash)); outputs[7].features.maturity = 42; - outputs[7].features.unique_id = Some(bytes.clone()); + outputs[7].features.sidechain_features = Some(SideChainFeatures::new(hash)); // Does not have maturity = 42 outputs[8].features.maturity = 123; - outputs[8].features.unique_id = Some(bytes.clone()); + outputs[8].features.sidechain_features = Some(SideChainFeatures::new(hash)); }); let mut output_set = OutputSet::new(&outputs); diff --git a/base_layer/core/src/covenants/filters/field_eq.rs b/base_layer/core/src/covenants/filters/field_eq.rs index b002f8c0d4..840b8c530f 100644 --- a/base_layer/core/src/covenants/filters/field_eq.rs +++ b/base_layer/core/src/covenants/filters/field_eq.rs @@ -103,7 +103,7 @@ mod test { fn it_filters_sender_offset_public_key() { let pk = PublicKey::from_hex("5615a327e1d19da34e5aa8bbd2ecc97addf29b158844b885bfc4efa0dab17052").unwrap(); let covenant = covenant!(field_eq( - @field::features_parent_public_key, + @field::sender_offset_public_key, @public_key(pk.clone()) )); let input = create_input(); @@ -111,15 +111,12 @@ mod test { // Remove `field_eq` context.next_filter().unwrap(); let mut outputs = create_outputs(10, Default::default()); - outputs[5].features.parent_public_key = Some(pk.clone()); + outputs[5].sender_offset_public_key = pk.clone(); let mut output_set = OutputSet::new(&outputs); FieldEqFilter.filter(&mut context, &mut output_set).unwrap(); assert_eq!(output_set.len(), 1); - assert_eq!( - *output_set.get(5).unwrap().features.parent_public_key.as_ref().unwrap(), - pk - ); + assert_eq!(output_set.get(5).unwrap().sender_offset_public_key, pk); } #[test] diff --git a/base_layer/core/src/covenants/filters/fields_hashed_eq.rs b/base_layer/core/src/covenants/filters/fields_hashed_eq.rs index 510c69bed7..e75ce72290 100644 --- a/base_layer/core/src/covenants/filters/fields_hashed_eq.rs +++ b/base_layer/core/src/covenants/filters/fields_hashed_eq.rs @@ -33,7 +33,7 @@ impl Filter for FieldsHashedEqFilter { let hash = context.next_arg()?.require_hash()?; output_set.retain(|output| { let challenge = fields.construct_challenge_from(output); - Ok(challenge.finalize()[..] == hash) + Ok(challenge.finalize()[..] == *hash) })?; Ok(()) } @@ -41,27 +41,27 @@ impl Filter for FieldsHashedEqFilter { #[cfg(test)] mod test { - use tari_common_types::types::Challenge; + use tari_common_types::types::{Challenge, FixedHash}; use super::*; use crate::{ consensus::ToConsensusBytes, covenant, covenants::{filters::test::setup_filter_test, test::create_input}, - transactions::transaction_components::OutputFeatures, + transactions::transaction_components::{OutputFeatures, SideChainFeatures}, }; #[test] fn it_filters_outputs_with_fields_that_hash_to_given_hash() { let features = OutputFeatures { maturity: 42, - unique_id: Some(vec![0xab, 0xcd, 0xef]), + sidechain_features: Some(SideChainFeatures::new(FixedHash::hash_bytes("A"))), ..Default::default() }; let hashed = Challenge::new().chain(features.to_consensus_bytes()).finalize(); let mut hash = [0u8; 32]; hash.copy_from_slice(hashed.as_slice()); - let covenant = covenant!(fields_hashed_eq(@fields(@field::features), @hash(hash))); + let covenant = covenant!(fields_hashed_eq(@fields(@field::features), @hash(hash.into()))); let input = create_input(); let (mut context, outputs) = setup_filter_test(&covenant, &input, 0, |outputs| { outputs[5].features = features.clone(); diff --git a/base_layer/core/src/covenants/filters/fields_preserved.rs b/base_layer/core/src/covenants/filters/fields_preserved.rs index 0872df308b..adf12ef514 100644 --- a/base_layer/core/src/covenants/filters/fields_preserved.rs +++ b/base_layer/core/src/covenants/filters/fields_preserved.rs @@ -36,38 +36,39 @@ impl Filter for FieldsPreservedFilter { #[cfg(test)] mod test { + use tari_common_types::types::FixedHash; use super::*; use crate::{ covenant, covenants::{filters::test::setup_filter_test, test::create_input}, - transactions::transaction_components::OutputFlags, + transactions::transaction_components::{OutputFlags, SideChainFeatures}, }; #[test] fn it_filters_outputs_that_match_input_fields() { - let bytes = vec![0xab, 0xcd, 0xef]; - let covenant = covenant!(fields_preserved(@fields(@field::features_maturity, @field::features_unique_id, @field::features_flags))); + let hash = FixedHash::hash_bytes("A"); + let covenant = covenant!(fields_preserved(@fields(@field::features_maturity, @field::features_contract_id, @field::features_flags))); let mut input = create_input(); input.set_maturity(42).unwrap(); - input.features_mut().unwrap().unique_id = Some(bytes.clone()); + input.features_mut().unwrap().sidechain_features = Some(SideChainFeatures::new(hash)); input.features_mut().unwrap().flags = OutputFlags::SIDECHAIN_CHECKPOINT; let (mut context, outputs) = setup_filter_test(&covenant, &input, 0, |outputs| { outputs[5].features.maturity = 42; - outputs[5].features.unique_id = Some(bytes.clone()); + outputs[5].features.sidechain_features = Some(SideChainFeatures::new(hash)); outputs[5].features.flags = OutputFlags::SIDECHAIN_CHECKPOINT; outputs[7].features.maturity = 42; outputs[7].features.flags = OutputFlags::SIDECHAIN_CHECKPOINT; - outputs[7].features.unique_id = Some(vec![0x01, 0x02]); + outputs[7].features.sidechain_features = Some(SideChainFeatures::new(FixedHash::hash_bytes("B"))); outputs[8].features.maturity = 42; - outputs[8].features.unique_id = Some(bytes.clone()); + outputs[8].features.sidechain_features = Some(SideChainFeatures::new(hash)); outputs[8].features.flags = OutputFlags::SIDECHAIN_CHECKPOINT | OutputFlags::COINBASE_OUTPUT; }); let mut output_set = OutputSet::new(&outputs); FieldsPreservedFilter.filter(&mut context, &mut output_set).unwrap(); - assert_eq!(output_set.len(), 1); assert_eq!(output_set.get_selected_indexes(), vec![5]); + assert_eq!(output_set.len(), 1); } } diff --git a/base_layer/core/src/covenants/filters/not.rs b/base_layer/core/src/covenants/filters/not.rs index 5c02587728..6b518e45de 100644 --- a/base_layer/core/src/covenants/filters/not.rs +++ b/base_layer/core/src/covenants/filters/not.rs @@ -37,23 +37,25 @@ impl Filter for NotFilter { #[cfg(test)] mod test { + use tari_common_types::types::FixedHash; use super::*; use crate::{ covenant, covenants::{filters::test::setup_filter_test, test::create_input}, + transactions::transaction_components::SideChainFeatures, }; #[test] fn it_filters_compliment_of_filter() { - let bytes = vec![0xab, 0xcd, 0xef]; - let covenant = covenant!(not(or(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::features_unique_id, @bytes(bytes.clone()))))); + let hash = FixedHash::hash_bytes("A"); + let covenant = covenant!(not(or(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::features_contract_id, @hash(hash))))); let input = create_input(); let (mut context, outputs) = setup_filter_test(&covenant, &input, 0, |outputs| { outputs[5].features.maturity = 42; - outputs[5].features.unique_id = Some(bytes.clone()); + outputs[5].features.sidechain_features = Some(SideChainFeatures::new(hash)); outputs[7].features.maturity = 42; - outputs[8].features.unique_id = Some(bytes.clone()); + outputs[8].features.sidechain_features = Some(SideChainFeatures::new(hash)); }); let mut output_set = OutputSet::new(&outputs); NotFilter.filter(&mut context, &mut output_set).unwrap(); diff --git a/base_layer/core/src/covenants/filters/or.rs b/base_layer/core/src/covenants/filters/or.rs index 53a8ba053b..f5e41ad3a8 100644 --- a/base_layer/core/src/covenants/filters/or.rs +++ b/base_layer/core/src/covenants/filters/or.rs @@ -42,23 +42,25 @@ impl Filter for OrFilter { #[cfg(test)] mod test { + use tari_common_types::types::FixedHash; use super::*; use crate::{ covenant, covenants::{filters::test::setup_filter_test, test::create_input}, + transactions::transaction_components::SideChainFeatures, }; #[test] fn it_filters_outputset_using_union() { - let bytes = vec![0xab, 0xcd, 0xef]; - let covenant = covenant!(or(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::features_unique_id, @bytes(bytes.clone())))); + let hash = FixedHash::hash_bytes("A"); + let covenant = covenant!(or(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::features_contract_id, @hash(hash)))); let input = create_input(); let (mut context, outputs) = setup_filter_test(&covenant, &input, 0, |outputs| { outputs[5].features.maturity = 42; - outputs[5].features.unique_id = Some(bytes.clone()); + outputs[5].features.sidechain_features = Some(SideChainFeatures::new(hash)); outputs[7].features.maturity = 42; - outputs[8].features.unique_id = Some(bytes.clone()); + outputs[8].features.sidechain_features = Some(SideChainFeatures::new(hash)); }); let mut output_set = OutputSet::new(&outputs); OrFilter.filter(&mut context, &mut output_set).unwrap(); diff --git a/base_layer/core/src/covenants/filters/output_hash_eq.rs b/base_layer/core/src/covenants/filters/output_hash_eq.rs index a1a292dc0a..3dfa73f90c 100644 --- a/base_layer/core/src/covenants/filters/output_hash_eq.rs +++ b/base_layer/core/src/covenants/filters/output_hash_eq.rs @@ -31,7 +31,7 @@ impl Filter for OutputHashEqFilter { fn filter(&self, context: &mut CovenantContext<'_>, output_set: &mut OutputSet<'_>) -> Result<(), CovenantError> { let hash = context.next_arg()?.require_hash()?; // An output's hash is unique so the output set is either 1 or 0 outputs will match - output_set.find_inplace(|output| output.hash() == hash); + output_set.find_inplace(|output| *output.hash().as_slice() == hash); Ok(()) } } @@ -54,7 +54,7 @@ mod test { let output_hash = output.hash(); let mut hash = [0u8; 32]; hash.copy_from_slice(output_hash.as_slice()); - let covenant = covenant!(output_hash_eq(@hash(hash))); + let covenant = covenant!(output_hash_eq(@hash(hash.into()))); let input = create_input(); let (mut context, outputs) = setup_filter_test(&covenant, &input, 0, move |outputs| { outputs.insert(5, output); diff --git a/base_layer/core/src/covenants/filters/xor.rs b/base_layer/core/src/covenants/filters/xor.rs index d587411941..ec429b1bbd 100644 --- a/base_layer/core/src/covenants/filters/xor.rs +++ b/base_layer/core/src/covenants/filters/xor.rs @@ -42,23 +42,25 @@ impl Filter for XorFilter { #[cfg(test)] mod test { + use tari_common_types::types::FixedHash; use super::*; use crate::{ covenant, covenants::{filters::test::setup_filter_test, test::create_input}, + transactions::transaction_components::SideChainFeatures, }; #[test] fn it_filters_outputset_using_symmetric_difference() { - let bytes = vec![0xab, 0xcd, 0xef]; - let covenant = covenant!(or(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::features_unique_id, @bytes(bytes.clone())))); + let hash = FixedHash::hash_bytes("A"); + let covenant = covenant!(or(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::features_contract_id, @hash(hash)))); let input = create_input(); let (mut context, outputs) = setup_filter_test(&covenant, &input, 0, |outputs| { outputs[5].features.maturity = 42; - outputs[5].features.unique_id = Some(bytes.clone()); + outputs[5].features.sidechain_features = Some(SideChainFeatures::new(hash)); outputs[7].features.maturity = 42; - outputs[8].features.unique_id = Some(bytes.clone()); + outputs[8].features.sidechain_features = Some(SideChainFeatures::new(hash)); }); let mut output_set = OutputSet::new(&outputs); XorFilter.filter(&mut context, &mut output_set).unwrap(); diff --git a/base_layer/core/src/covenants/macros.rs b/base_layer/core/src/covenants/macros.rs index 247a9020db..1b423bccc4 100644 --- a/base_layer/core/src/covenants/macros.rs +++ b/base_layer/core/src/covenants/macros.rs @@ -125,7 +125,7 @@ macro_rules! __covenant_inner { #[cfg(test)] mod test { - use tari_common_types::types::PublicKey; + use tari_common_types::types::{FixedHash, PublicKey}; use tari_script::script; use tari_test_utils::unpack_enum; use tari_utilities::hex::{from_hex, Hex}; @@ -155,14 +155,14 @@ mod test { let hash_vec = from_hex(hash_str).unwrap(); let mut hash = [0u8; 32]; hash.copy_from_slice(hash_vec.as_slice()); - let covenant = covenant!(output_hash_eq(@hash(hash))); + let covenant = covenant!(output_hash_eq(@hash(hash.into()))); assert_eq!(covenant.to_bytes().to_hex(), format!("3001{}", hash_str)); let covenant = covenant!(and( identity(), or( identity(), - fields_preserved(@hash(hash),) + fields_preserved(@hash(hash.into()),) ) )); assert_eq!( @@ -186,22 +186,25 @@ mod test { identity(), fields_hashed_eq( @fields(@field::commitment, @field::features_metadata), - @hash(hash), + @hash(hash.into()), ), ), field_eq(@field::features_maturity, @uint(42)) )); assert_eq!( covenant.to_bytes().to_hex(), - "21222032080200090153563b674ba8e5166adb57afa8355bcf2ee759941eef8f8959b802367c2558bd330706062a" + "21222032080200070153563b674ba8e5166adb57afa8355bcf2ee759941eef8f8959b802367c2558bd330706062a" ); } #[test] fn covenant() { - let bytes = vec![0xba, 0xda, 0x55]; - let covenant = covenant!(field_eq(@field::covenant, @covenant_lit(and(field_eq(@field::features_unique_id, @bytes(bytes), identity()))))); - assert_eq!(covenant.to_bytes().to_hex(), "330703050a213307070903bada5520"); + let hash = FixedHash::zero(); + let covenant = covenant!(field_eq(@field::covenant, @covenant_lit(and(field_eq(@field::features_contract_id, @hash(hash), identity()))))); + assert_eq!( + covenant.to_bytes().to_hex(), + "33070305262133070901000000000000000000000000000000000000000000000000000000000000000020" + ); } #[test] diff --git a/base_layer/core/src/covenants/token.rs b/base_layer/core/src/covenants/token.rs index 52b054d2c6..028316bad3 100644 --- a/base_layer/core/src/covenants/token.rs +++ b/base_layer/core/src/covenants/token.rs @@ -22,11 +22,11 @@ use std::{collections::VecDeque, io, iter::FromIterator}; -use tari_common_types::types::{Commitment, PublicKey}; +use tari_common_types::types::{Commitment, FixedHash, PublicKey}; use tari_script::TariScript; use crate::covenants::{ - arguments::{CovenantArg, Hash}, + arguments::CovenantArg, decoder::{CovenantDecodeError, CovenantReadExt}, fields::OutputField, filters::{ @@ -145,7 +145,7 @@ impl CovenantToken { } #[allow(dead_code)] - pub fn hash(hash: Hash) -> Self { + pub fn hash(hash: FixedHash) -> Self { CovenantArg::Hash(hash).into() } diff --git a/base_layer/core/src/proto/transaction.proto b/base_layer/core/src/proto/transaction.proto index c804b29546..5f5b35cb34 100644 --- a/base_layer/core/src/proto/transaction.proto +++ b/base_layer/core/src/proto/transaction.proto @@ -79,32 +79,77 @@ message TransactionOutput { // Options for UTXOs message OutputFeatures { + // Version + uint32 version = 1; // Flags are the feature flags that differentiate between outputs, eg Coinbase all of which has different rules - uint32 flags = 1; + uint32 flags = 2; // The maturity of the specific UTXO. This is the min lock height at which an UTXO can be spend. Coinbase UTXO // require a min maturity of the Coinbase_lock_height, this should be checked on receiving new blocks. - uint64 maturity = 2; + uint64 maturity = 3; + bytes metadata = 4; + // The recovery byte - not consensus critical - can help reduce the bandwidth with wallet recovery or in other + // instances when a wallet needs to request the complete UTXO set from a base node. + uint32 recovery_byte = 5; + SideChainFeatures sidechain_features = 6; - bytes metadata = 3; - AssetOutputFeatures asset = 4; + AssetOutputFeatures asset = 7; + bytes unique_id = 8; + bytes parent_public_key = 9; + MintNonFungibleFeatures mint_non_fungible = 10; + SideChainCheckpointFeatures sidechain_checkpoint = 11; + CommitteeDefinitionFeatures committee_definition = 13; +} - bytes unique_id = 5; +message SideChainFeatures { + bytes contract_id = 1; + ContractConstitution constitution = 3; +} - bytes parent_public_key = 6; +message ContractConstitution { + CommitteeMembers validator_committee = 1; + ContractAcceptanceRequirements acceptance_requirements = 2; + SideChainConsensus consensus = 3; + CheckpointParameters checkpoint_params = 4; + ConstitutionChangeRules constitution_change_rules = 5; + uint64 initial_reward = 6; +} - MintNonFungibleFeatures mint_non_fungible = 7; +message ContractAcceptanceRequirements { + uint64 acceptance_period_expiry = 1; + uint32 minimum_quorum_required = 2; +} - SideChainCheckpointFeatures sidechain_checkpoint = 8; - // Version - uint32 version = 9; - CommitteeDefinitionFeatures committee_definition = 10; +message CommitteeMembers { + repeated bytes members = 1; +} - // The recovery byte - not consensus critical - can help reduce the bandwidth with wallet recovery or in other - // instances when a wallet needs to request the complete UTXO set from a base node. - uint32 recovery_byte = 11; +message CheckpointParameters { + uint64 abandoned_interval = 1; + uint32 minimum_quorum_required = 2; +} + +message ConstitutionChangeRules { + uint32 change_flags = 1; + RequirementsForConstitutionChange requirements_for_constitution_change = 2; +} + +message RequirementsForConstitutionChange { + // The minimum required constitution committee signatures required for a constitution change proposal to pass. + uint32 minimum_constitution_committee_signatures = 1; + // An allowlist of keys that are able to accept and ratify the initial constitution and its amendments. If this is + // None, the constitution cannot be amended. + CommitteeMembers constitution_committee = 2; +} + +enum SideChainConsensus { + UNSPECIFIED = 0; + BFT = 1; + PROOF_OF_WORK = 2; + MERKLE_ROOT = 3; } +// TODO: deprecated message AssetOutputFeatures { bytes public_key = 1; repeated uint32 template_ids_implemented = 2; diff --git a/base_layer/core/src/proto/transaction.rs b/base_layer/core/src/proto/transaction.rs index 49a373bf23..39e72a3c69 100644 --- a/base_layer/core/src/proto/transaction.rs +++ b/base_layer/core/src/proto/transaction.rs @@ -40,13 +40,22 @@ use crate::{ tari_amount::MicroTari, transaction_components::{ AssetOutputFeatures, + CheckpointParameters, CommitteeDefinitionFeatures, + CommitteeMembers, + ConstitutionChangeFlags, + ConstitutionChangeRules, + ContractAcceptanceRequirements, + ContractConstitution, KernelFeatures, MintNonFungibleFeatures, OutputFeatures, OutputFeaturesVersion, OutputFlags, + RequirementsForConstitutionChange, SideChainCheckpointFeatures, + SideChainConsensus, + SideChainFeatures, TemplateParameter, Transaction, TransactionInput, @@ -281,8 +290,12 @@ impl TryFrom for OutputFeatures { } else { Some(PublicKey::from_bytes(features.parent_public_key.as_bytes()).map_err(|err| format!("{:?}", err))?) }; + let sidechain_features = features + .sidechain_features + .map(SideChainFeatures::try_from) + .transpose()?; - let flags = u16::try_from(features.flags).map_err(|_| "Invalid output flags: overflowed u8")?; + let flags = u16::try_from(features.flags).map_err(|_| "Invalid output flags: overflowed u16")?; Ok(OutputFeatures::new( OutputFeaturesVersion::try_from( @@ -293,17 +306,12 @@ impl TryFrom for OutputFeatures { u8::try_from(features.recovery_byte).map_err(|_| "Invalid recovery byte: overflowed u8")?, features.metadata, unique_id, + sidechain_features, parent_public_key, - match features.asset { - Some(a) => Some(a.try_into()?), - None => None, - }, - match features.mint_non_fungible { - Some(m) => Some(m.try_into()?), - None => None, - }, - features.sidechain_checkpoint.map(|s| s.try_into()).transpose()?, - features.committee_definition.map(|c| c.try_into()).transpose()?, + None, + None, + None, + None, )) } } @@ -325,10 +333,242 @@ impl From for proto::types::OutputFeatures { version: features.version as u32, committee_definition: features.committee_definition.map(|c| c.into()), recovery_byte: u32::from(features.recovery_byte), + sidechain_features: features.sidechain_features.map(Into::into), } } } +//---------------------------------- SideChainFeatures --------------------------------------------// +impl From for proto::types::SideChainFeatures { + fn from(value: SideChainFeatures) -> Self { + Self { + contract_id: value.contract_id.to_vec(), + constitution: value.constitution.map(Into::into), + } + } +} + +impl TryFrom for SideChainFeatures { + type Error = String; + + fn try_from(features: proto::types::SideChainFeatures) -> Result { + let constitution = features.constitution.map(ContractConstitution::try_from).transpose()?; + + Ok(Self { + contract_id: features.contract_id.try_into().map_err(|_| "Invalid contract_id")?, + constitution, + }) + } +} + +//---------------------------------- ContractConstitution --------------------------------------------// +impl From for proto::types::ContractConstitution { + fn from(value: ContractConstitution) -> Self { + Self { + validator_committee: Some(value.validator_committee.into()), + acceptance_requirements: Some(value.acceptance_requirements.into()), + consensus: value.consensus.into(), + checkpoint_params: Some(value.checkpoint_params.into()), + constitution_change_rules: Some(value.constitution_change_rules.into()), + initial_reward: value.initial_reward.into(), + } + } +} + +impl TryFrom for ContractConstitution { + type Error = String; + + fn try_from(value: proto::types::ContractConstitution) -> Result { + use num_traits::FromPrimitive; + let validator_committee = value + .validator_committee + .map(TryInto::try_into) + .ok_or("validator_committee not provided")??; + let acceptance_requirements = value + .acceptance_requirements + .map(TryInto::try_into) + .ok_or("acceptance_requirements not provided")??; + let consensus = SideChainConsensus::from_i32(value.consensus).ok_or("Invalid SideChainConsensus")?; + let checkpoint_params = value + .checkpoint_params + .map(TryInto::try_into) + .ok_or("checkpoint_params not provided")??; + let constitution_change_rules = value + .constitution_change_rules + .map(TryInto::try_into) + .ok_or("constitution_change_rules not provided")??; + let initial_reward = value.initial_reward.into(); + + Ok(Self { + validator_committee, + acceptance_requirements, + consensus, + checkpoint_params, + constitution_change_rules, + initial_reward, + }) + } +} + +//---------------------------------- ContractAcceptanceRequirements --------------------------------------------// +impl From for proto::types::ContractAcceptanceRequirements { + fn from(value: ContractAcceptanceRequirements) -> Self { + Self { + acceptance_period_expiry: value.acceptance_period_expiry, + minimum_quorum_required: value.minimum_quorum_required, + } + } +} + +impl TryFrom for ContractAcceptanceRequirements { + type Error = String; + + fn try_from(value: proto::types::ContractAcceptanceRequirements) -> Result { + Ok(Self { + acceptance_period_expiry: value.acceptance_period_expiry, + minimum_quorum_required: value.minimum_quorum_required, + }) + } +} + +//---------------------------------- SideChainConsensus --------------------------------------------// +impl From for proto::types::SideChainConsensus { + fn from(value: SideChainConsensus) -> Self { + #[allow(clippy::enum_glob_use)] + use proto::types::SideChainConsensus::*; + match value { + SideChainConsensus::Bft => Bft, + SideChainConsensus::ProofOfWork => ProofOfWork, + SideChainConsensus::MerkleRoot => MerkleRoot, + } + } +} + +impl TryFrom for SideChainConsensus { + type Error = String; + + fn try_from(value: proto::types::SideChainConsensus) -> Result { + #[allow(clippy::enum_glob_use)] + use proto::types::SideChainConsensus::*; + match value { + Unspecified => Err("Side chain consensus not specified or invalid".to_string()), + Bft => Ok(SideChainConsensus::Bft), + ProofOfWork => Ok(SideChainConsensus::ProofOfWork), + MerkleRoot => Ok(SideChainConsensus::MerkleRoot), + } + } +} + +//---------------------------------- CheckpointParameters --------------------------------------------// +impl From for proto::types::CheckpointParameters { + fn from(value: CheckpointParameters) -> Self { + Self { + minimum_quorum_required: value.minimum_quorum_required, + abandoned_interval: value.abandoned_interval, + } + } +} + +impl TryFrom for CheckpointParameters { + type Error = String; + + fn try_from(value: proto::types::CheckpointParameters) -> Result { + Ok(Self { + minimum_quorum_required: value.minimum_quorum_required, + abandoned_interval: value.abandoned_interval, + }) + } +} + +//---------------------------------- ConstitutionChangeRules --------------------------------------------// +impl From for proto::types::ConstitutionChangeRules { + fn from(value: ConstitutionChangeRules) -> Self { + Self { + change_flags: value.change_flags.bits().into(), + requirements_for_constitution_change: value.requirements_for_constitution_change.map(Into::into), + } + } +} + +impl TryFrom for ConstitutionChangeRules { + type Error = String; + + fn try_from(value: proto::types::ConstitutionChangeRules) -> Result { + Ok(Self { + change_flags: u8::try_from(value.change_flags) + .ok() + .and_then(ConstitutionChangeFlags::from_bits) + .ok_or("Invalid change_flags")?, + requirements_for_constitution_change: value + .requirements_for_constitution_change + .map(RequirementsForConstitutionChange::try_from) + .transpose()?, + }) + } +} + +//---------------------------------- RequirementsForConstitutionChange --------------------------------------------// +impl From for proto::types::RequirementsForConstitutionChange { + fn from(value: RequirementsForConstitutionChange) -> Self { + Self { + minimum_constitution_committee_signatures: value.minimum_constitution_committee_signatures, + constitution_committee: value.constitution_committee.map(Into::into), + } + } +} + +impl TryFrom for RequirementsForConstitutionChange { + type Error = String; + + fn try_from(value: proto::types::RequirementsForConstitutionChange) -> Result { + Ok(Self { + minimum_constitution_committee_signatures: value.minimum_constitution_committee_signatures, + constitution_committee: value + .constitution_committee + .map(CommitteeMembers::try_from) + .transpose()?, + }) + } +} + +//---------------------------------- CommitteeMembers --------------------------------------------// +impl From for proto::types::CommitteeMembers { + fn from(value: CommitteeMembers) -> Self { + Self { + members: value.members().iter().map(|pk| pk.to_vec()).collect(), + } + } +} + +impl TryFrom for CommitteeMembers { + type Error = String; + + fn try_from(value: proto::types::CommitteeMembers) -> Result { + if value.members.len() > CommitteeMembers::MAX_MEMBERS { + return Err(format!( + "Too many committee members: expected {} but got {}", + CommitteeMembers::MAX_MEMBERS, + value.members.len() + )); + } + + let members = value + .members + .iter() + .enumerate() + .map(|(i, c)| { + PublicKey::from_bytes(c) + .map_err(|err| format!("committee member #{} was not a valid public key: {}", i + 1, err)) + }) + .collect::, _>>()?; + + let members = CommitteeMembers::try_from(members).map_err(|e| e.to_string())?; + Ok(members) + } +} + +// TODO: deprecated + impl TryFrom for AssetOutputFeatures { type Error = String; diff --git a/base_layer/core/src/transactions/transaction_components/error.rs b/base_layer/core/src/transactions/transaction_components/error.rs index 83090d6f49..7cc4c7f2d1 100644 --- a/base_layer/core/src/transactions/transaction_components/error.rs +++ b/base_layer/core/src/transactions/transaction_components/error.rs @@ -71,6 +71,8 @@ pub enum TransactionError { CovenantError(String), #[error("Consensus encoding error: {0}")] ConsensusEncodingError(String), + #[error("Committee contains too many members: contains {len} members but maximum is {max}")] + InvalidCommitteeLength { len: usize, max: usize }, } impl From for TransactionError { diff --git a/base_layer/core/src/transactions/transaction_components/mod.rs b/base_layer/core/src/transactions/transaction_components/mod.rs index 86b246d3b7..c36bcd8f28 100644 --- a/base_layer/core/src/transactions/transaction_components/mod.rs +++ b/base_layer/core/src/transactions/transaction_components/mod.rs @@ -35,7 +35,7 @@ pub use output_features::OutputFeatures; pub use output_features_version::OutputFeaturesVersion; pub use output_flags::OutputFlags; pub use rewind_result::RewindResult; -pub use side_chain::ContractConstitution; +pub use side_chain::{ContractConstitution, *}; pub use side_chain_checkpoint_features::SideChainCheckpointFeatures; use tari_common_types::types::Commitment; use tari_script::TariScript; 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 b1e4b9e0bb..f8ab4ff8b2 100644 --- a/base_layer/core/src/transactions/transaction_components/output_features.rs +++ b/base_layer/core/src/transactions/transaction_components/output_features.rs @@ -42,6 +42,7 @@ use crate::{ consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, MaxSizeBytes}, transactions::{ transaction_components::{ + side_chain::SideChainFeatures, AssetOutputFeatures, CommitteeDefinitionFeatures, MintNonFungibleFeatures, @@ -67,12 +68,10 @@ pub struct OutputFeatures { #[serde(default)] pub recovery_byte: u8, pub metadata: Vec, - // TODO: Add these fields - // pub contract_id: Option, - // pub constitution: Option, - - // Deprecated fields + pub sidechain_features: Option, pub unique_id: Option>, + + // TODO: Deprecated pub parent_public_key: Option, pub asset: Option, pub mint_non_fungible: Option, @@ -88,6 +87,8 @@ impl OutputFeatures { recovery_byte: u8, metadata: Vec, unique_id: Option>, + sidechain_features: Option, + // TODO: Deprecated parent_public_key: Option, asset: Option, mint_non_fungible: Option, @@ -101,6 +102,8 @@ impl OutputFeatures { recovery_byte, metadata, unique_id, + sidechain_features, + // Deprecated parent_public_key, asset, mint_non_fungible, @@ -115,6 +118,8 @@ impl OutputFeatures { recovery_byte: u8, metadata: Vec, unique_id: Option>, + sidechain_features: Option, + // TODO: Deprecated parent_public_key: Option, asset: Option, mint_non_fungible: Option, @@ -128,6 +133,8 @@ impl OutputFeatures { recovery_byte, metadata, unique_id, + sidechain_features, + // TODO: Deprecated parent_public_key, asset, mint_non_fungible, @@ -302,6 +309,10 @@ impl OutputFeatures { let recovery_byte = buf[0] as u8; Ok(recovery_byte) } + + pub fn contract_id(&self) -> Option { + self.sidechain_features.as_ref().map(|f| f.contract_id) + } } impl ConsensusEncoding for OutputFeatures { @@ -317,6 +328,7 @@ impl ConsensusEncoding for OutputFeatures { } self.parent_public_key.consensus_encode(writer)?; self.unique_id.consensus_encode(writer)?; + self.sidechain_features.consensus_encode(writer)?; self.asset.consensus_encode(writer)?; self.mint_non_fungible.consensus_encode(writer)?; self.sidechain_checkpoint.consensus_encode(writer)?; @@ -347,6 +359,7 @@ impl ConsensusDecoding for OutputFeatures { let parent_public_key = as ConsensusDecoding>::consensus_decode(reader)?; const MAX_UNIQUE_ID_SIZE: usize = 256; let unique_id = > as ConsensusDecoding>::consensus_decode(reader)?; + let sidechain_features = as ConsensusDecoding>::consensus_decode(reader)?; let asset = as ConsensusDecoding>::consensus_decode(reader)?; let mint_non_fungible = as ConsensusDecoding>::consensus_decode(reader)?; let sidechain_checkpoint = @@ -366,6 +379,7 @@ impl ConsensusDecoding for OutputFeatures { recovery_byte, parent_public_key, unique_id: unique_id.map(Into::into), + sidechain_features, asset, mint_non_fungible, sidechain_checkpoint, @@ -377,7 +391,19 @@ impl ConsensusDecoding for OutputFeatures { impl Default for OutputFeatures { fn default() -> Self { - OutputFeatures::new_current_version(OutputFlags::empty(), 0, 0, vec![], None, None, None, None, None, None) + OutputFeatures::new_current_version( + OutputFlags::empty(), + 0, + 0, + vec![], + None, + None, + None, + None, + None, + None, + None, + ) } } @@ -405,10 +431,24 @@ impl Display for OutputFeatures { #[cfg(test)] mod test { - use std::{io::ErrorKind, iter}; + use std::{convert::TryInto, io::ErrorKind, iter}; use super::*; - use crate::consensus::check_consensus_encoding_correctness; + use crate::{ + consensus::check_consensus_encoding_correctness, + transactions::transaction_components::{ + side_chain::{ + CheckpointParameters, + CommitteeMembers, + ConstitutionChangeFlags, + ConstitutionChangeRules, + ContractAcceptanceRequirements, + RequirementsForConstitutionChange, + SideChainConsensus, + }, + ContractConstitution, + }, + }; fn make_fully_populated_output_features(version: OutputFeaturesVersion) -> OutputFeatures { OutputFeatures { @@ -421,6 +461,36 @@ mod test { }, metadata: vec![1; 1024], unique_id: Some(vec![0u8; 256]), + sidechain_features: Some(SideChainFeatures { + contract_id: FixedHash::zero(), + constitution: Some(ContractConstitution { + validator_committee: vec![PublicKey::default(); CommitteeMembers::MAX_MEMBERS] + .try_into() + .unwrap(), + acceptance_requirements: ContractAcceptanceRequirements { + acceptance_period_expiry: 100, + minimum_quorum_required: 5, + }, + consensus: SideChainConsensus::MerkleRoot, + checkpoint_params: CheckpointParameters { + minimum_quorum_required: 5, + abandoned_interval: 100, + }, + constitution_change_rules: ConstitutionChangeRules { + change_flags: ConstitutionChangeFlags::all(), + requirements_for_constitution_change: Some(RequirementsForConstitutionChange { + minimum_constitution_committee_signatures: 5, + constitution_committee: Some( + vec![PublicKey::default(); CommitteeMembers::MAX_MEMBERS] + .try_into() + .unwrap(), + ), + }), + }, + initial_reward: 100.into(), + }), + }), + // Deprecated parent_public_key: Some(PublicKey::default()), asset: Some(AssetOutputFeatures { public_key: Default::default(), @@ -464,6 +534,7 @@ mod test { fn it_encodes_and_decodes_correctly_in_none_case() { let mut subject = make_fully_populated_output_features(OutputFeaturesVersion::V1); subject.unique_id = None; + subject.sidechain_features = None; subject.asset = None; subject.mint_non_fungible = None; subject.sidechain_checkpoint = None; @@ -549,7 +620,7 @@ mod test { #[test] fn test_for_checkpoint() { let unique_id = vec![7, 2, 3, 4]; - let hash = [13; 32].into(); + let hash = FixedHash::hash_bytes("MERKLE"); let committee = vec![PublicKey::default()]; // Initial assert_eq!( diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/committee_members.rs b/base_layer/core/src/transactions/transaction_components/side_chain/committee_members.rs index 5c85889d3e..af6b453885 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/committee_members.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/committee_members.rs @@ -20,12 +20,18 @@ // 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 std::io; +use std::{ + convert::{TryFrom, TryInto}, + io, +}; use serde::{Deserialize, Serialize}; use tari_common_types::types::PublicKey; -use crate::consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, MaxSizeVec}; +use crate::{ + consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, MaxSizeVec}, + transactions::transaction_components::TransactionError, +}; #[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq, Default)] pub struct CommitteeMembers { @@ -38,6 +44,25 @@ impl CommitteeMembers { pub fn new(members: MaxSizeVec) -> Self { Self { members } } + + pub fn members(&self) -> &[PublicKey] { + &self.members + } +} + +impl TryFrom> for CommitteeMembers { + type Error = TransactionError; + + fn try_from(members: Vec) -> Result { + let len = members.len(); + let members = members + .try_into() + .map_err(|_| TransactionError::InvalidCommitteeLength { + len, + max: Self::MAX_MEMBERS, + })?; + Ok(Self { members }) + } } impl ConsensusEncoding for CommitteeMembers { diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/contract_constitution.rs b/base_layer/core/src/transactions/transaction_components/side_chain/contract_constitution.rs index 5597c28970..380d5f71ac 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/contract_constitution.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/contract_constitution.rs @@ -26,6 +26,8 @@ use std::{ }; use bitflags::bitflags; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; use serde::{Deserialize, Serialize}; use super::CommitteeMembers; @@ -86,14 +88,14 @@ impl ConsensusDecoding for ContractConstitution { #[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq)] pub struct ContractAcceptanceRequirements { /// The acceptance expiry period as a relative block height. - pub period_expiry: u64, + pub acceptance_period_expiry: u64, /// The minimum number of acceptance UTXOs required for the contract acceptance period to succeed. pub minimum_quorum_required: u32, } impl ConsensusEncoding for ContractAcceptanceRequirements { fn consensus_encode(&self, writer: &mut W) -> Result<(), Error> { - self.period_expiry.consensus_encode(writer)?; + self.acceptance_period_expiry.consensus_encode(writer)?; self.minimum_quorum_required.consensus_encode(writer)?; Ok(()) } @@ -104,7 +106,7 @@ impl ConsensusEncodingSized for ContractAcceptanceRequirements {} impl ConsensusDecoding for ContractAcceptanceRequirements { fn consensus_decode(reader: &mut R) -> Result { Ok(Self { - period_expiry: u64::consensus_decode(reader)?, + acceptance_period_expiry: u64::consensus_decode(reader)?, minimum_quorum_required: u32::consensus_decode(reader)?, }) } @@ -113,14 +115,14 @@ impl ConsensusDecoding for ContractAcceptanceRequirements { #[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq)] pub struct CheckpointParameters { /// The minimum number of votes (signatures) required on each checkpoint. - pub quorum_minimum: u32, + pub minimum_quorum_required: u32, /// If this number of blocks have passed without a checkpoint, the contract becomes abandoned. pub abandoned_interval: u64, } impl ConsensusEncoding for CheckpointParameters { fn consensus_encode(&self, writer: &mut W) -> Result<(), Error> { - self.quorum_minimum.consensus_encode(writer)?; + self.minimum_quorum_required.consensus_encode(writer)?; self.abandoned_interval.consensus_encode(writer)?; Ok(()) } @@ -131,7 +133,7 @@ impl ConsensusEncodingSized for CheckpointParameters {} impl ConsensusDecoding for CheckpointParameters { fn consensus_decode(reader: &mut R) -> Result { Ok(Self { - quorum_minimum: u32::consensus_decode(reader)?, + minimum_quorum_required: u32::consensus_decode(reader)?, abandoned_interval: u64::consensus_decode(reader)?, }) } @@ -141,8 +143,6 @@ impl ConsensusDecoding for CheckpointParameters { pub struct ConstitutionChangeRules { /// Bitflag that indicates the constitution changes that are permitted. pub change_flags: ConstitutionChangeFlags, - /// The rules for changes to the validator committee. - pub committee_change_rules: CommitteeChangeRules, /// Requirements for amendments to the contract constitution. If None, then the `ContractConstitution` cannot be /// changed. pub requirements_for_constitution_change: Option, @@ -151,7 +151,6 @@ pub struct ConstitutionChangeRules { impl ConsensusEncoding for ConstitutionChangeRules { fn consensus_encode(&self, writer: &mut W) -> Result<(), Error> { self.change_flags.consensus_encode(writer)?; - self.committee_change_rules.consensus_encode(writer)?; self.requirements_for_constitution_change.consensus_encode(writer)?; Ok(()) } @@ -163,39 +162,11 @@ impl ConsensusDecoding for ConstitutionChangeRules { fn consensus_decode(reader: &mut R) -> Result { Ok(Self { change_flags: ConstitutionChangeFlags::consensus_decode(reader)?, - committee_change_rules: CommitteeChangeRules::consensus_decode(reader)?, requirements_for_constitution_change: ConsensusDecoding::consensus_decode(reader)?, }) } } -#[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq)] -pub struct CommitteeChangeRules { - /// The maximum number of committee members that can be added. - pub max_new_committee_members: u32, - /// The maximum number of committee members that can be evicted. - pub max_evicted_committee_members: u32, -} - -impl ConsensusEncoding for CommitteeChangeRules { - fn consensus_encode(&self, writer: &mut W) -> Result<(), Error> { - self.max_new_committee_members.consensus_encode(writer)?; - self.max_evicted_committee_members.consensus_encode(writer)?; - Ok(()) - } -} - -impl ConsensusEncodingSized for CommitteeChangeRules {} - -impl ConsensusDecoding for CommitteeChangeRules { - fn consensus_decode(reader: &mut R) -> Result { - Ok(Self { - max_new_committee_members: u32::consensus_decode(reader)?, - max_evicted_committee_members: u32::consensus_decode(reader)?, - }) - } -} - bitflags! { #[derive(Deserialize, Serialize)] pub struct ConstitutionChangeFlags: u8 { @@ -230,16 +201,16 @@ impl ConsensusDecoding for ConstitutionChangeFlags { } } -#[derive(Debug, Clone, Copy, Hash, PartialEq, Deserialize, Serialize, Eq)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Deserialize, Serialize, Eq, FromPrimitive)] #[repr(u8)] pub enum SideChainConsensus { /// BFT consensus e.g. HotStuff - Bft = 0, + Bft = 1, /// Proof of work consensus. - ProofOfWork = 1, + ProofOfWork = 2, /// Custom consensus that uses the base layer as a notary for side-chain state. This mode requires that the /// checkpoint provides some merklish commitment to the state. - MerkleRoot = 2, + MerkleRoot = 3, } impl ConsensusEncoding for SideChainConsensus { @@ -259,15 +230,18 @@ impl ConsensusDecoding for SideChainConsensus { fn consensus_decode(reader: &mut R) -> Result { let mut buf = [0u8; 1]; reader.read_exact(&mut buf)?; - match buf[0] { - 0 => Ok(SideChainConsensus::Bft), - 1 => Ok(SideChainConsensus::ProofOfWork), - 2 => Ok(SideChainConsensus::MerkleRoot), - b => Err(io::Error::new( + SideChainConsensus::from_u8(buf[0]).ok_or_else(|| { + io::Error::new( ErrorKind::Other, - format!("Invalid byte '{}' for SideChainConsensus", b), - )), - } + format!("Invalid byte '{}' for SideChainConsensus", buf[0]), + ) + }) + } +} + +impl From for i32 { + fn from(value: SideChainConsensus) -> Self { + value as i32 } } @@ -314,20 +288,16 @@ mod tests { let subject = ContractConstitution { validator_committee: CommitteeMembers::new(vec![].try_into().unwrap()), acceptance_requirements: ContractAcceptanceRequirements { - period_expiry: 123, + acceptance_period_expiry: 123, minimum_quorum_required: 321, }, consensus: SideChainConsensus::ProofOfWork, checkpoint_params: CheckpointParameters { - quorum_minimum: 123, + minimum_quorum_required: 123, abandoned_interval: 321, }, constitution_change_rules: ConstitutionChangeRules { change_flags: ConstitutionChangeFlags::all(), - committee_change_rules: CommitteeChangeRules { - max_new_committee_members: 0, - max_evicted_committee_members: 0, - }, requirements_for_constitution_change: Some(RequirementsForConstitutionChange { minimum_constitution_committee_signatures: 321, constitution_committee: Some(CommitteeMembers::new( diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/mod.rs b/base_layer/core/src/transactions/transaction_components/side_chain/mod.rs index 1a784c27a2..fb3e37e630 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/mod.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/mod.rs @@ -21,7 +21,18 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. mod contract_constitution; -pub use contract_constitution::ContractConstitution; +pub use contract_constitution::{ + CheckpointParameters, + ConstitutionChangeFlags, + ConstitutionChangeRules, + ContractAcceptanceRequirements, + ContractConstitution, + RequirementsForConstitutionChange, + SideChainConsensus, +}; mod committee_members; pub use committee_members::CommitteeMembers; + +mod sidechain_features; +pub use sidechain_features::SideChainFeatures; 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 new file mode 100644 index 0000000000..d893835725 --- /dev/null +++ b/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_features.rs @@ -0,0 +1,146 @@ +// 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 std::io::{Error, Read, Write}; + +use serde::{Deserialize, Serialize}; +use tari_common_types::types::FixedHash; + +use crate::{ + consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}, + transactions::transaction_components::ContractConstitution, +}; + +#[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq)] +pub struct SideChainFeatures { + pub contract_id: FixedHash, + pub constitution: Option, +} + +impl SideChainFeatures { + pub fn new(contract_id: FixedHash) -> Self { + Self::builder(contract_id).finish() + } + + pub fn builder(contract_id: FixedHash) -> SideChainFeaturesBuilder { + SideChainFeaturesBuilder::new(contract_id) + } +} + +impl ConsensusEncoding for SideChainFeatures { + fn consensus_encode(&self, writer: &mut W) -> Result<(), Error> { + self.contract_id.consensus_encode(writer)?; + self.constitution.consensus_encode(writer)?; + Ok(()) + } +} + +impl ConsensusEncodingSized for SideChainFeatures {} + +impl ConsensusDecoding for SideChainFeatures { + fn consensus_decode(reader: &mut R) -> Result { + Ok(Self { + contract_id: FixedHash::consensus_decode(reader)?, + constitution: ConsensusDecoding::consensus_decode(reader)?, + }) + } +} + +pub struct SideChainFeaturesBuilder { + features: SideChainFeatures, +} + +impl SideChainFeaturesBuilder { + pub fn new(contract_id: FixedHash) -> Self { + Self { + features: SideChainFeatures { + contract_id, + constitution: None, + }, + } + } + + pub fn with_contract_constitution(mut self, contract_constitution: ContractConstitution) -> Self { + self.features.constitution = Some(contract_constitution); + self + } + + pub fn finish(self) -> SideChainFeatures { + self.features + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryInto; + + use tari_common_types::types::PublicKey; + + use super::*; + use crate::{ + consensus::check_consensus_encoding_correctness, + transactions::transaction_components::{ + CheckpointParameters, + CommitteeMembers, + ConstitutionChangeFlags, + ConstitutionChangeRules, + ContractAcceptanceRequirements, + RequirementsForConstitutionChange, + SideChainConsensus, + }, + }; + + #[test] + fn it_encodes_and_decodes_correctly() { + let subject = SideChainFeatures { + contract_id: FixedHash::zero(), + constitution: Some(ContractConstitution { + validator_committee: vec![PublicKey::default(); CommitteeMembers::MAX_MEMBERS] + .try_into() + .unwrap(), + acceptance_requirements: ContractAcceptanceRequirements { + acceptance_period_expiry: 100, + minimum_quorum_required: 5, + }, + consensus: SideChainConsensus::MerkleRoot, + checkpoint_params: CheckpointParameters { + minimum_quorum_required: 5, + abandoned_interval: 100, + }, + constitution_change_rules: ConstitutionChangeRules { + change_flags: ConstitutionChangeFlags::all(), + requirements_for_constitution_change: Some(RequirementsForConstitutionChange { + minimum_constitution_committee_signatures: 5, + constitution_committee: Some( + vec![PublicKey::default(); CommitteeMembers::MAX_MEMBERS] + .try_into() + .unwrap(), + ), + }), + }, + initial_reward: 100.into(), + }), + }; + + check_consensus_encoding_correctness(subject).unwrap(); + } +} diff --git a/base_layer/core/src/transactions/transaction_components/test.rs b/base_layer/core/src/transactions/transaction_components/test.rs index 6a7cba5b52..e483d74bf0 100644 --- a/base_layer/core/src/transactions/transaction_components/test.rs +++ b/base_layer/core/src/transactions/transaction_components/test.rs @@ -476,15 +476,15 @@ mod output_features { let mut buf = Vec::new(); features.consensus_encode(&mut buf).unwrap(); - assert_eq!(buf.len(), 10); - assert_eq!(features.consensus_encode_exact_size(), 10); + assert_eq!(buf.len(), 11); + assert_eq!(features.consensus_encode_exact_size(), 11); let mut features = OutputFeatures::default(); features.version = OutputFeaturesVersion::V1; let mut buf = Vec::new(); features.consensus_encode(&mut buf).unwrap(); - assert_eq!(buf.len(), 12); - assert_eq!(features.consensus_encode_exact_size(), 12); + assert_eq!(buf.len(), 13); + assert_eq!(features.consensus_encode_exact_size(), 13); } #[test] @@ -497,10 +497,10 @@ mod output_features { let known_size_u8_min = features_u8_min.consensus_encode_exact_size(); assert_eq!(known_size_u8_max, known_size_u8_min); let mut buf = Vec::with_capacity(known_size_u8_max); - assert_eq!(known_size_u8_max, 19); + assert_eq!(known_size_u8_max, 20); features_u8_max.consensus_encode(&mut buf).unwrap(); - assert_eq!(buf.len(), 19); - assert_eq!(features_u8_max.consensus_encode_exact_size(), 19); + assert_eq!(buf.len(), 20); + assert_eq!(features_u8_max.consensus_encode_exact_size(), 20); let decoded_features = OutputFeatures::consensus_decode(&mut &buf[..]).unwrap(); // Recovery byte is not encoded for OutputFeaturesVersion::V0; the default is returned when decoded assert_ne!(features_u8_max, decoded_features); @@ -512,11 +512,11 @@ mod output_features { let known_size_u8_max = features_u8_max.consensus_encode_exact_size(); let known_size_u8_min = features_u8_min.consensus_encode_exact_size(); assert_eq!(known_size_u8_max, known_size_u8_min); - assert_eq!(known_size_u8_max, 21); + assert_eq!(known_size_u8_max, 22); let mut buf = Vec::with_capacity(known_size_u8_max); features_u8_max.consensus_encode(&mut buf).unwrap(); - assert_eq!(buf.len(), 21); - assert_eq!(features_u8_max.consensus_encode_exact_size(), 21); + assert_eq!(buf.len(), 22); + assert_eq!(features_u8_max.consensus_encode_exact_size(), 22); let decoded_features = OutputFeatures::consensus_decode(&mut &buf[..]).unwrap(); assert_eq!(features_u8_max, decoded_features); @@ -524,17 +524,19 @@ mod output_features { features.version = OutputFeaturesVersion::V1; let known_size = features.consensus_encode_exact_size(); let mut buf = Vec::with_capacity(known_size); - assert_eq!(known_size, 21); + assert_eq!(known_size, 22); features.consensus_encode(&mut buf).unwrap(); - assert_eq!(buf.len(), 21); - assert_eq!(features.consensus_encode_exact_size(), 21); + assert_eq!(buf.len(), 22); + assert_eq!(features.consensus_encode_exact_size(), 22); let decoded_features = OutputFeatures::consensus_decode(&mut &buf[..]).unwrap(); assert_eq!(features, decoded_features); } #[test] fn consensus_decode_bad_flags() { - let data = [0x00u8, 0x00, 0x02, 0x00u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + let data = [ + 0x00u8, 0x00, 0x02, 0x00u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; let features = OutputFeatures::consensus_decode(&mut &data[..]).unwrap(); // Assert the flag data is preserved assert_eq!(features.flags.bits() & 0x02, 0x02); @@ -542,7 +544,7 @@ mod output_features { #[test] fn consensus_decode_bad_maturity() { - let data = [0x00u8, 0xFF, 0x00u8]; + let data = [0x00u8, 0xFF, 0x00, 0x00, 0x00]; let err = OutputFeatures::consensus_decode(&mut &data[..]).unwrap_err(); assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof); } @@ -556,6 +558,7 @@ mod output_features { } mod validate_internal_consistency { + use super::*; use crate::consensus::ToConsensusBytes; diff --git a/base_layer/core/src/transactions/transaction_protocol/single_receiver.rs b/base_layer/core/src/transactions/transaction_protocol/single_receiver.rs index b2d9c11c68..5c0b09de7f 100644 --- a/base_layer/core/src/transactions/transaction_protocol/single_receiver.rs +++ b/base_layer/core/src/transactions/transaction_protocol/single_receiver.rs @@ -74,7 +74,7 @@ impl SingleReceiverTransactionProtocol { /// Validates the sender info fn validate_sender_data(sender_info: &SD) -> Result<(), TPE> { - if sender_info.amount == 0.into() && sender_info.features.unique_id.is_none() { + if sender_info.amount == 0.into() && sender_info.features.unique_asset_id().is_none() { return Err(TPE::ValidationError("Cannot send zero microTari".into())); } Ok(()) diff --git a/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs b/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs index 7b57a532fc..c908ef1144 100644 --- a/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs +++ b/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs @@ -618,7 +618,7 @@ impl SenderTransactionInitializer { // Don't care about the fees when we are sending token. if self.amounts.size() > 0 && total_fee > self.calculate_amount_to_others() && - recipient_output_features[0].unique_id.is_none() + recipient_output_features[0].unique_asset_id().is_none() { warn!( target: LOG_TARGET, diff --git a/base_layer/core/src/validation/block_validators/test.rs b/base_layer/core/src/validation/block_validators/test.rs index 33b21a3818..affdb63db5 100644 --- a/base_layer/core/src/validation/block_validators/test.rs +++ b/base_layer/core/src/validation/block_validators/test.rs @@ -42,7 +42,13 @@ use crate::{ CryptoFactories, }, txn_schema, - validation::{block_validators::BlockValidator, BlockSyncBodyValidation, ValidationError}, + validation::{ + block_validators::{BlockValidator, BodyOnlyValidator, OrphanBlockValidator}, + traits::PostOrphanBodyValidation, + BlockSyncBodyValidation, + OrphanValidation, + ValidationError, + }, }; fn setup_with_rules(rules: ConsensusManager) -> (TestBlockchain, BlockValidator) { @@ -409,9 +415,7 @@ mod unique_id { } mod body_only { - use super::*; - use crate::validation::{block_validators::BodyOnlyValidator, PostOrphanBodyValidation}; #[test] fn it_rejects_invalid_input_metadata() { @@ -445,7 +449,6 @@ mod body_only { mod orphan_validator { use super::*; - use crate::validation::{block_validators::OrphanBlockValidator, OrphanValidation}; #[test] fn it_rejects_zero_conf_double_spends() { diff --git a/base_layer/core/tests/mempool.rs b/base_layer/core/tests/mempool.rs index 2b6be2ce9f..ab6f6b2149 100644 --- a/base_layer/core/tests/mempool.rs +++ b/base_layer/core/tests/mempool.rs @@ -1076,6 +1076,7 @@ async fn consensus_validation_versions() { None, None, None, + None, ); let test_params = TestParams::new(); diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/new_output_sql.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/new_output_sql.rs index a22d15e54f..8abcd0220e 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/new_output_sql.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/new_output_sql.rs @@ -94,7 +94,7 @@ impl NewOutputSql { .parent_public_key .clone() .map(|a| a.to_vec()), - features_unique_id: output.unblinded_output.features.unique_id.clone(), + features_unique_id: output.unblinded_output.features.unique_asset_id().map(|id| id.to_vec()), sender_offset_public_key: output.unblinded_output.sender_offset_public_key.to_vec(), metadata_signature_nonce: output.unblinded_output.metadata_signature.public_nonce().to_vec(), metadata_signature_u_key: output.unblinded_output.metadata_signature.u().to_vec(), diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs index b3d09b38a3..5e6fe09d95 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs @@ -28,13 +28,13 @@ use diesel::{prelude::*, sql_query, SqliteConnection}; use log::*; use tari_common_types::{ transaction::TxId, - types::{ComSignature, Commitment, PrivateKey, PublicKey}, + types::{ComSignature, Commitment, FixedHash, PrivateKey, PublicKey}, }; use tari_core::{ covenants::Covenant, transactions::{ tari_amount::MicroTari, - transaction_components::{OutputFeatures, OutputFlags, UnblindedOutput}, + transaction_components::{OutputFeatures, OutputFlags, SideChainFeatures, UnblindedOutput}, CryptoFactories, }, }; @@ -512,7 +512,16 @@ impl TryFrom for DbUnblindedOutput { })?; features.maturity = o.maturity as u64; features.metadata = o.metadata.unwrap_or_default(); - features.unique_id = o.features_unique_id.clone(); + features.sidechain_features = o + .features_unique_id + .as_ref() + .map(|v| FixedHash::try_from(v.as_slice())) + .transpose() + .map_err(|_| OutputManagerStorageError::ConversionError { + reason: "Invalid contract ID".to_string(), + })? + // TODO: Add side chain features to wallet db + .map(SideChainFeatures::new); features.parent_public_key = o .features_parent_public_key .map(|p| PublicKey::from_bytes(&p)) diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index b7bd10d64c..2a1e899c17 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -1153,6 +1153,7 @@ pub unsafe extern "C" fn output_features_create_from_bytes( recovery_byte, decoded_metadata, decoded_unique_id, + None, decoded_parent_public_key, asset, mint_non_fungible, diff --git a/common/config/presets/base_node.toml b/common/config/presets/base_node.toml index c092fb97b3..5b8e92eaa5 100644 --- a/common/config/presets/base_node.toml +++ b/common/config/presets/base_node.toml @@ -10,7 +10,7 @@ [base_node] # Selected network -network = "dibbler" +#network = "dibbler" # The socket to expose for the gRPC base node server grpc_address = "/ip4/127.0.0.1/tcp/18142" diff --git a/integration_tests/helpers/transactionBuilder.js b/integration_tests/helpers/transactionBuilder.js index a6d6b8d974..36a95b3157 100644 --- a/integration_tests/helpers/transactionBuilder.js +++ b/integration_tests/helpers/transactionBuilder.js @@ -65,6 +65,8 @@ class TransactionBuilder { : Buffer.from([features.recovery_byte]), bufFromOpt(features.parent_public_key, "hex"), bufFromOpt(unique_id, false), + // TODO: SideChainFeatures + bufFromOpt(null), // TODO: AssetOutputFeatures bufFromOpt(null), // TODO: MintNonFungibleFeatures @@ -271,6 +273,7 @@ class TransactionBuilder { unique_id: features.unique_id ? Buffer.from(features.unique_id, "utf8") : null, + sidechain_features: null, parent_public_key: null, asset: null, mint_non_fungible: null, diff --git a/integration_tests/helpers/util.js b/integration_tests/helpers/util.js index ef34cb7b6f..d478128277 100644 --- a/integration_tests/helpers/util.js +++ b/integration_tests/helpers/util.js @@ -265,7 +265,11 @@ const getTransactionOutputHash = function (output) { Buffer.from(features), encodeOption(output.features.unique_id), ]); - // features.asset + // features.sidechain_features + features = Buffer.concat([ + Buffer.from(features), + encodeOption(output.features.sidechain_features), + ]); // features.asset features = Buffer.concat([ Buffer.from(features), encodeOption(output.features.asset),