diff --git a/applications/tari_app_grpc/proto/types.proto b/applications/tari_app_grpc/proto/types.proto index 3a60d92eec..63dab6db90 100644 --- a/applications/tari_app_grpc/proto/types.proto +++ b/applications/tari_app_grpc/proto/types.proto @@ -233,6 +233,9 @@ message SideChainFeatures { ContractDefinition definition = 2; ContractConstitution constitution = 3; ContractAcceptance acceptance = 4; + ContractUpdateProposal update_proposal = 5; + ContractUpdateProposalAcceptance update_proposal_acceptance = 6; + ContractAmendment amendment = 7; } message ContractConstitution { @@ -278,6 +281,30 @@ enum SideChainConsensus { MERKLE_ROOT = 3; } +message ContractUpdateProposal { + uint64 proposal_id = 1; + Signature signature = 2; + ContractConstitution updated_constitution = 3; +} + +message ContractUpdateProposalAcceptance { + uint64 proposal_id = 1; + bytes validator_node_public_key = 2; + Signature signature = 3; +} + +message ContractAmendment { + uint64 proposal_id = 1; + CommitteeMembers validator_committee = 2; + CommitteeSignatures validator_signatures = 3; + ContractConstitution updated_constitution = 4; + uint64 activation_window = 5; +} + +message CommitteeSignatures { + repeated Signature signatures = 1; +} + // TODO: DEPRECATED message AssetOutputFeatures { diff --git a/applications/tari_app_grpc/src/conversions/sidechain_features.rs b/applications/tari_app_grpc/src/conversions/sidechain_features.rs index 76dfef9c12..3dbc5dc60f 100644 --- a/applications/tari_app_grpc/src/conversions/sidechain_features.rs +++ b/applications/tari_app_grpc/src/conversions/sidechain_features.rs @@ -22,18 +22,22 @@ use std::convert::{TryFrom, TryInto}; -use tari_common_types::types::{FixedHash, PublicKey}; +use tari_common_types::types::{FixedHash, PublicKey, Signature}; use tari_core::transactions::transaction_components::{ vec_into_fixed_string, CheckpointParameters, CommitteeMembers, + CommitteeSignatures, ConstitutionChangeFlags, ConstitutionChangeRules, ContractAcceptance, ContractAcceptanceRequirements, + ContractAmendment, ContractConstitution, ContractDefinition, ContractSpecification, + ContractUpdateProposal, + ContractUpdateProposalAcceptance, FunctionRef, PublicFunction, RequirementsForConstitutionChange, @@ -51,6 +55,9 @@ impl From for grpc::SideChainFeatures { definition: value.definition.map(Into::into), constitution: value.constitution.map(Into::into), acceptance: value.acceptance.map(Into::into), + update_proposal: value.update_proposal.map(Into::into), + update_proposal_acceptance: value.update_proposal_acceptance.map(Into::into), + amendment: value.amendment.map(Into::into), } } } @@ -62,12 +69,24 @@ impl TryFrom for SideChainFeatures { let definition = features.definition.map(ContractDefinition::try_from).transpose()?; let constitution = features.constitution.map(ContractConstitution::try_from).transpose()?; let acceptance = features.acceptance.map(ContractAcceptance::try_from).transpose()?; + let update_proposal = features + .update_proposal + .map(ContractUpdateProposal::try_from) + .transpose()?; + let update_proposal_acceptance = features + .update_proposal_acceptance + .map(ContractUpdateProposalAcceptance::try_from) + .transpose()?; + let amendment = features.amendment.map(ContractAmendment::try_from).transpose()?; Ok(Self { contract_id: features.contract_id.try_into().map_err(|_| "Invalid contract_id")?, definition, constitution, acceptance, + update_proposal, + update_proposal_acceptance, + amendment, }) } } @@ -108,6 +127,9 @@ impl TryFrom for SideChainFeatures { initial_reward: 100.into(), }), acceptance: None, + update_proposal: None, + update_proposal_acceptance: None, + amendment: None, }) } } @@ -435,6 +457,42 @@ impl TryFrom for CommitteeMembers { } } +//---------------------------------- CommitteeSignatures --------------------------------------------// +impl From for grpc::CommitteeSignatures { + fn from(value: CommitteeSignatures) -> Self { + Self { + signatures: value.signatures().into_iter().map(Into::into).collect(), + } + } +} + +impl TryFrom for CommitteeSignatures { + type Error = String; + + fn try_from(value: grpc::CommitteeSignatures) -> Result { + if value.signatures.len() > CommitteeSignatures::MAX_SIGNATURES { + return Err(format!( + "Too many committee signatures: expected {} but got {}", + CommitteeSignatures::MAX_SIGNATURES, + value.signatures.len() + )); + } + + let signatures = value + .signatures + .into_iter() + .enumerate() + .map(|(i, s)| { + Signature::try_from(s) + .map_err(|err| format!("committee signature #{} was not a valid signature: {}", i + 1, err)) + }) + .collect::, _>>()?; + + let signatures = CommitteeSignatures::try_from(signatures).map_err(|e| e.to_string())?; + Ok(signatures) + } +} + //---------------------------------- ContractAcceptance --------------------------------------------// impl From for grpc::ContractAcceptance { @@ -457,11 +515,122 @@ impl TryFrom for ContractAcceptance { .signature .ok_or_else(|| "signature not provided".to_string())? .try_into() - .map_err(|_| "signaturecould not be converted".to_string())?; + .map_err(|_| "signature could not be converted".to_string())?; + + Ok(Self { + validator_node_public_key, + signature, + }) + } +} + +//---------------------------------- ContractUpdateProposal --------------------------------------------// + +impl From for grpc::ContractUpdateProposal { + fn from(value: ContractUpdateProposal) -> Self { + Self { + proposal_id: value.proposal_id, + signature: Some(value.signature.into()), + updated_constitution: Some(value.updated_constitution.into()), + } + } +} + +impl TryFrom for ContractUpdateProposal { + type Error = String; + + fn try_from(value: grpc::ContractUpdateProposal) -> Result { + let signature = value + .signature + .ok_or_else(|| "signature not provided".to_string())? + .try_into() + .map_err(|_| "signature could not be converted".to_string())?; + + let updated_constitution = value + .updated_constitution + .ok_or_else(|| "updated_constiution not provided".to_string())? + .try_into()?; + + Ok(Self { + proposal_id: value.proposal_id, + signature, + updated_constitution, + }) + } +} + +//---------------------------------- ContractUpdateProposalAcceptance --------------------------------------------// + +impl From for grpc::ContractUpdateProposalAcceptance { + fn from(value: ContractUpdateProposalAcceptance) -> Self { + Self { + proposal_id: value.proposal_id, + validator_node_public_key: value.validator_node_public_key.as_bytes().to_vec(), + signature: Some(value.signature.into()), + } + } +} + +impl TryFrom for ContractUpdateProposalAcceptance { + type Error = String; + + fn try_from(value: grpc::ContractUpdateProposalAcceptance) -> Result { + let validator_node_public_key = + PublicKey::from_bytes(value.validator_node_public_key.as_bytes()).map_err(|err| format!("{:?}", err))?; + + let signature = value + .signature + .ok_or_else(|| "signature not provided".to_string())? + .try_into() + .map_err(|_| "signature could not be converted".to_string())?; Ok(Self { + proposal_id: value.proposal_id, validator_node_public_key, signature, }) } } + +//---------------------------------- ContractAmendment --------------------------------------------// + +impl From for grpc::ContractAmendment { + fn from(value: ContractAmendment) -> Self { + Self { + proposal_id: value.proposal_id, + validator_committee: Some(value.validator_committee.into()), + validator_signatures: Some(value.validator_signatures.into()), + updated_constitution: Some(value.updated_constitution.into()), + activation_window: value.activation_window, + } + } +} + +impl TryFrom for ContractAmendment { + type Error = String; + + fn try_from(value: grpc::ContractAmendment) -> Result { + let validator_committee = value + .validator_committee + .map(TryInto::try_into) + .ok_or("validator_committee not provided")??; + + let validator_signatures = value + .validator_signatures + .map(TryInto::try_into) + .ok_or("validator_signatures not provided")??; + + let updated_constitution = value + .updated_constitution + .ok_or_else(|| "updated_constiution not provided".to_string())? + .try_into()?; + + Ok(Self { + proposal_id: value.proposal_id, + validator_committee, + validator_signatures, + updated_constitution, + activation_window: value.activation_window, + }) + } +} diff --git a/base_layer/core/src/proto/transaction.proto b/base_layer/core/src/proto/transaction.proto index ce5af1d3a1..6e7dd738aa 100644 --- a/base_layer/core/src/proto/transaction.proto +++ b/base_layer/core/src/proto/transaction.proto @@ -106,6 +106,9 @@ message SideChainFeatures { ContractDefinition definition = 2; ContractConstitution constitution = 3; ContractAcceptance acceptance = 4; + ContractUpdateProposal update_proposal = 5; + ContractUpdateProposalAcceptance update_proposal_acceptance = 6; + ContractAmendment amendment = 7; } message ContractConstitution { @@ -205,6 +208,30 @@ message ContractAcceptance { Signature signature = 2; } +message ContractUpdateProposal { + uint64 proposal_id = 1; + Signature signature = 2; + ContractConstitution updated_constitution = 3; +} + +message ContractUpdateProposalAcceptance { + uint64 proposal_id = 1; + bytes validator_node_public_key = 2; + Signature signature = 3; +} + +message ContractAmendment { + uint64 proposal_id = 1; + CommitteeMembers validator_committee = 2; + CommitteeSignatures validator_signatures = 3; + ContractConstitution updated_constitution = 4; + uint64 activation_window = 5; +} + +message CommitteeSignatures { + repeated Signature signatures = 1; +} + // The components of the block or transaction. The same struct can be used for either, since in Mimblewimble, // cut-through means that blocks and transactions have the same structure. The inputs, outputs and kernels should // be sorted by their Blake2b-256bit digest hash diff --git a/base_layer/core/src/proto/transaction.rs b/base_layer/core/src/proto/transaction.rs index a71d7beed8..ef65156b8b 100644 --- a/base_layer/core/src/proto/transaction.rs +++ b/base_layer/core/src/proto/transaction.rs @@ -27,7 +27,7 @@ use std::{ sync::Arc, }; -use tari_common_types::types::{BlindingFactor, BulletRangeProof, Commitment, FixedHash, PublicKey}; +use tari_common_types::types::{BlindingFactor, BulletRangeProof, Commitment, FixedHash, PublicKey, Signature}; use tari_crypto::tari_utilities::{ByteArray, ByteArrayError}; use tari_script::{ExecutionStack, TariScript}; use tari_utilities::convert::try_convert_all; @@ -44,13 +44,17 @@ use crate::{ CheckpointParameters, CommitteeDefinitionFeatures, CommitteeMembers, + CommitteeSignatures, ConstitutionChangeFlags, ConstitutionChangeRules, ContractAcceptance, ContractAcceptanceRequirements, + ContractAmendment, ContractConstitution, ContractDefinition, ContractSpecification, + ContractUpdateProposal, + ContractUpdateProposalAcceptance, FunctionRef, KernelFeatures, MintNonFungibleFeatures, @@ -352,6 +356,9 @@ impl From for proto::types::SideChainFeatures { definition: value.definition.map(Into::into), constitution: value.constitution.map(Into::into), acceptance: value.acceptance.map(Into::into), + update_proposal: value.update_proposal.map(Into::into), + update_proposal_acceptance: value.update_proposal_acceptance.map(Into::into), + amendment: value.amendment.map(Into::into), } } } @@ -360,15 +367,28 @@ impl TryFrom for SideChainFeatures { type Error = String; fn try_from(features: proto::types::SideChainFeatures) -> Result { + let contract_id = features.contract_id.try_into().map_err(|_| "Invalid contract_id")?; let definition = features.definition.map(ContractDefinition::try_from).transpose()?; let constitution = features.constitution.map(ContractConstitution::try_from).transpose()?; let acceptance = features.acceptance.map(ContractAcceptance::try_from).transpose()?; + let update_proposal = features + .update_proposal + .map(ContractUpdateProposal::try_from) + .transpose()?; + let update_proposal_acceptance = features + .update_proposal_acceptance + .map(ContractUpdateProposalAcceptance::try_from) + .transpose()?; + let amendment = features.amendment.map(ContractAmendment::try_from).transpose()?; Ok(Self { - contract_id: features.contract_id.try_into().map_err(|_| "Invalid contract_id")?, + contract_id, definition, constitution, acceptance, + update_proposal, + update_proposal_acceptance, + amendment, }) } } @@ -473,6 +493,116 @@ impl TryFrom for ContractAcceptance { } } +//---------------------------------- ContractUpdateProposal --------------------------------------------// + +impl From for proto::types::ContractUpdateProposal { + fn from(value: ContractUpdateProposal) -> Self { + Self { + proposal_id: value.proposal_id, + signature: Some(value.signature.into()), + updated_constitution: Some(value.updated_constitution.into()), + } + } +} + +impl TryFrom for ContractUpdateProposal { + type Error = String; + + fn try_from(value: proto::types::ContractUpdateProposal) -> Result { + let signature = value + .signature + .ok_or_else(|| "signature not provided".to_string())? + .try_into() + .map_err(|err: ByteArrayError| err.to_string())?; + + let updated_constitution = value + .updated_constitution + .ok_or_else(|| "updated_constiution not provided".to_string())? + .try_into()?; + + Ok(Self { + proposal_id: value.proposal_id, + signature, + updated_constitution, + }) + } +} + +//---------------------------------- ContractUpdateProposalAcceptance --------------------------------------------// + +impl From for proto::types::ContractUpdateProposalAcceptance { + fn from(value: ContractUpdateProposalAcceptance) -> Self { + Self { + proposal_id: value.proposal_id, + validator_node_public_key: value.validator_node_public_key.as_bytes().to_vec(), + signature: Some(value.signature.into()), + } + } +} + +impl TryFrom for ContractUpdateProposalAcceptance { + type Error = String; + + fn try_from(value: proto::types::ContractUpdateProposalAcceptance) -> Result { + let validator_node_public_key = + PublicKey::from_bytes(value.validator_node_public_key.as_bytes()).map_err(|err| format!("{:?}", err))?; + let signature = value + .signature + .ok_or_else(|| "signature not provided".to_string())? + .try_into() + .map_err(|err: ByteArrayError| err.to_string())?; + + Ok(Self { + proposal_id: value.proposal_id, + validator_node_public_key, + signature, + }) + } +} + +//---------------------------------- ContractAmendment --------------------------------------------// + +impl From for proto::types::ContractAmendment { + fn from(value: ContractAmendment) -> Self { + Self { + proposal_id: value.proposal_id, + validator_committee: Some(value.validator_committee.into()), + validator_signatures: Some(value.validator_signatures.into()), + updated_constitution: Some(value.updated_constitution.into()), + activation_window: value.activation_window, + } + } +} + +impl TryFrom for ContractAmendment { + type Error = String; + + fn try_from(value: proto::types::ContractAmendment) -> Result { + let validator_committee = value + .validator_committee + .map(TryInto::try_into) + .ok_or("validator_committee not provided")??; + + let validator_signatures = value + .validator_signatures + .map(TryInto::try_into) + .ok_or("validator_signatures not provided")??; + + let updated_constitution = value + .updated_constitution + .ok_or_else(|| "updated_constiution not provided".to_string())? + .try_into()?; + + Ok(Self { + proposal_id: value.proposal_id, + validator_committee, + validator_signatures, + updated_constitution, + activation_window: value.activation_window, + }) + } +} + //---------------------------------- SideChainConsensus --------------------------------------------// impl From for proto::types::SideChainConsensus { fn from(value: SideChainConsensus) -> Self { @@ -609,6 +739,42 @@ impl TryFrom for CommitteeMembers { } } +//---------------------------------- CommitteeSignatures --------------------------------------------// +impl From for proto::types::CommitteeSignatures { + fn from(value: CommitteeSignatures) -> Self { + Self { + signatures: value.signatures().into_iter().map(Into::into).collect(), + } + } +} + +impl TryFrom for CommitteeSignatures { + type Error = String; + + fn try_from(value: proto::types::CommitteeSignatures) -> Result { + if value.signatures.len() > CommitteeSignatures::MAX_SIGNATURES { + return Err(format!( + "Too many committee signatures: expected {} but got {}", + CommitteeSignatures::MAX_SIGNATURES, + value.signatures.len() + )); + } + + let signatures = value + .signatures + .into_iter() + .enumerate() + .map(|(i, s)| { + Signature::try_from(s) + .map_err(|err| format!("committee signature #{} was not a valid signature: {}", i + 1, err)) + }) + .collect::, _>>()?; + + let signatures = CommitteeSignatures::try_from(signatures).map_err(|e| e.to_string())?; + Ok(signatures) + } +} + // TODO: deprecated impl TryFrom for AssetOutputFeatures { 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 3a16ed4381..4b18423404 100644 --- a/base_layer/core/src/transactions/transaction_components/output_features.rs +++ b/base_layer/core/src/transactions/transaction_components/output_features.rs @@ -480,16 +480,48 @@ mod test { SideChainConsensus, }, vec_into_fixed_string, + CommitteeSignatures, ContractAcceptance, + ContractAmendment, ContractConstitution, ContractDefinition, ContractSpecification, + ContractUpdateProposal, + ContractUpdateProposalAcceptance, FunctionRef, PublicFunction, }, }; + #[allow(clippy::too_many_lines)] fn make_fully_populated_output_features(version: OutputFeaturesVersion) -> OutputFeatures { + let constitution = 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(), + }; + OutputFeatures { version, flags: OutputFlags::all(), @@ -502,32 +534,7 @@ mod test { 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(), - }), + constitution: Some(constitution.clone()), definition: Some(ContractDefinition { contract_name: vec_into_fixed_string("name".as_bytes().to_vec()), contract_issuer: PublicKey::default(), @@ -555,6 +562,27 @@ mod test { validator_node_public_key: PublicKey::default(), signature: Signature::default(), }), + update_proposal: Some(ContractUpdateProposal { + proposal_id: 0_u64, + signature: Signature::default(), + updated_constitution: constitution.clone(), + }), + update_proposal_acceptance: Some(ContractUpdateProposalAcceptance { + proposal_id: 0_u64, + validator_node_public_key: PublicKey::default(), + signature: Signature::default(), + }), + amendment: Some(ContractAmendment { + proposal_id: 0_u64, + validator_committee: vec![PublicKey::default(); CommitteeMembers::MAX_MEMBERS] + .try_into() + .unwrap(), + validator_signatures: vec![Signature::default(); CommitteeSignatures::MAX_SIGNATURES] + .try_into() + .unwrap(), + updated_constitution: constitution, + activation_window: 0_u64, + }), }), // Deprecated parent_public_key: Some(PublicKey::default()), diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/committee_signatures.rs b/base_layer/core/src/transactions/transaction_components/side_chain/committee_signatures.rs new file mode 100644 index 0000000000..3cbbf0c4f2 --- /dev/null +++ b/base_layer/core/src/transactions/transaction_components/side_chain/committee_signatures.rs @@ -0,0 +1,111 @@ +// 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}, + io, +}; + +use serde::{Deserialize, Serialize}; +use tari_common_types::types::Signature; + +use crate::{ + consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, MaxSizeVec}, + transactions::transaction_components::TransactionError, +}; + +#[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq, Default)] +pub struct CommitteeSignatures { + signatures: MaxSizeVec, +} + +impl CommitteeSignatures { + pub const MAX_SIGNATURES: usize = 512; + + pub fn new(signatures: MaxSizeVec) -> Self { + Self { signatures } + } + + pub fn signatures(&self) -> Vec { + self.signatures.to_vec() + } +} + +impl TryFrom> for CommitteeSignatures { + type Error = TransactionError; + + fn try_from(signatures: Vec) -> Result { + let len = signatures.len(); + let signatures = signatures + .try_into() + .map_err(|_| TransactionError::InvalidCommitteeLength { + len, + max: Self::MAX_SIGNATURES, + })?; + Ok(Self { signatures }) + } +} + +impl ConsensusEncoding for CommitteeSignatures { + fn consensus_encode(&self, writer: &mut W) -> Result<(), io::Error> { + self.signatures.consensus_encode(writer)?; + Ok(()) + } +} + +impl ConsensusEncodingSized for CommitteeSignatures {} + +impl ConsensusDecoding for CommitteeSignatures { + fn consensus_decode(reader: &mut R) -> Result { + Ok(Self { + signatures: ConsensusDecoding::consensus_decode(reader)?, + }) + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryInto; + + use super::*; + use crate::consensus::{check_consensus_encoding_correctness, ToConsensusBytes}; + + #[test] + fn it_encodes_and_decodes_correctly() { + let subject = CommitteeSignatures::new( + vec![Signature::default(); CommitteeSignatures::MAX_SIGNATURES] + .try_into() + .unwrap(), + ); + check_consensus_encoding_correctness(subject).unwrap(); + + let subject = CommitteeSignatures::default(); + check_consensus_encoding_correctness(subject).unwrap(); + } + + #[test] + fn it_fails_for_more_than_max_signatures() { + let v = vec![Signature::default(); CommitteeSignatures::MAX_SIGNATURES + 1]; + let encoded = v.to_consensus_bytes(); + CommitteeSignatures::consensus_decode(&mut encoded.as_slice()).unwrap_err(); + } +} diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/contract_amendment.rs b/base_layer/core/src/transactions/transaction_components/side_chain/contract_amendment.rs new file mode 100644 index 0000000000..2ab51efade --- /dev/null +++ b/base_layer/core/src/transactions/transaction_components/side_chain/contract_amendment.rs @@ -0,0 +1,131 @@ +// 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 super::{CommitteeMembers, CommitteeSignatures, ContractConstitution}; +use crate::consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}; + +/// # ContractAmendment +/// +/// This details a ratification of a contract update proposal, accepted by all the required validator nodes +#[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq)] +pub struct ContractAmendment { + /// The unique identification of the proposal + pub proposal_id: u64, + /// The committee of validator nodes accepting the changes + pub validator_committee: CommitteeMembers, + /// Signatures for all the proposal acceptances of the validator committee + pub validator_signatures: CommitteeSignatures, + /// Reiteration of the accepted constitution changes + pub updated_constitution: ContractConstitution, + /// Number of blocks until the contract changes are enforced by the base layer + pub activation_window: u64, +} + +impl ConsensusEncoding for ContractAmendment { + fn consensus_encode(&self, writer: &mut W) -> Result<(), Error> { + self.proposal_id.consensus_encode(writer)?; + self.validator_committee.consensus_encode(writer)?; + self.validator_signatures.consensus_encode(writer)?; + self.updated_constitution.consensus_encode(writer)?; + self.activation_window.consensus_encode(writer)?; + + Ok(()) + } +} + +impl ConsensusEncodingSized for ContractAmendment {} + +impl ConsensusDecoding for ContractAmendment { + fn consensus_decode(reader: &mut R) -> Result { + Ok(Self { + proposal_id: u64::consensus_decode(reader)?, + validator_committee: CommitteeMembers::consensus_decode(reader)?, + validator_signatures: CommitteeSignatures::consensus_decode(reader)?, + updated_constitution: ContractConstitution::consensus_decode(reader)?, + activation_window: u64::consensus_decode(reader)?, + }) + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryInto; + + use tari_common_types::types::PublicKey; + + use super::*; + use crate::{ + consensus::check_consensus_encoding_correctness, + transactions::{ + tari_amount::MicroTari, + transaction_components::{ + CheckpointParameters, + CommitteeMembers, + ConstitutionChangeFlags, + ConstitutionChangeRules, + ContractAcceptanceRequirements, + RequirementsForConstitutionChange, + SideChainConsensus, + }, + }, + }; + + #[test] + fn it_encodes_and_decodes_correctly() { + let constitution = ContractConstitution { + validator_committee: CommitteeMembers::new(vec![].try_into().unwrap()), + acceptance_requirements: ContractAcceptanceRequirements { + acceptance_period_expiry: 123, + minimum_quorum_required: 321, + }, + consensus: SideChainConsensus::ProofOfWork, + checkpoint_params: CheckpointParameters { + minimum_quorum_required: 123, + abandoned_interval: 321, + }, + constitution_change_rules: ConstitutionChangeRules { + change_flags: ConstitutionChangeFlags::all(), + requirements_for_constitution_change: Some(RequirementsForConstitutionChange { + minimum_constitution_committee_signatures: 321, + constitution_committee: Some(CommitteeMembers::new( + vec![PublicKey::default(); 32].try_into().unwrap(), + )), + }), + }, + initial_reward: MicroTari::from(123u64), + }; + + let amendment = ContractAmendment { + proposal_id: 0_u64, + validator_committee: CommitteeMembers::new(vec![].try_into().unwrap()), + validator_signatures: CommitteeSignatures::new(vec![].try_into().unwrap()), + updated_constitution: constitution, + activation_window: 0_u64, + }; + + check_consensus_encoding_correctness(amendment).unwrap(); + } +} diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/contract_update_proposal.rs b/base_layer/core/src/transactions/transaction_components/side_chain/contract_update_proposal.rs new file mode 100644 index 0000000000..5987ec8e73 --- /dev/null +++ b/base_layer/core/src/transactions/transaction_components/side_chain/contract_update_proposal.rs @@ -0,0 +1,123 @@ +// 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::Signature; + +use super::ContractConstitution; +use crate::consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}; + +/// # ContractUpdateProposal +/// +/// This details a proposal of changes in a contract constitution, so the committee member can accept or reject. +/// It specifies all the fields in the new constitution. +#[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq)] +pub struct ContractUpdateProposal { + /// A unique identification of the proposal, for later reference + pub proposal_id: u64, + /// Signature of the proposal by the proposer party + pub signature: Signature, + /// The new constitution that is proposed + pub updated_constitution: ContractConstitution, +} + +impl ConsensusEncoding for ContractUpdateProposal { + fn consensus_encode(&self, writer: &mut W) -> Result<(), Error> { + self.proposal_id.consensus_encode(writer)?; + self.signature.consensus_encode(writer)?; + self.updated_constitution.consensus_encode(writer)?; + + Ok(()) + } +} + +impl ConsensusEncodingSized for ContractUpdateProposal {} + +impl ConsensusDecoding for ContractUpdateProposal { + fn consensus_decode(reader: &mut R) -> Result { + Ok(Self { + proposal_id: u64::consensus_decode(reader)?, + signature: Signature::consensus_decode(reader)?, + updated_constitution: ContractConstitution::consensus_decode(reader)?, + }) + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryInto; + + use tari_common_types::types::PublicKey; + + use super::*; + use crate::{ + consensus::check_consensus_encoding_correctness, + transactions::{ + tari_amount::MicroTari, + transaction_components::{ + CheckpointParameters, + CommitteeMembers, + ConstitutionChangeFlags, + ConstitutionChangeRules, + ContractAcceptanceRequirements, + RequirementsForConstitutionChange, + SideChainConsensus, + }, + }, + }; + + #[test] + fn it_encodes_and_decodes_correctly() { + let constitution = ContractConstitution { + validator_committee: CommitteeMembers::new(vec![].try_into().unwrap()), + acceptance_requirements: ContractAcceptanceRequirements { + acceptance_period_expiry: 123, + minimum_quorum_required: 321, + }, + consensus: SideChainConsensus::ProofOfWork, + checkpoint_params: CheckpointParameters { + minimum_quorum_required: 123, + abandoned_interval: 321, + }, + constitution_change_rules: ConstitutionChangeRules { + change_flags: ConstitutionChangeFlags::all(), + requirements_for_constitution_change: Some(RequirementsForConstitutionChange { + minimum_constitution_committee_signatures: 321, + constitution_committee: Some(CommitteeMembers::new( + vec![PublicKey::default(); 32].try_into().unwrap(), + )), + }), + }, + initial_reward: MicroTari::from(123u64), + }; + + let constitution_update_proposal = ContractUpdateProposal { + proposal_id: 0_u64, + signature: Signature::default(), + updated_constitution: constitution, + }; + + check_consensus_encoding_correctness(constitution_update_proposal).unwrap(); + } +} diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/contract_update_proposal_acceptance.rs b/base_layer/core/src/transactions/transaction_components/side_chain/contract_update_proposal_acceptance.rs new file mode 100644 index 0000000000..db258f9945 --- /dev/null +++ b/base_layer/core/src/transactions/transaction_components/side_chain/contract_update_proposal_acceptance.rs @@ -0,0 +1,82 @@ +// 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::{PublicKey, Signature}; + +use crate::consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}; + +/// # ContractUpdateProposalAcceptance +/// +/// Represents the acceptance, by a validator todo, of a contract update proposal +#[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq)] +pub struct ContractUpdateProposalAcceptance { + /// A unique identification of the proposal + pub proposal_id: u64, + /// The public key of the validator node accepting the proposal + pub validator_node_public_key: PublicKey, + /// Signature of the proposal hash by the validator node + pub signature: Signature, +} + +impl ConsensusEncoding for ContractUpdateProposalAcceptance { + fn consensus_encode(&self, writer: &mut W) -> Result<(), Error> { + self.proposal_id.consensus_encode(writer)?; + self.validator_node_public_key.consensus_encode(writer)?; + self.signature.consensus_encode(writer)?; + + Ok(()) + } +} + +impl ConsensusEncodingSized for ContractUpdateProposalAcceptance {} + +impl ConsensusDecoding for ContractUpdateProposalAcceptance { + fn consensus_decode(reader: &mut R) -> Result { + Ok(Self { + proposal_id: u64::consensus_decode(reader)?, + validator_node_public_key: PublicKey::consensus_decode(reader)?, + signature: Signature::consensus_decode(reader)?, + }) + } +} + +#[cfg(test)] +mod tests { + use tari_common_types::types::PublicKey; + + use super::*; + use crate::consensus::check_consensus_encoding_correctness; + + #[test] + fn it_encodes_and_decodes_correctly() { + let subject = ContractUpdateProposalAcceptance { + proposal_id: 0_u64, + validator_node_public_key: PublicKey::default(), + signature: Signature::default(), + }; + + check_consensus_encoding_correctness(subject).unwrap(); + } +} 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 c861dea8ee..eea6e6b49c 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 @@ -43,8 +43,20 @@ pub use contract_definition::{ PublicFunction, }; +mod contract_update_proposal; +pub use contract_update_proposal::ContractUpdateProposal; + +mod contract_update_proposal_acceptance; +pub use contract_update_proposal_acceptance::ContractUpdateProposalAcceptance; + +mod contract_amendment; +pub use contract_amendment::ContractAmendment; + mod committee_members; pub use committee_members::CommitteeMembers; +mod committee_signatures; +pub use committee_signatures::CommitteeSignatures; + mod sidechain_features; pub use sidechain_features::{SideChainFeatures, SideChainFeaturesBuilder}; diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_features.rs b/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_features.rs index 7f414bcbc1..d71dc3c162 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_features.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_features.rs @@ -25,7 +25,13 @@ use std::io::{Error, Read, Write}; use serde::{Deserialize, Serialize}; use tari_common_types::types::FixedHash; -use super::{ContractAcceptance, ContractDefinition}; +use super::{ + ContractAcceptance, + ContractAmendment, + ContractDefinition, + ContractUpdateProposal, + ContractUpdateProposalAcceptance, +}; use crate::{ consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}, transactions::transaction_components::ContractConstitution, @@ -37,6 +43,9 @@ pub struct SideChainFeatures { pub definition: Option, pub constitution: Option, pub acceptance: Option, + pub update_proposal: Option, + pub update_proposal_acceptance: Option, + pub amendment: Option, } impl SideChainFeatures { @@ -55,6 +64,10 @@ impl ConsensusEncoding for SideChainFeatures { self.definition.consensus_encode(writer)?; self.constitution.consensus_encode(writer)?; self.acceptance.consensus_encode(writer)?; + self.update_proposal.consensus_encode(writer)?; + self.update_proposal_acceptance.consensus_encode(writer)?; + self.amendment.consensus_encode(writer)?; + Ok(()) } } @@ -68,6 +81,9 @@ impl ConsensusDecoding for SideChainFeatures { definition: ConsensusDecoding::consensus_decode(reader)?, constitution: ConsensusDecoding::consensus_decode(reader)?, acceptance: ConsensusDecoding::consensus_decode(reader)?, + update_proposal: ConsensusDecoding::consensus_decode(reader)?, + update_proposal_acceptance: ConsensusDecoding::consensus_decode(reader)?, + amendment: ConsensusDecoding::consensus_decode(reader)?, }) } } @@ -84,6 +100,9 @@ impl SideChainFeaturesBuilder { definition: None, constitution: None, acceptance: None, + update_proposal: None, + update_proposal_acceptance: None, + amendment: None, }, } } @@ -121,6 +140,7 @@ mod tests { vec_into_fixed_string, CheckpointParameters, CommitteeMembers, + CommitteeSignatures, ConstitutionChangeFlags, ConstitutionChangeRules, ContractAcceptanceRequirements, @@ -134,34 +154,36 @@ mod tests { #[test] fn it_encodes_and_decodes_correctly() { + let constitution = 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(), + }; + 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(), - }), + constitution: Some(constitution.clone()), definition: Some(ContractDefinition { contract_name: vec_into_fixed_string("name".as_bytes().to_vec()), contract_issuer: PublicKey::default(), @@ -189,6 +211,27 @@ mod tests { validator_node_public_key: PublicKey::default(), signature: Signature::default(), }), + update_proposal: Some(ContractUpdateProposal { + proposal_id: 0_u64, + signature: Signature::default(), + updated_constitution: constitution.clone(), + }), + update_proposal_acceptance: Some(ContractUpdateProposalAcceptance { + proposal_id: 0_u64, + validator_node_public_key: PublicKey::default(), + signature: Signature::default(), + }), + amendment: Some(ContractAmendment { + proposal_id: 0_u64, + validator_committee: vec![PublicKey::default(); CommitteeMembers::MAX_MEMBERS] + .try_into() + .unwrap(), + validator_signatures: vec![Signature::default(); CommitteeSignatures::MAX_SIGNATURES] + .try_into() + .unwrap(), + updated_constitution: constitution, + activation_window: 0_u64, + }), }; check_consensus_encoding_correctness(subject).unwrap(); diff --git a/integration_tests/fixtures/contract_constitution.json b/integration_tests/fixtures/contract_constitution.json index c63ac16591..e0bc0eb12d 100644 --- a/integration_tests/fixtures/contract_constitution.json +++ b/integration_tests/fixtures/contract_constitution.json @@ -18,4 +18,4 @@ "change_flags": 1 }, "initial_reward": 5 -} \ No newline at end of file +}