From 0e3bee06a08760b3fb61c2896a52b53a86d7e4a9 Mon Sep 17 00:00:00 2001 From: mrnaveira <47919901+mrnaveira@users.noreply.github.com> Date: Wed, 15 Jun 2022 13:12:58 +0100 Subject: [PATCH] feat(wallet): new command to publish a contract update proposal (#4188) Description --- New subcommand in the wallet CLI (`contract publish-update-proposal`): * Reads a proposal description from a JSON file * Creates a transaction with the appropriate features * Publishes the transaction into the network Motivation and Context --- We need a way to publish a contract update proposal into the blockchain, via a wallet command How Has This Been Tested? --- The new integration test pass --- .../src/automation/commands.rs | 43 +++++++++++- applications/tari_console_wallet/src/cli.rs | 8 +++ .../transaction_components/output_features.rs | 23 ++++++- .../side_chain/sidechain_features.rs | 5 ++ base_layer/wallet/src/assets/asset_manager.rs | 22 +++++++ .../wallet/src/assets/asset_manager_handle.rs | 22 +++++++ .../contract_update_proposal_file_format.rs | 66 +++++++++++++++++++ .../infrastructure/asset_manager_service.rs | 13 ++++ .../wallet/src/assets/infrastructure/mod.rs | 6 ++ base_layer/wallet/src/assets/mod.rs | 2 + .../features/ValidatorNode.feature | 7 +- integration_tests/features/WalletCli.feature | 18 +++++ .../features/support/validator_node_steps.js | 2 +- .../features/support/wallet_cli_steps.js | 19 +++++- .../fixtures/contract_update_proposal.json | 29 ++++++++ 15 files changed, 278 insertions(+), 7 deletions(-) create mode 100644 base_layer/wallet/src/assets/contract_update_proposal_file_format.rs create mode 100644 integration_tests/fixtures/contract_update_proposal.json diff --git a/applications/tari_console_wallet/src/automation/commands.rs b/applications/tari_console_wallet/src/automation/commands.rs index 8d83c8da2e..3b6dfb3357 100644 --- a/applications/tari_console_wallet/src/automation/commands.rs +++ b/applications/tari_console_wallet/src/automation/commands.rs @@ -34,7 +34,11 @@ use futures::FutureExt; use log::*; use sha2::Sha256; use strum_macros::{Display, EnumIter, EnumString}; -use tari_common_types::{emoji::EmojiId, transaction::TxId, types::PublicKey}; +use tari_common_types::{ + emoji::EmojiId, + transaction::TxId, + types::{FixedHash, PublicKey}, +}; use tari_comms::{ connectivity::{ConnectivityEvent, ConnectivityRequester}, multiaddr::Multiaddr, @@ -47,6 +51,7 @@ use tari_core::transactions::{ CheckpointParameters, ContractAcceptanceRequirements, ContractDefinition, + ContractUpdateProposal, SideChainConsensus, SideChainFeatures, TransactionOutput, @@ -61,6 +66,7 @@ use tari_wallet::{ ConstitutionDefinitionFileFormat, ContractDefinitionFileFormat, ContractSpecificationFileFormat, + ContractUpdateProposalFileFormat, }, error::WalletError, output_manager_service::handle::OutputManagerHandle, @@ -791,6 +797,7 @@ async fn handle_contract_definition_command( ContractSubcommand::InitConstitution(args) => init_contract_constitution_spec(args), ContractSubcommand::PublishDefinition(args) => publish_contract_definition(wallet, args).await, ContractSubcommand::PublishConstitution(args) => publish_contract_constitution(wallet, args).await, + ContractSubcommand::PublishUpdateProposal(args) => publish_contract_update_proposal(wallet, args).await, } } @@ -951,6 +958,40 @@ async fn publish_contract_constitution(wallet: &WalletSqlite, args: PublishFileA Ok(()) } +async fn publish_contract_update_proposal(wallet: &WalletSqlite, args: PublishFileArgs) -> Result<(), CommandError> { + let file = File::open(&args.file_path).map_err(|e| CommandError::JsonFile(e.to_string()))?; + let file_reader = BufReader::new(file); + + // parse the JSON file + let update_proposal: ContractUpdateProposalFileFormat = + serde_json::from_reader(file_reader).map_err(|e| CommandError::JsonFile(e.to_string()))?; + let contract_id_hex = update_proposal.updated_constitution.contract_id.clone(); + let contract_id = FixedHash::from_hex(&contract_id_hex).map_err(|e| CommandError::JsonFile(e.to_string()))?; + let update_proposal_features = ContractUpdateProposal::try_from(update_proposal).map_err(CommandError::JsonFile)?; + + let mut asset_manager = wallet.asset_manager.clone(); + let (tx_id, transaction) = asset_manager + .create_update_proposal(&contract_id, &update_proposal_features) + .await?; + + let message = format!( + "Contract update proposal {} for contract {}", + update_proposal_features.proposal_id, contract_id_hex + ); + + let mut transaction_service = wallet.transaction_service.clone(); + transaction_service + .submit_transaction(tx_id, transaction, 0.into(), message) + .await?; + + println!( + "Contract update proposal transaction submitted with tx_id={} for contract with contract_id={}", + tx_id, contract_id_hex + ); + + Ok(()) +} + fn write_utxos_to_csv_file(utxos: Vec, file_path: PathBuf) -> Result<(), CommandError> { let factory = PedersenCommitmentFactory::default(); let file = File::create(file_path).map_err(|e| CommandError::CSVFile(e.to_string()))?; diff --git a/applications/tari_console_wallet/src/cli.rs b/applications/tari_console_wallet/src/cli.rs index 4a0f72c716..fa58c189f7 100644 --- a/applications/tari_console_wallet/src/cli.rs +++ b/applications/tari_console_wallet/src/cli.rs @@ -218,6 +218,9 @@ pub enum ContractSubcommand { /// Creates and publishes a contract definition UTXO from the JSON spec file. PublishConstitution(PublishFileArgs), + + /// Creates and publishes a contract update proposal UTXO from the JSON spec file. + PublishUpdateProposal(PublishFileArgs), } #[derive(Debug, Args, Clone)] @@ -256,3 +259,8 @@ pub struct InitConstitutionArgs { pub struct PublishFileArgs { pub file_path: PathBuf, } + +#[derive(Debug, Args, Clone)] +pub struct PublishUpdateProposalArgs { + pub file_path: PathBuf, +} 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 52138f4224..9075fb0fa7 100644 --- a/base_layer/core/src/transactions/transaction_components/output_features.rs +++ b/base_layer/core/src/transactions/transaction_components/output_features.rs @@ -37,7 +37,13 @@ use tari_common_types::types::{Commitment, FixedHash, PublicKey, Signature}; use tari_crypto::ristretto::pedersen::PedersenCommitment; use tari_utilities::ByteArray; -use super::{ContractAcceptance, ContractDefinition, OutputFeaturesVersion, SideChainFeaturesBuilder}; +use super::{ + ContractAcceptance, + ContractDefinition, + ContractUpdateProposal, + OutputFeaturesVersion, + SideChainFeaturesBuilder, +}; use crate::{ consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, MaxSizeBytes}, transactions::{ @@ -311,6 +317,21 @@ impl OutputFeatures { } } + pub fn for_contract_update_proposal( + contract_id: FixedHash, + update_proposal: ContractUpdateProposal, + ) -> OutputFeatures { + Self { + output_type: OutputType::ContractConstitutionProposal, + sidechain_features: Some( + SideChainFeaturesBuilder::new(contract_id) + .with_update_proposal(update_proposal) + .finish(), + ), + ..Default::default() + } + } + pub fn unique_asset_id(&self) -> Option<&[u8]> { self.unique_id.as_deref() } 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 d71dc3c162..5ee593fb43 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 @@ -122,6 +122,11 @@ impl SideChainFeaturesBuilder { self } + pub fn with_update_proposal(mut self, update_proposal: ContractUpdateProposal) -> Self { + self.features.update_proposal = Some(update_proposal); + self + } + pub fn finish(self) -> SideChainFeatures { self.features } diff --git a/base_layer/wallet/src/assets/asset_manager.rs b/base_layer/wallet/src/assets/asset_manager.rs index 5be7dda260..d029000d8b 100644 --- a/base_layer/wallet/src/assets/asset_manager.rs +++ b/base_layer/wallet/src/assets/asset_manager.rs @@ -27,6 +27,7 @@ use tari_common_types::{ }; use tari_core::transactions::transaction_components::{ ContractDefinition, + ContractUpdateProposal, OutputFeatures, OutputType, SideChainFeatures, @@ -309,6 +310,27 @@ impl AssetManager { Ok((tx_id, transaction)) } + + pub async fn create_update_proposal( + &mut self, + contract_id: FixedHash, + update_proposal: ContractUpdateProposal, + ) -> Result<(TxId, Transaction), WalletError> { + let output = self + .output_manager + .create_output_with_features( + 0.into(), + OutputFeatures::for_contract_update_proposal(contract_id, update_proposal), + ) + .await?; + + let (tx_id, transaction) = self + .output_manager + .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), None, None) + .await?; + + Ok((tx_id, transaction)) + } } fn convert_to_asset(unblinded_output: DbUnblindedOutput) -> Result { diff --git a/base_layer/wallet/src/assets/asset_manager_handle.rs b/base_layer/wallet/src/assets/asset_manager_handle.rs index fee171eae6..471636504a 100644 --- a/base_layer/wallet/src/assets/asset_manager_handle.rs +++ b/base_layer/wallet/src/assets/asset_manager_handle.rs @@ -26,6 +26,7 @@ use tari_common_types::{ }; use tari_core::transactions::transaction_components::{ ContractDefinition, + ContractUpdateProposal, OutputFeatures, SideChainFeatures, TemplateParameter, @@ -235,4 +236,25 @@ impl AssetManagerHandle { }), } } + + pub async fn create_update_proposal( + &mut self, + contract_id: &FixedHash, + update_proposal: &ContractUpdateProposal, + ) -> Result<(TxId, Transaction), WalletError> { + match self + .handle + .call(AssetManagerRequest::CreateContractUpdateProposal { + contract_id: *contract_id, + update_proposal: Box::new(update_proposal.clone()), + }) + .await?? + { + AssetManagerResponse::CreateContractUpdateProposal { transaction, tx_id } => Ok((tx_id, *transaction)), + _ => Err(WalletError::UnexpectedApiResponse { + method: "create_update_proposal".to_string(), + api: "AssetManagerService".to_string(), + }), + } + } } diff --git a/base_layer/wallet/src/assets/contract_update_proposal_file_format.rs b/base_layer/wallet/src/assets/contract_update_proposal_file_format.rs new file mode 100644 index 0000000000..ba451088d5 --- /dev/null +++ b/base_layer/wallet/src/assets/contract_update_proposal_file_format.rs @@ -0,0 +1,66 @@ +// Copyright 2022. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::convert::{TryFrom, TryInto}; + +use serde::{Deserialize, Serialize}; +use tari_common_types::types::{PrivateKey, PublicKey, Signature}; +use tari_core::transactions::transaction_components::ContractUpdateProposal; +use tari_utilities::hex::Hex; + +use super::ConstitutionDefinitionFileFormat; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ContractUpdateProposalFileFormat { + pub proposal_id: u64, + pub signature: SignatureFileFormat, + pub updated_constitution: ConstitutionDefinitionFileFormat, +} + +impl TryFrom for ContractUpdateProposal { + type Error = String; + + fn try_from(value: ContractUpdateProposalFileFormat) -> Result { + Ok(Self { + proposal_id: value.proposal_id, + signature: value.signature.try_into()?, + updated_constitution: value.updated_constitution.try_into()?, + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SignatureFileFormat { + pub public_nonce: String, + pub signature: String, +} + +impl TryFrom for Signature { + type Error = String; + + fn try_from(value: SignatureFileFormat) -> Result { + let public_key = PublicKey::from_hex(&value.public_nonce).map_err(|e| format!("{}", e))?; + let signature = PrivateKey::from_hex(&value.signature).map_err(|e| format!("{}", e))?; + + Ok(Signature::new(public_key, signature)) + } +} diff --git a/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs b/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs index 5ec84b7f3c..0014337fc2 100644 --- a/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs +++ b/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs @@ -193,6 +193,19 @@ impl AssetManagerService { tx_id, }) }, + AssetManagerRequest::CreateContractUpdateProposal { + contract_id, + update_proposal, + } => { + let (tx_id, transaction) = self + .manager + .create_update_proposal(contract_id, *update_proposal) + .await?; + Ok(AssetManagerResponse::CreateContractUpdateProposal { + transaction: Box::new(transaction), + tx_id, + }) + }, } } } diff --git a/base_layer/wallet/src/assets/infrastructure/mod.rs b/base_layer/wallet/src/assets/infrastructure/mod.rs index f82966bceb..0a78e1e982 100644 --- a/base_layer/wallet/src/assets/infrastructure/mod.rs +++ b/base_layer/wallet/src/assets/infrastructure/mod.rs @@ -28,6 +28,7 @@ use tari_common_types::{ }; use tari_core::transactions::transaction_components::{ ContractDefinition, + ContractUpdateProposal, OutputFeatures, SideChainFeatures, TemplateParameter, @@ -79,6 +80,10 @@ pub enum AssetManagerRequest { validator_node_public_key: Box, signature: Box, }, + CreateContractUpdateProposal { + contract_id: FixedHash, + update_proposal: Box, + }, } pub enum AssetManagerResponse { @@ -91,4 +96,5 @@ pub enum AssetManagerResponse { CreateConstitutionDefinition { transaction: Box, tx_id: TxId }, CreateContractDefinition { transaction: Box, tx_id: TxId }, CreateContractAcceptance { transaction: Box, tx_id: TxId }, + CreateContractUpdateProposal { transaction: Box, tx_id: TxId }, } diff --git a/base_layer/wallet/src/assets/mod.rs b/base_layer/wallet/src/assets/mod.rs index 637055f65a..5e30e27612 100644 --- a/base_layer/wallet/src/assets/mod.rs +++ b/base_layer/wallet/src/assets/mod.rs @@ -33,6 +33,8 @@ pub(crate) mod infrastructure; mod constitution_definition_file_format; mod contract_definition_file_format; +mod contract_update_proposal_file_format; pub use constitution_definition_file_format::{ConstitutionChangeRulesFileFormat, ConstitutionDefinitionFileFormat}; pub use contract_definition_file_format::{ContractDefinitionFileFormat, ContractSpecificationFileFormat}; +pub use contract_update_proposal_file_format::ContractUpdateProposalFileFormat; diff --git a/integration_tests/features/ValidatorNode.feature b/integration_tests/features/ValidatorNode.feature index 48161a9e71..2c041fafce 100644 --- a/integration_tests/features/ValidatorNode.feature +++ b/integration_tests/features/ValidatorNode.feature @@ -27,10 +27,13 @@ Feature: Validator Node And I have wallet WALLET1 connected to all seed nodes When I mine 9 blocks using wallet WALLET1 on NODE1 Then I wait for wallet WALLET1 to have at least 1000000 uT + And I publish a contract definition from file "fixtures/contract_definition.json" on wallet WALLET1 via command line + When I mine 8 blocks using wallet WALLET1 on NODE1 + Then wallet WALLET1 has at least 1 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled And I have a validator node VN1 connected to base node NODE1 and wallet WALLET1 with "constitiution_auto_accept" set to "false" Then I publish a contract acceptance transaction for the validator node VN1 - When I mine 4 blocks using wallet WALLET1 on NODE1 - Then wallet WALLET1 has at least 1 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled + When I mine 8 blocks using wallet WALLET1 on NODE1 + Then wallet WALLET1 has at least 2 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled @dan @broken Scenario: Contract auto acceptance diff --git a/integration_tests/features/WalletCli.feature b/integration_tests/features/WalletCli.feature index 0df343c01a..087a20ac2e 100644 --- a/integration_tests/features/WalletCli.feature +++ b/integration_tests/features/WalletCli.feature @@ -177,4 +177,22 @@ Feature: Wallet CLI And I publish a contract constitution from file "fixtures/contract_constitution.json" on wallet WALLET via command line And mining node MINE mines 8 blocks Then wallet WALLET has at least 2 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled + Then WALLET is connected to BASE + + @dan @critical + Scenario: As a user I want to publish a contract update proposal via command line + Given I have a base node BASE + And I have wallet WALLET connected to base node BASE + And I have mining node MINE connected to base node BASE and wallet WALLET + And mining node MINE mines 4 blocks + Then I wait for wallet WALLET to have at least 1000000 uT + And I publish a contract definition from file "fixtures/contract_definition.json" on wallet WALLET via command line + And mining node MINE mines 8 blocks + Then wallet WALLET has at least 1 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled + And I publish a contract constitution from file "fixtures/contract_constitution.json" on wallet WALLET via command line + And mining node MINE mines 8 blocks + Then wallet WALLET has at least 2 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled + And I publish a contract update proposal from file "fixtures/contract_update_proposal.json" on wallet WALLET via command line + And mining node MINE mines 8 blocks + Then wallet WALLET has at least 3 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled Then WALLET is connected to BASE \ No newline at end of file diff --git a/integration_tests/features/support/validator_node_steps.js b/integration_tests/features/support/validator_node_steps.js index f22c063a0e..87e9cf9202 100644 --- a/integration_tests/features/support/validator_node_steps.js +++ b/integration_tests/features/support/validator_node_steps.js @@ -162,7 +162,7 @@ Then( let dan_node = this.getNode(vn_name); let grpc_dan_node = await dan_node.createGrpcClient(); let response = await grpc_dan_node.publishContractAcceptance( - "f665775dbbf4e428e5c8c2bb1c5e7d2e508e93c83250c495ac617a0a1fb2d76d" // contract_id + "90b1da4524ea0e9479040d906db9194d8af90f28d05ff2d64c0a82eb93125177" // contract_id ); expect(response.status).to.be.equal("Accepted"); console.log({ response }); diff --git a/integration_tests/features/support/wallet_cli_steps.js b/integration_tests/features/support/wallet_cli_steps.js index e417f3b840..7233960913 100644 --- a/integration_tests/features/support/wallet_cli_steps.js +++ b/integration_tests/features/support/wallet_cli_steps.js @@ -308,7 +308,7 @@ Then( let wallet = this.getWallet(wallet_name); let output = await wallet_run_command( wallet, - `contract-definition publish ${absolute_path}` + `contract publish-definition ${absolute_path}` ); console.log(output.buffer); } @@ -323,7 +323,22 @@ Then( let output = await wallet_run_command( wallet, - `publish-constitution-definition ${absolute_path}` + `contract publish-constitution ${absolute_path}` + ); + console.log(output.buffer); + } +); + +Then( + "I publish a contract update proposal from file {string} on wallet {word} via command line", + { timeout: 120 * 1000 }, + async function (relative_file_path, wallet_name) { + let absolute_path = path.resolve(relative_file_path); + let wallet = this.getWallet(wallet_name); + + let output = await wallet_run_command( + wallet, + `contract publish-update-proposal ${absolute_path}` ); console.log(output.buffer); } diff --git a/integration_tests/fixtures/contract_update_proposal.json b/integration_tests/fixtures/contract_update_proposal.json new file mode 100644 index 0000000000..fa0819d8a4 --- /dev/null +++ b/integration_tests/fixtures/contract_update_proposal.json @@ -0,0 +1,29 @@ + +{ + "proposal_id": 1, + "signature": { + "public_nonce": "3431860a4f70ddd6748d759cf66179321809e1c120a97cbdbbf2c01af5c8802f", + "signature": "be1b1e7cd18210bfced717d39bebc2534b31274976fb141856d9ee2bfe571900" + }, + "updated_constitution": { + "contract_id": "90b1da4524ea0e9479040d906db9194d8af90f28d05ff2d64c0a82eb93125177", + "validator_committee": [ + "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", + "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", + "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44" + ], + "acceptance_parameters": { + "acceptance_period_expiry": 50, + "minimum_quorum_required": 2 + }, + "consensus": "MerkleRoot", + "checkpoint_parameters": { + "minimum_quorum_required": 2, + "abandoned_interval": 100 + }, + "constitution_change_rules": { + "change_flags": 1 + }, + "initial_reward": 5 + } +}