diff --git a/applications/tari_validator_node/src/contract_worker_manager.rs b/applications/tari_validator_node/src/contract_worker_manager.rs index bfb2566f86..a224fd2ea6 100644 --- a/applications/tari_validator_node/src/contract_worker_manager.rs +++ b/applications/tari_validator_node/src/contract_worker_manager.rs @@ -28,19 +28,17 @@ use std::{ }; use log::*; -use rand::rngs::OsRng; -use tari_common_types::types::{FixedHash, FixedHashSizeError, HashDigest, PrivateKey, Signature}; +use tari_common_types::types::{FixedHash, FixedHashSizeError}; use tari_comms::{types::CommsPublicKey, NodeIdentity}; use tari_comms_dht::Dht; -use tari_core::{consensus::ConsensusHashWriter, transactions::transaction_components::ContractConstitution}; -use tari_crypto::{ - keys::SecretKey, - tari_utilities::{hex::Hex, message_format::MessageFormat, ByteArray}, -}; +use tari_core::transactions::transaction_components::ContractConstitution; +use tari_crypto::tari_utilities::{hex::Hex, message_format::MessageFormat, ByteArray}; use tari_dan_core::{ models::{AssetDefinition, BaseLayerMetadata, Committee}, services::{ + AcceptanceManager, BaseNodeClient, + ConcreteAcceptanceManager, ConcreteAssetProcessor, ConcreteCheckpointManager, ConcreteCommitteeManager, @@ -49,7 +47,6 @@ use tari_dan_core::{ NodeIdentitySigningService, TariDanPayloadProcessor, TariDanPayloadProvider, - WalletClient, }, storage::{ global::{ContractState, GlobalDb, GlobalDbMetadataKey}, @@ -88,7 +85,7 @@ pub struct ContractWorkerManager { last_scanned_height: u64, last_scanned_hash: Option, base_node_client: GrpcBaseNodeClient, - wallet_client: GrpcWalletClient, + acceptance_manager: ConcreteAcceptanceManager, identity: Arc, active_workers: HashMap>, mempool: MempoolServiceHandle, @@ -113,7 +110,7 @@ impl ContractWorkerManager { identity: Arc, global_db: GlobalDb, base_node_client: GrpcBaseNodeClient, - wallet_client: GrpcWalletClient, + acceptance_manager: ConcreteAcceptanceManager, mempool: MempoolServiceHandle, handles: ServiceHandles, subscription_factory: SubscriptionFactory, @@ -126,7 +123,7 @@ impl ContractWorkerManager { last_scanned_height: 0, last_scanned_hash: None, base_node_client, - wallet_client, + acceptance_manager, identity, mempool, handles, @@ -444,13 +441,10 @@ impl ContractWorkerManager { } async fn post_contract_acceptance(&mut self, contract: &ActiveContract) -> Result<(), WorkerManagerError> { - let nonce = PrivateKey::random(&mut OsRng); - let challenge = generate_constitution_challenge(&contract.constitution); - let signature = Signature::sign(self.identity.secret_key().clone(), nonce, challenge.as_slice()).unwrap(); + let mut acceptance_manager = self.acceptance_manager.clone(); - let tx_id = self - .wallet_client - .submit_contract_acceptance(&contract.contract_id, self.identity.public_key(), &signature) + let tx_id = acceptance_manager + .publish_acceptance(&self.identity, &contract.contract_id) .await?; info!( "Contract {} acceptance submitted with id={}", @@ -474,12 +468,6 @@ impl ContractWorkerManager { } } -fn generate_constitution_challenge(constitution: &ContractConstitution) -> [u8; 32] { - ConsensusHashWriter::new(HashDigest::with_params(&[], &[], b"tari/vn/constsig")) - .chain(constitution) - .finalize() -} - #[derive(Debug, thiserror::Error)] pub enum WorkerManagerError { #[error(transparent)] diff --git a/applications/tari_validator_node/src/dan_node.rs b/applications/tari_validator_node/src/dan_node.rs index 1c880958b7..ed156da799 100644 --- a/applications/tari_validator_node/src/dan_node.rs +++ b/applications/tari_validator_node/src/dan_node.rs @@ -24,7 +24,10 @@ use std::sync::Arc; use tari_common::exit_codes::{ExitCode, ExitError}; use tari_comms::NodeIdentity; -use tari_dan_core::{services::MempoolServiceHandle, storage::global::GlobalDb}; +use tari_dan_core::{ + services::{ConcreteAcceptanceManager, MempoolServiceHandle}, + storage::global::GlobalDb, +}; use tari_dan_storage_sqlite::{global::SqliteGlobalDbBackendAdapter, SqliteDbFactory}; use tari_p2p::comms_connector::SubscriptionFactory; use tari_service_framework::ServiceHandles; @@ -68,12 +71,13 @@ impl DanNode { ) -> Result<(), ExitError> { let base_node_client = GrpcBaseNodeClient::new(self.config.base_node_grpc_address); let wallet_client = GrpcWalletClient::new(self.config.wallet_grpc_address); + let acceptance_manager = ConcreteAcceptanceManager::new(wallet_client, base_node_client.clone()); let workers = ContractWorkerManager::new( self.config.clone(), self.identity.clone(), self.global_db.clone(), base_node_client, - wallet_client, + acceptance_manager, mempool_service, handles, subscription_factory, diff --git a/applications/tari_validator_node/src/default_service_specification.rs b/applications/tari_validator_node/src/default_service_specification.rs index 98573e6d0e..76caf5f94e 100644 --- a/applications/tari_validator_node/src/default_service_specification.rs +++ b/applications/tari_validator_node/src/default_service_specification.rs @@ -24,6 +24,7 @@ use tari_common_types::types::PublicKey; use tari_dan_core::{ models::{domain_events::ConsensusWorkerDomainEvent, TariDanPayload}, services::{ + ConcreteAcceptanceManager, ConcreteAssetProcessor, ConcreteAssetProxy, ConcreteCheckpointManager, @@ -57,6 +58,7 @@ use crate::{ pub struct DefaultServiceSpecification; impl ServiceSpecification for DefaultServiceSpecification { + type AcceptanceManager = ConcreteAcceptanceManager; type Addr = PublicKey; type AssetProcessor = ConcreteAssetProcessor; type AssetProxy = ConcreteAssetProxy; diff --git a/applications/tari_validator_node/src/grpc/services/base_node_client.rs b/applications/tari_validator_node/src/grpc/services/base_node_client.rs index 57a6eea358..84f603b2a7 100644 --- a/applications/tari_validator_node/src/grpc/services/base_node_client.rs +++ b/applications/tari_validator_node/src/grpc/services/base_node_client.rs @@ -85,7 +85,7 @@ impl BaseNodeClient for GrpcBaseNodeClient { _height: u64, contract_id: FixedHash, output_type: OutputType, - ) -> Result, DigitalAssetError> { + ) -> Result, DigitalAssetError> { let inner = self.connection().await?; let request = grpc::GetCurrentContractOutputsRequest { contract_id: contract_id.to_vec(), @@ -105,23 +105,23 @@ impl BaseNodeClient for GrpcBaseNodeClient { Err(err) => return Err(err.into()), }; - resp.outputs - .into_iter() - .map(|output| { - let mined_height = output.mined_height; - let features = output.output.and_then(|o| o.features).ok_or_else(|| { - DigitalAssetError::ConversionError("Output was none/pruned or did not contain features".to_string()) - })?; - - match features.try_into() { - Ok(features) => Ok(BaseLayerOutput { - features, - height: mined_height, - }), - Err(e) => Err(DigitalAssetError::ConversionError(e)), - } - }) - .collect() + let mut outputs = vec![]; + for mined_info in resp.outputs { + let output = mined_info + .output + .map(TryInto::try_into) + .transpose() + .map_err(DigitalAssetError::ConversionError)? + .ok_or_else(|| DigitalAssetError::InvalidPeerMessage("Mined info contained no output".to_string()))?; + + outputs.push(UtxoMinedInfo { + output: PrunedOutput::NotPruned { output }, + mmr_position: mined_info.mmr_position, + mined_height: mined_info.mined_height, + header_hash: mined_info.header_hash, + }); + } + Ok(outputs) } async fn get_constitutions( diff --git a/applications/tari_validator_node/src/grpc/validator_node_grpc_server.rs b/applications/tari_validator_node/src/grpc/validator_node_grpc_server.rs index 8db5bd6e46..3167efb8b5 100644 --- a/applications/tari_validator_node/src/grpc/validator_node_grpc_server.rs +++ b/applications/tari_validator_node/src/grpc/validator_node_grpc_server.rs @@ -30,7 +30,7 @@ use tari_common_types::types::{FixedHash, PublicKey, Signature}; use tari_comms::NodeIdentity; use tari_crypto::tari_utilities::ByteArray; use tari_dan_core::{ - services::{AssetProcessor, AssetProxy, ServiceSpecification, WalletClient}, + services::{AcceptanceManager, AssetProcessor, AssetProxy, ServiceSpecification, WalletClient}, storage::DbFactory, }; use tari_dan_engine::instructions::Instruction; @@ -45,6 +45,7 @@ pub struct ValidatorNodeGrpcServer asset_processor: TServiceSpecification::AssetProcessor, asset_proxy: TServiceSpecification::AssetProxy, wallet_client: TServiceSpecification::WalletClient, + acceptance_manager: TServiceSpecification::AcceptanceManager, } impl ValidatorNodeGrpcServer { @@ -54,6 +55,7 @@ impl ValidatorNodeGrpcServer Self { Self { node_identity, @@ -61,6 +63,7 @@ impl ValidatorNodeGrpcServer rpc::validator_node_ &self, request: tonic::Request, ) -> Result, tonic::Status> { - let mut wallet_client = self.wallet_client.clone(); + let mut acceptance_manager = self.acceptance_manager.clone(); let request = request.into_inner(); let contract_id = FixedHash::try_from(request.contract_id).map_err(|err| tonic::Status::invalid_argument(err.to_string()))?; - let validator_node_public_key = self.node_identity.public_key(); - let signature = Signature::default(); - match wallet_client - .submit_contract_acceptance(&contract_id, validator_node_public_key, &signature) + match acceptance_manager + .publish_acceptance(&self.node_identity, &contract_id) .await { Ok(tx_id) => Ok(Response::new(rpc::PublishContractAcceptanceResponse { diff --git a/applications/tari_validator_node/src/main.rs b/applications/tari_validator_node/src/main.rs index 13ab37b864..977c8b14b0 100644 --- a/applications/tari_validator_node/src/main.rs +++ b/applications/tari_validator_node/src/main.rs @@ -51,7 +51,13 @@ use tari_comms::{ }; use tari_comms_dht::Dht; use tari_dan_core::{ - services::{ConcreteAssetProcessor, ConcreteAssetProxy, MempoolServiceHandle, ServiceSpecification}, + services::{ + ConcreteAcceptanceManager, + ConcreteAssetProcessor, + ConcreteAssetProxy, + MempoolServiceHandle, + ServiceSpecification, + }, storage::{global::GlobalDb, DbFactory}, }; use tari_dan_storage_sqlite::{global::SqliteGlobalDbBackendAdapter, SqliteDbFactory}; @@ -141,20 +147,23 @@ async fn run_node(config: &ApplicationConfig) -> Result<(), ExitError> { let asset_processor = ConcreteAssetProcessor::default(); let validator_node_client_factory = TariCommsValidatorNodeClientFactory::new(handles.expect_handle::().dht_requester()); + let base_node_client = GrpcBaseNodeClient::new(config.validator_node.base_node_grpc_address); let asset_proxy: ConcreteAssetProxy = ConcreteAssetProxy::new( - GrpcBaseNodeClient::new(config.validator_node.base_node_grpc_address), + base_node_client.clone(), validator_node_client_factory, 5, mempool_service.clone(), db_factory.clone(), ); let wallet_client = GrpcWalletClient::new(config.validator_node.wallet_grpc_address); + let acceptance_manager = ConcreteAcceptanceManager::new(wallet_client.clone(), base_node_client); let grpc_server: ValidatorNodeGrpcServer = ValidatorNodeGrpcServer::new( node_identity.as_ref().clone(), db_factory.clone(), asset_processor, asset_proxy, wallet_client, + acceptance_manager, ); if let Some(address) = config.validator_node.grpc_address.clone() { diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/contract_acceptance_challenge.rs b/base_layer/core/src/transactions/transaction_components/side_chain/contract_acceptance_challenge.rs new file mode 100644 index 0000000000..10888dca39 --- /dev/null +++ b/base_layer/core/src/transactions/transaction_components/side_chain/contract_acceptance_challenge.rs @@ -0,0 +1,46 @@ +// 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 digest::Digest; +use tari_common_types::types::{Commitment, FixedHash, HashDigest}; +use tari_utilities::ByteArray; + +#[derive(Debug, Clone, Copy)] +pub struct ContractAcceptanceChallenge(FixedHash); + +impl ContractAcceptanceChallenge { + pub fn new(constiution_commitment: &Commitment, contract_id: &FixedHash) -> Self { + // TODO: Use new tari_crypto domain-separated hashing + let hash = HashDigest::new() + .chain(constiution_commitment.as_bytes()) + .chain(contract_id.as_slice()) + .finalize() + .into(); + Self(hash) + } +} + +impl AsRef<[u8]> for ContractAcceptanceChallenge { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} 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 3a2012e99d..4774ad2cab 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 @@ -23,6 +23,9 @@ mod contract_acceptance; pub use contract_acceptance::ContractAcceptance; +mod contract_acceptance_challenge; +pub use contract_acceptance_challenge::ContractAcceptanceChallenge; + mod contract_constitution; pub use contract_constitution::{ CheckpointParameters, diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/signer_signature.rs b/base_layer/core/src/transactions/transaction_components/side_chain/signer_signature.rs index 95a4dbadab..8c510f73e7 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/signer_signature.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/signer_signature.rs @@ -22,7 +22,7 @@ use std::io; -use digest::Digest; +use digest::{Digest, Output}; use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; use tari_common_types::types::{HashDigest, PrivateKey, PublicKey, Signature}; @@ -45,15 +45,30 @@ impl SignerSignature { pub fn sign>(signer_secret: &PrivateKey, challenge: C) -> Self { let signer = PublicKey::from_secret_key(signer_secret); let (nonce, public_nonce) = PublicKey::random_keypair(&mut OsRng); + + let final_challenge = Self::build_final_challenge(&signer, challenge, &public_nonce); + let signature = + Signature::sign(signer_secret.clone(), nonce, &*final_challenge).expect("challenge is the correct length"); + Self { signer, signature } + } + + pub fn verify>(signature: &Signature, signer: &PublicKey, challenge: C) -> bool { + let public_nonce = signature.get_public_nonce(); + let final_challenge = Self::build_final_challenge(signer, challenge, public_nonce); + signature.verify_challenge(signer, &final_challenge) + } + + fn build_final_challenge>( + signer: &PublicKey, + challenge: C, + public_nonce: &PublicKey, + ) -> Output { // TODO: Use domain-seperated hasher from tari_crypto - let final_challenge = HashDigest::new() + HashDigest::new() .chain(signer.as_bytes()) .chain(public_nonce.as_bytes()) .chain(challenge) - .finalize(); - let signature = - Signature::sign(signer_secret.clone(), nonce, &*final_challenge).expect("challenge is the correct length"); - Self { signer, signature } + .finalize() } pub fn signer(&self) -> &PublicKey { diff --git a/base_layer/core/src/validation/dan_validators/acceptance_validator.rs b/base_layer/core/src/validation/dan_validators/acceptance_validator.rs index 3100758009..88c04652b7 100644 --- a/base_layer/core/src/validation/dan_validators/acceptance_validator.rs +++ b/base_layer/core/src/validation/dan_validators/acceptance_validator.rs @@ -20,7 +20,7 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use tari_common_types::types::{FixedHash, PublicKey}; +use tari_common_types::types::{Commitment, FixedHash, PublicKey, Signature}; use tari_utilities::hex::Hex; use super::helpers::{ @@ -34,9 +34,11 @@ use crate::{ chain_storage::{BlockchainBackend, BlockchainDatabase}, transactions::transaction_components::{ ContractAcceptance, + ContractAcceptanceChallenge, ContractConstitution, OutputType, SideChainFeatures, + SignerSignature, TransactionOutput, }, validation::{dan_validators::DanLayerValidationError, ValidationError}, @@ -54,14 +56,15 @@ pub fn validate_acceptance( let acceptance_features = get_contract_acceptance(sidechain_features)?; let validator_node_public_key = &acceptance_features.validator_node_public_key; + let signature = &acceptance_features.signature; let constitution = fetch_contract_constitution(db, contract_id)?; validate_uniqueness(db, contract_id, validator_node_public_key)?; validate_public_key(&constitution, validator_node_public_key)?; validate_acceptance_window(db, contract_id, &constitution)?; + validate_signature(db, signature, contract_id, validator_node_public_key)?; - // TODO: check that the signature of the transaction is valid // TODO: check that the stake of the transaction is at least the minimum specified in the constitution Ok(()) @@ -150,22 +153,63 @@ pub fn fetch_constitution_height( } } +pub fn validate_signature( + db: &BlockchainDatabase, + signature: &Signature, + contract_id: FixedHash, + validator_node_public_key: &PublicKey, +) -> Result<(), ValidationError> { + let commitment = fetch_constitution_commitment(db, contract_id)?; + let challenge = ContractAcceptanceChallenge::new(&commitment, &contract_id); + + let is_valid_signature = SignerSignature::verify(signature, validator_node_public_key, challenge); + if !is_valid_signature { + return Err(ValidationError::DanLayerError( + DanLayerValidationError::InvalidAcceptanceSignature, + )); + } + + Ok(()) +} + +pub fn fetch_constitution_commitment( + db: &BlockchainDatabase, + contract_id: FixedHash, +) -> Result { + let outputs: Vec = fetch_contract_utxos(db, contract_id, OutputType::ContractConstitution)? + .into_iter() + .filter_map(|utxo| utxo.output.into_unpruned_output()) + .collect(); + + // Only one constitution should be stored for a particular contract_id + if outputs.is_empty() { + return Err(ValidationError::DanLayerError( + DanLayerValidationError::ContractConstitutionNotFound { contract_id }, + )); + } + + Ok(outputs[0].commitment().clone()) +} + #[cfg(test)] mod test { use std::convert::TryInto; - use tari_common_types::types::PublicKey; - use tari_utilities::hex::Hex; + use tari_common_types::types::{Commitment, PublicKey}; + use super::fetch_constitution_commitment; use crate::{ txn_schema, validation::dan_validators::test_helpers::{ assert_dan_validator_fail, assert_dan_validator_success, + create_acceptance_signature, create_block, create_contract_acceptance_schema, + create_contract_acceptance_schema_with_signature, create_contract_constitution, create_contract_constitution_schema, + create_random_key_pair, init_test_blockchain, publish_constitution, publish_definition, @@ -182,14 +226,17 @@ mod test { let contract_id = publish_definition(&mut blockchain, change[0].clone()); // publish the contract constitution into a block - let validator_node_public_key = PublicKey::default(); + let (private_key, public_key) = create_random_key_pair(); let mut constitution = create_contract_constitution(); - constitution.validator_committee = vec![validator_node_public_key.clone()].try_into().unwrap(); + constitution.validator_committee = vec![public_key.clone()].try_into().unwrap(); publish_constitution(&mut blockchain, change[1].clone(), contract_id, constitution); // create a valid contract acceptance transaction - let schema = create_contract_acceptance_schema(contract_id, change[2].clone(), validator_node_public_key); + let commitment = fetch_constitution_commitment(blockchain.db(), contract_id).unwrap(); + let schema = + create_contract_acceptance_schema(contract_id, commitment, change[2].clone(), private_key, public_key); let (tx, _) = schema_to_transaction(&schema); + assert_dan_validator_success(&blockchain, &tx); } @@ -204,8 +251,10 @@ mod test { // skip the contract constitution publication // create a contract acceptance transaction - let validator_node_public_key = PublicKey::default(); - let schema = create_contract_acceptance_schema(contract_id, change[1].clone(), validator_node_public_key); + let (private_key, public_key) = create_random_key_pair(); + let commitment = Commitment::default(); + let schema = + create_contract_acceptance_schema(contract_id, commitment, change[1].clone(), private_key, public_key); let (tx, _) = schema_to_transaction(&schema); // try to validate the acceptance transaction and check that we get the error @@ -221,18 +270,25 @@ mod test { let contract_id = publish_definition(&mut blockchain, change[0].clone()); // publish the contract constitution into a block - let validator_node_public_key = PublicKey::default(); + let (private_key, public_key) = create_random_key_pair(); let mut constitution = create_contract_constitution(); - constitution.validator_committee = vec![validator_node_public_key.clone()].try_into().unwrap(); + constitution.validator_committee = vec![public_key.clone()].try_into().unwrap(); publish_constitution(&mut blockchain, change[1].clone(), contract_id, constitution); // publish a contract acceptance into a block - let schema = - create_contract_acceptance_schema(contract_id, change[2].clone(), validator_node_public_key.clone()); + let commitment = fetch_constitution_commitment(blockchain.db(), contract_id).unwrap(); + let schema = create_contract_acceptance_schema( + contract_id, + commitment.clone(), + change[2].clone(), + private_key.clone(), + public_key.clone(), + ); create_block(&mut blockchain, "acceptance", schema); // create a (duplicated) contract acceptance transaction - let schema = create_contract_acceptance_schema(contract_id, change[3].clone(), validator_node_public_key); + let schema = + create_contract_acceptance_schema(contract_id, commitment, change[3].clone(), private_key, public_key); let (tx, _) = schema_to_transaction(&schema); // try to validate the duplicated acceptance transaction and check that we get the error @@ -240,7 +296,7 @@ mod test { } #[test] - fn it_rejects_contract_acceptances_of_non_committee_members() { + fn it_rejects_acceptances_of_non_committee_members() { // initialise a blockchain with enough funds to spend at contract transactions let (mut blockchain, change) = init_test_blockchain(); @@ -257,9 +313,10 @@ mod test { // create a contract acceptance transaction // we use a public key that is not included in the constitution committee, to trigger the error - let validator_node_public_key = - PublicKey::from_hex("70350e09c474809209824c6e6888707b7dd09959aa227343b5106382b856f73a").unwrap(); - let schema = create_contract_acceptance_schema(contract_id, change[2].clone(), validator_node_public_key); + let (private_key, public_key) = create_random_key_pair(); + let commitment = fetch_constitution_commitment(blockchain.db(), contract_id).unwrap(); + let schema = + create_contract_acceptance_schema(contract_id, commitment, change[2].clone(), private_key, public_key); let (tx, _) = schema_to_transaction(&schema); // try to validate the acceptance transaction and check that we get the committee error @@ -275,8 +332,8 @@ mod test { let contract_id = publish_definition(&mut blockchain, change[0].clone()); // publish the contract constitution into a block, with a very short (1 block) expiration time - let validator_node_public_key = PublicKey::default(); - let committee = vec![validator_node_public_key.clone()]; + let (private_key, public_key) = create_random_key_pair(); + let committee = vec![public_key.clone()]; let mut constitution = create_contract_constitution(); constitution.validator_committee = committee.try_into().unwrap(); constitution.acceptance_requirements.acceptance_period_expiry = 1; @@ -289,10 +346,38 @@ mod test { create_block(&mut blockchain, "filler2", schema); // create a contract acceptance after the expiration block height - let schema = create_contract_acceptance_schema(contract_id, change[4].clone(), validator_node_public_key); + let commitment = fetch_constitution_commitment(blockchain.db(), contract_id).unwrap(); + let schema = + create_contract_acceptance_schema(contract_id, commitment, change[4].clone(), private_key, public_key); let (tx, _) = schema_to_transaction(&schema); // try to validate the acceptance transaction and check that we get the expiration error assert_dan_validator_fail(&blockchain, &tx, "Acceptance window has expired"); } + + #[test] + fn it_rejects_acceptances_with_invalid_signatures() { + // initialise a blockchain with enough funds to spend at contract transactions + let (mut blockchain, change) = init_test_blockchain(); + + // publish the contract definition into a block + let contract_id = publish_definition(&mut blockchain, change[0].clone()); + + // publish the contract constitution into a block + let (_, public_key) = create_random_key_pair(); + let mut constitution = create_contract_constitution(); + constitution.validator_committee = vec![public_key.clone()].try_into().unwrap(); + publish_constitution(&mut blockchain, change[1].clone(), contract_id, constitution); + + // create a valid contract acceptance transaction, but with a signature done by a different private key + let (altered_private_key, _) = create_random_key_pair(); + let commitment = fetch_constitution_commitment(blockchain.db(), contract_id).unwrap(); + let signature = create_acceptance_signature(contract_id, commitment, altered_private_key); + let schema = + create_contract_acceptance_schema_with_signature(contract_id, change[2].clone(), public_key, signature); + let (tx, _) = schema_to_transaction(&schema); + + // try to validate the acceptance transaction and check that we get the error + assert_dan_validator_fail(&blockchain, &tx, "Invalid acceptance signature"); + } } diff --git a/base_layer/core/src/validation/dan_validators/error.rs b/base_layer/core/src/validation/dan_validators/error.rs index cea5ab7d82..bd8822ca68 100644 --- a/base_layer/core/src/validation/dan_validators/error.rs +++ b/base_layer/core/src/validation/dan_validators/error.rs @@ -59,6 +59,8 @@ pub enum DanLayerValidationError { UpdatedConstitutionAmendmentMismatch, #[error("Acceptance window has expired for contract_id ({contract_id})")] AcceptanceWindowHasExpired { contract_id: FixedHash }, + #[error("Invalid acceptance signature")] + InvalidAcceptanceSignature, #[error("Proposal acceptance window has expired for contract_id ({contract_id}) and proposal_id ({proposal_id})")] ProposalAcceptanceWindowHasExpired { contract_id: FixedHash, proposal_id: u64 }, #[error("Checkpoint has non-sequential number. Got: {got}, expected: {expected}")] diff --git a/base_layer/core/src/validation/dan_validators/test_helpers.rs b/base_layer/core/src/validation/dan_validators/test_helpers.rs index a6359d21ad..2fd6592494 100644 --- a/base_layer/core/src/validation/dan_validators/test_helpers.rs +++ b/base_layer/core/src/validation/dan_validators/test_helpers.rs @@ -22,7 +22,8 @@ use std::convert::TryInto; -use tari_common_types::types::{FixedHash, PublicKey, Signature}; +use tari_common_types::types::{Commitment, FixedHash, PublicKey, Signature}; +use tari_crypto::ristretto::{RistrettoPublicKey, RistrettoSecretKey}; use tari_p2p::Network; use super::TxDanLayerValidator; @@ -39,6 +40,7 @@ use crate::{ CommitteeSignatures, ConstitutionChangeFlags, ConstitutionChangeRules, + ContractAcceptanceChallenge, ContractAcceptanceRequirements, ContractAmendment, ContractCheckpoint, @@ -218,17 +220,44 @@ pub fn create_contract_checkpoint_schema( pub fn create_contract_acceptance_schema( contract_id: FixedHash, + commitment: Commitment, input: UnblindedOutput, - validator_node_public_key: PublicKey, + private_key: RistrettoSecretKey, + public_key: RistrettoPublicKey, ) -> TransactionSchema { - let signature = Signature::default(); + let signature = create_acceptance_signature(contract_id, commitment, private_key); - let acceptance_features = - OutputFeatures::for_contract_acceptance(contract_id, validator_node_public_key, signature); + let acceptance_features = OutputFeatures::for_contract_acceptance(contract_id, public_key, signature); txn_schema!(from: vec![input], to: vec![0.into()], fee: 5.into(), lock: 0, features: acceptance_features) } +pub fn create_contract_acceptance_schema_with_signature( + contract_id: FixedHash, + input: UnblindedOutput, + public_key: RistrettoPublicKey, + signature: Signature, +) -> TransactionSchema { + let acceptance_features = OutputFeatures::for_contract_acceptance(contract_id, public_key, signature); + + txn_schema!(from: vec![input], to: vec![0.into()], fee: 5.into(), lock: 0, features: acceptance_features) +} + +pub fn create_acceptance_signature( + contract_id: FixedHash, + commitment: Commitment, + private_key: RistrettoSecretKey, +) -> Signature { + let challenge = ContractAcceptanceChallenge::new(&commitment, &contract_id); + + SignerSignature::sign(&private_key, &challenge).signature +} + +pub fn create_random_key_pair() -> (RistrettoSecretKey, RistrettoPublicKey) { + let mut rng = rand::thread_rng(); + ::random_keypair(&mut rng) +} + pub fn create_contract_proposal_schema( contract_id: FixedHash, input: UnblindedOutput, diff --git a/dan_layer/core/src/models/base_layer_output.rs b/dan_layer/core/src/models/base_layer_output.rs index 3bae121e89..d01f18d255 100644 --- a/dan_layer/core/src/models/base_layer_output.rs +++ b/dan_layer/core/src/models/base_layer_output.rs @@ -24,9 +24,12 @@ use std::convert::TryFrom; use tari_common_types::types::{FixedHash, PublicKey}; -use tari_core::transactions::transaction_components::{OutputFeatures, OutputType}; +use tari_core::{ + chain_storage::UtxoMinedInfo, + transactions::transaction_components::{OutputFeatures, OutputType}, +}; -use crate::models::ModelError; +use crate::{models::ModelError, DigitalAssetError}; #[derive(Debug)] pub struct BaseLayerOutput { @@ -87,6 +90,27 @@ impl TryFrom for CheckpointOutput { } } +impl TryFrom for BaseLayerOutput { + type Error = DigitalAssetError; + + fn try_from(utxo: UtxoMinedInfo) -> Result { + let mined_height = utxo.mined_height; + let features = match utxo.output.as_transaction_output() { + Some(o) => o.features.clone(), + None => { + return Err(DigitalAssetError::ConversionError( + "Output was none/pruned or did not contain features".to_string(), + )) + }, + }; + + Ok(BaseLayerOutput { + features, + height: mined_height, + }) + } +} + #[derive(Debug, Clone)] pub struct CommitteeOutput { pub flags: OutputType, diff --git a/dan_layer/core/src/services/acceptance_manager.rs b/dan_layer/core/src/services/acceptance_manager.rs new file mode 100644 index 0000000000..e7a4e03ece --- /dev/null +++ b/dan_layer/core/src/services/acceptance_manager.rs @@ -0,0 +1,111 @@ +// Copyright 2021. 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 async_trait::async_trait; +use tari_common_types::types::{Commitment, FixedHash}; +use tari_comms::NodeIdentity; +use tari_core::{ + chain_storage::UtxoMinedInfo, + transactions::transaction_components::{ + ContractAcceptanceChallenge, + OutputType, + SignerSignature, + TransactionOutput, + }, +}; +use tari_utilities::hex::Hex; + +use super::BaseNodeClient; +use crate::{services::wallet_client::WalletClient, DigitalAssetError}; + +#[async_trait] +pub trait AcceptanceManager: Send + Sync { + async fn publish_acceptance( + &mut self, + node_identity: &NodeIdentity, + contract_id: &FixedHash, + ) -> Result; +} + +#[derive(Clone)] +pub struct ConcreteAcceptanceManager { + wallet: TWallet, + base_node: TBaseNode, +} + +impl ConcreteAcceptanceManager { + pub fn new(wallet: TWallet, base_node: TBaseNode) -> Self { + Self { wallet, base_node } + } +} + +#[async_trait] +impl AcceptanceManager + for ConcreteAcceptanceManager +{ + async fn publish_acceptance( + &mut self, + node_identity: &NodeIdentity, + contract_id: &FixedHash, + ) -> Result { + let public_key = node_identity.public_key(); + + // build the acceptance signature + let secret_key = node_identity.secret_key(); + let constitution_commitment = self.fetch_constitution_commitment(contract_id).await?; + let challenge = ContractAcceptanceChallenge::new(&constitution_commitment, contract_id); + let signature = SignerSignature::sign(secret_key, challenge).signature; + + // publish the acceptance + self.wallet + .submit_contract_acceptance(contract_id, public_key, &signature) + .await + } +} + +impl + ConcreteAcceptanceManager +{ + async fn fetch_constitution_commitment( + &mut self, + contract_id: &FixedHash, + ) -> Result { + let outputs: Vec = self + .base_node + .get_current_contract_outputs(0, *contract_id, OutputType::ContractConstitution) + .await?; + let transaction_outputs: Vec = outputs + .into_iter() + .filter_map(|utxo| utxo.output.into_unpruned_output()) + .collect(); + + if transaction_outputs.is_empty() { + return Err(DigitalAssetError::NotFound { + entity: "constitution", + id: contract_id.to_hex(), + }); + } + let constitution_commitment = transaction_outputs[0].commitment(); + + Ok(constitution_commitment.clone()) + } +} diff --git a/dan_layer/core/src/services/asset_proxy.rs b/dan_layer/core/src/services/asset_proxy.rs index 0c0babd1e1..3065ff3b5c 100644 --- a/dan_layer/core/src/services/asset_proxy.rs +++ b/dan_layer/core/src/services/asset_proxy.rs @@ -20,6 +20,8 @@ // 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; + use async_trait::async_trait; use futures::stream::FuturesUnordered; use log::*; @@ -31,6 +33,7 @@ use tari_utilities::hex::Hex; use tokio_stream::StreamExt; use crate::{ + models::BaseLayerOutput, services::{ validator_node_rpc_client::ValidatorNodeRpcClient, BaseNodeClient, @@ -141,7 +144,7 @@ impl> ConcreteAsse ) -> Result>, DigitalAssetError> { let mut base_node_client = self.base_node_client.clone(); let tip = base_node_client.get_tip_info().await?; - let mut constitution = base_node_client + let mut outputs = base_node_client .get_current_contract_outputs( tip.height_of_longest_chain, contract_id, @@ -149,8 +152,8 @@ impl> ConcreteAsse ) .await?; - let constitution = match constitution.pop() { - Some(chk) => chk, + let constitution = match outputs.pop() { + Some(chk) => BaseLayerOutput::try_from(chk)?, None => { return Err(DigitalAssetError::NotFound { entity: "checkpoint", diff --git a/dan_layer/core/src/services/base_node_client.rs b/dan_layer/core/src/services/base_node_client.rs index b76fe2b3ab..0b6caeb92d 100644 --- a/dan_layer/core/src/services/base_node_client.rs +++ b/dan_layer/core/src/services/base_node_client.rs @@ -38,7 +38,7 @@ pub trait BaseNodeClient: Send + Sync { height: u64, contract_id: FixedHash, output_type: OutputType, - ) -> Result, DigitalAssetError>; + ) -> Result, DigitalAssetError>; async fn get_constitutions( &mut self, diff --git a/dan_layer/core/src/services/mocks/mod.rs b/dan_layer/core/src/services/mocks/mod.rs index d4bdbd1947..2a7966aa1b 100644 --- a/dan_layer/core/src/services/mocks/mod.rs +++ b/dan_layer/core/src/services/mocks/mod.rs @@ -259,7 +259,7 @@ impl BaseNodeClient for MockBaseNodeClient { _height: u64, _contract_id: FixedHash, _output_type: OutputType, - ) -> Result, DigitalAssetError> { + ) -> Result, DigitalAssetError> { todo!() } } @@ -475,6 +475,7 @@ pub struct MockServiceSpecification; #[cfg(test)] impl ServiceSpecification for MockServiceSpecification { + type AcceptanceManager = super::ConcreteAcceptanceManager; type Addr = RistrettoPublicKey; type AssetProcessor = MockAssetProcessor; type AssetProxy = ConcreteAssetProxy; diff --git a/dan_layer/core/src/services/mod.rs b/dan_layer/core/src/services/mod.rs index 34a809df4a..d239a6567e 100644 --- a/dan_layer/core/src/services/mod.rs +++ b/dan_layer/core/src/services/mod.rs @@ -20,6 +20,7 @@ // 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. +mod acceptance_manager; mod asset_processor; mod base_node_client; mod committee_manager; @@ -30,6 +31,7 @@ mod payload_processor; mod payload_provider; mod signing_service; +pub use acceptance_manager::{AcceptanceManager, ConcreteAcceptanceManager}; pub use asset_processor::{AssetProcessor, ConcreteAssetProcessor, MemoryInstructionLog}; pub use asset_proxy::{AssetProxy, ConcreteAssetProxy}; pub use base_node_client::BaseNodeClient; @@ -39,7 +41,6 @@ pub use mempool_service::{ConcreteMempoolService, MempoolService, MempoolService pub use payload_processor::{PayloadProcessor, TariDanPayloadProcessor}; pub use payload_provider::{PayloadProvider, TariDanPayloadProvider}; pub use signing_service::{NodeIdentitySigningService, SigningService}; - mod asset_proxy; mod checkpoint_manager; pub mod mocks; diff --git a/dan_layer/core/src/services/service_specification.rs b/dan_layer/core/src/services/service_specification.rs index 7b6586769e..2bb0fdb5a1 100644 --- a/dan_layer/core/src/services/service_specification.rs +++ b/dan_layer/core/src/services/service_specification.rs @@ -22,6 +22,7 @@ use tari_dan_engine::state::StateDbBackendAdapter; +use super::acceptance_manager::AcceptanceManager; use crate::{ models::{domain_events::ConsensusWorkerDomainEvent, Payload}, services::{ @@ -52,6 +53,7 @@ use crate::{ /// simply reference types. /// This trait is intended to only include `types` and no methods. pub trait ServiceSpecification: Default + Clone { + type AcceptanceManager: AcceptanceManager + Clone; type Addr: NodeAddressable; type AssetProcessor: AssetProcessor + Clone; type AssetProxy: AssetProxy + Clone; diff --git a/dan_layer/core/src/workers/states/starting.rs b/dan_layer/core/src/workers/states/starting.rs index 75f39bac69..360aba2947 100644 --- a/dan_layer/core/src/workers/states/starting.rs +++ b/dan_layer/core/src/workers/states/starting.rs @@ -20,7 +20,7 @@ // 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::marker::PhantomData; +use std::{convert::TryInto, marker::PhantomData}; use log::*; use tari_core::transactions::transaction_components::OutputType; @@ -62,7 +62,7 @@ impl Starting { ); let tip = base_node_client.get_tip_info().await?; // get latest checkpoint on the base layer - let mut constitution = base_node_client + let mut outputs = base_node_client .get_current_contract_outputs( tip.height_of_longest_chain - asset_definition.base_layer_confirmation_time, asset_definition.contract_id, @@ -70,8 +70,8 @@ impl Starting { ) .await?; - let output = match constitution.pop() { - Some(chk) => chk, + let output = match outputs.pop() { + Some(chk) => chk.try_into()?, None => return Ok(ConsensusWorkerStateEvent::BaseLayerCheckopintNotFound), }; diff --git a/dan_layer/core/src/workers/states/synchronizing.rs b/dan_layer/core/src/workers/states/synchronizing.rs index d94f12553a..d937ea8425 100644 --- a/dan_layer/core/src/workers/states/synchronizing.rs +++ b/dan_layer/core/src/workers/states/synchronizing.rs @@ -28,7 +28,7 @@ use tari_core::transactions::transaction_components::OutputType; use tari_dan_engine::state::StateDbUnitOfWorkReader; use crate::{ - models::{AssetDefinition, CheckpointOutput}, + models::{AssetDefinition, BaseLayerOutput, CheckpointOutput}, services::{BaseNodeClient, ServiceSpecification}, storage::DbFactory, workers::{state_sync::StateSynchronizer, states::ConsensusWorkerStateEvent}, @@ -70,7 +70,10 @@ impl> Synchronizing< .await?; let last_checkpoint = match last_checkpoint.pop() { - Some(o) => CheckpointOutput::try_from(o)?, + Some(utxo) => { + let output = BaseLayerOutput::try_from(utxo)?; + CheckpointOutput::try_from(output)? + }, None => return Ok(ConsensusWorkerStateEvent::BaseLayerCheckpointNotFound), }; @@ -83,7 +86,7 @@ impl> Synchronizing< .await?; let current_constitution = match constitution.pop() { - Some(o) => o, + Some(o) => BaseLayerOutput::try_from(o)?, None => return Ok(ConsensusWorkerStateEvent::BaseLayerCheckopintNotFound), }; diff --git a/integration_tests/features/ValidatorNode.feature b/integration_tests/features/ValidatorNode.feature index ad5f813b28..28776a5860 100644 --- a/integration_tests/features/ValidatorNode.feature +++ b/integration_tests/features/ValidatorNode.feature @@ -33,7 +33,7 @@ Feature: Validator Node And I add VN1 to the validator committee on COM1 And I publish the contract constitution COM1 on wallet WALLET1 via command line And I mine 9 blocks using wallet WALLET1 on NODE1 - Then wallet WALLET1 will have a successfully mined constitution acceptance transaction for contract DEF1 + Then wallet WALLET1 will have a successfully mined contract acceptance transaction for contract DEF1 @critical Scenario: Publish contract update proposal acceptance @@ -43,6 +43,10 @@ Feature: Validator Node And I wait for wallet WALLET1 to have at least 1000000 uT And I publish a contract definition DEF1 from file "fixtures/contract_definition.json" on wallet WALLET1 via command line And I mine 4 blocks using wallet WALLET1 on NODE1 + And I publish a contract constitution from file "fixtures/contract_constitution.json" on wallet WALLET1 via command line + And I mine 4 blocks using wallet WALLET1 on NODE1 + And I publish a contract update proposal from file "fixtures/contract_update_proposal.json" on wallet WALLET1 via command line + And I mine 4 blocks using wallet WALLET1 on NODE1 And I have a validator node VN1 connected to base node NODE1 and wallet WALLET1 When I publish a contract update proposal acceptance transaction for the validator node VN1 And I mine 9 blocks using wallet WALLET1 on NODE1 diff --git a/integration_tests/features/support/validator_node_steps.js b/integration_tests/features/support/validator_node_steps.js index c8da062b8b..6082dfc787 100644 --- a/integration_tests/features/support/validator_node_steps.js +++ b/integration_tests/features/support/validator_node_steps.js @@ -71,7 +71,7 @@ Then( let dan_node = this.getNode(vn_name); let grpc_dan_node = await dan_node.createGrpcClient(); let response = await grpc_dan_node.publishContractUpdateProposalAcceptance( - "90b1da4524ea0e9479040d906db9194d8af90f28d05ff2d64c0a82eb93125177", // contract_id + "a58fb2adefcc40242f20f2d896e14451549dd60839fee78a7bd40ba2cc0a0e91", // contract_id 0 // proposal_id ); expect(response.status).to.be.equal("Accepted"); @@ -79,19 +79,6 @@ Then( } ); -Then( - "wallet {word} will have a successfully mined constitution acceptance transaction for contract {word}", - { timeout: 40 * 1000 }, - async function (wallet_name, contract_name) { - let wallet = await this.getWallet(wallet_name); - let contract_id = await this.fetchContract(contract_name); - let message = `Contract acceptance for contract with id=${contract_id}`; - - let utxos = await findUtxoWithOutputMessage(wallet, message); - expect(utxos.length).to.equal(1); - } -); - Then( "wallet {word} will have a successfully mined contract acceptance transaction for contract {word}", { timeout: 40 * 1000 }, @@ -101,8 +88,7 @@ Then( let message = `Contract acceptance for contract with id=${contract_id}`; let utxos = await findUtxoWithOutputMessage(wallet, message); - // FIXME: it seems that the validator node publishes acceptances for both definitions and constitutions - expect(utxos.length).to.be.gte(1); + expect(utxos.length).to.equal(1); } ); diff --git a/integration_tests/fixtures/contract_amendment.json b/integration_tests/fixtures/contract_amendment.json index 7fd9a90ddf..3bbbc3f592 100644 --- a/integration_tests/fixtures/contract_amendment.json +++ b/integration_tests/fixtures/contract_amendment.json @@ -1,38 +1,19 @@ { "proposal_id": 1, "validator_committee": [ - "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", - "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44" ], "validator_signatures": [ { "signer": "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", - "signature": { - "public_nonce": "3431860a4f70ddd6748d759cf66179321809e1c120a97cbdbbf2c01af5c8802f", - "signature": "be1b1e7cd18210bfced717d39bebc2534b31274976fb141856d9ee2bfe571900" - } - }, - { - "signer": "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", - "signature": { - "public_nonce": "3431860a4f70ddd6748d759cf66179321809e1c120a97cbdbbf2c01af5c8802f", - "signature": "be1b1e7cd18210bfced717d39bebc2534b31274976fb141856d9ee2bfe571900" - } - }, - { - "signer": "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", - "signature": { - "public_nonce": "3431860a4f70ddd6748d759cf66179321809e1c120a97cbdbbf2c01af5c8802f", - "signature": "be1b1e7cd18210bfced717d39bebc2534b31274976fb141856d9ee2bfe571900" - } + "public_nonce": "3431860a4f70ddd6748d759cf66179321809e1c120a97cbdbbf2c01af5c8802f", + "signature": "be1b1e7cd18210bfced717d39bebc2534b31274976fb141856d9ee2bfe571900" } ], "updated_constitution": { - "contract_id": "90b1da4524ea0e9479040d906db9194d8af90f28d05ff2d64c0a82eb93125177", + "contract_id": "a58fb2adefcc40242f20f2d896e14451549dd60839fee78a7bd40ba2cc0a0e91", "validator_committee": [ - "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", - "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", + "608001dffed28d058591cd65eaca11c465165592baf872cf1d984e26fb12b472", "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44" ], "acceptance_parameters": { diff --git a/integration_tests/fixtures/contract_update_proposal.json b/integration_tests/fixtures/contract_update_proposal.json index fa0819d8a4..c990e487a8 100644 --- a/integration_tests/fixtures/contract_update_proposal.json +++ b/integration_tests/fixtures/contract_update_proposal.json @@ -6,10 +6,9 @@ "signature": "be1b1e7cd18210bfced717d39bebc2534b31274976fb141856d9ee2bfe571900" }, "updated_constitution": { - "contract_id": "90b1da4524ea0e9479040d906db9194d8af90f28d05ff2d64c0a82eb93125177", + "contract_id": "a58fb2adefcc40242f20f2d896e14451549dd60839fee78a7bd40ba2cc0a0e91", "validator_committee": [ - "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", - "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", + "608001dffed28d058591cd65eaca11c465165592baf872cf1d984e26fb12b472", "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44" ], "acceptance_parameters": {