Skip to content

Commit

Permalink
feat(wallet): publish contract amendment (#4200)
Browse files Browse the repository at this point in the history
Description
---
* New subcommand in the wallet CLI (`contract publish-amendment`):
    * Reads an amendment description from a JSON file
    * Creates a transaction with the appropriate features
    * Publishes the transaction into the network
* New `OutputType` for contract amendments. Refactored conversion from byte into the `OutputType` enum type using the `num_derive` crate.

Motivation and Context
---
We need a way to publish a contract amendment into the blockchain, via a wallet command

How Has This Been Tested?
---
The new integration test pass

* create mock cli command and integration test

* parse json file with the amendment

* publish contract amendment transaction

* fix dan layer integration tests

* fix outputtype byte conversion

* assert the "None" variant in outputtype
  • Loading branch information
mrnaveira authored Jun 15, 2022
1 parent e3b2b9b commit edcce4a
Show file tree
Hide file tree
Showing 15 changed files with 293 additions and 23 deletions.
37 changes: 37 additions & 0 deletions applications/tari_console_wallet/src/automation/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ use tari_core::transactions::{
transaction_components::{
CheckpointParameters,
ContractAcceptanceRequirements,
ContractAmendment,
ContractDefinition,
ContractUpdateProposal,
SideChainConsensus,
Expand All @@ -64,6 +65,7 @@ use tari_wallet::{
assets::{
ConstitutionChangeRulesFileFormat,
ConstitutionDefinitionFileFormat,
ContractAmendmentFileFormat,
ContractDefinitionFileFormat,
ContractSpecificationFileFormat,
ContractUpdateProposalFileFormat,
Expand Down Expand Up @@ -798,6 +800,7 @@ async fn handle_contract_definition_command(
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,
ContractSubcommand::PublishAmendment(args) => publish_contract_amendment(wallet, args).await,
}
}

Expand Down Expand Up @@ -992,6 +995,40 @@ async fn publish_contract_update_proposal(wallet: &WalletSqlite, args: PublishFi
Ok(())
}

async fn publish_contract_amendment(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 amendment: ContractAmendmentFileFormat =
serde_json::from_reader(file_reader).map_err(|e| CommandError::JsonFile(e.to_string()))?;
let contract_id_hex = amendment.updated_constitution.contract_id.clone();
let contract_id = FixedHash::from_hex(&contract_id_hex).map_err(|e| CommandError::JsonFile(e.to_string()))?;
let amendment_features = ContractAmendment::try_from(amendment).map_err(CommandError::JsonFile)?;

let mut asset_manager = wallet.asset_manager.clone();
let (tx_id, transaction) = asset_manager
.create_contract_amendment(&contract_id, &amendment_features)
.await?;

let message = format!(
"Contract amendment {} for contract {}",
amendment_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 amendment transaction submitted with tx_id={} for contract with contract_id={}",
tx_id, contract_id_hex
);

Ok(())
}

fn write_utxos_to_csv_file(utxos: Vec<UnblindedOutput>, file_path: PathBuf) -> Result<(), CommandError> {
let factory = PedersenCommitmentFactory::default();
let file = File::create(file_path).map_err(|e| CommandError::CSVFile(e.to_string()))?;
Expand Down
3 changes: 3 additions & 0 deletions applications/tari_console_wallet/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ pub enum ContractSubcommand {

/// Creates and publishes a contract update proposal UTXO from the JSON spec file.
PublishUpdateProposal(PublishFileArgs),

/// Creates and publishes a contract amendment UTXO from the JSON spec file.
PublishAmendment(PublishFileArgs),
}

#[derive(Debug, Args, Clone)]
Expand Down
3 changes: 2 additions & 1 deletion base_layer/core/src/chain_storage/lmdb_db/contract_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ impl<'a> ContractIndex<'a, WriteTransaction<'a>> {
// These are collections of output hashes
OutputType::ContractValidatorAcceptance |
OutputType::ContractConstitutionProposal |
OutputType::ContractConstitutionChangeAcceptance => {
OutputType::ContractConstitutionChangeAcceptance |
OutputType::ContractAmendment => {
self.assert_definition_exists(contract_id)?;
let mut hashes = self.find::<FixedHashSet>(&key)?.unwrap_or_default();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ use tari_utilities::ByteArray;

use super::{
ContractAcceptance,
ContractAmendment,
ContractDefinition,
ContractUpdateProposal,
ContractUpdateProposalAcceptance,
Expand Down Expand Up @@ -354,6 +355,18 @@ impl OutputFeatures {
}
}

pub fn for_contract_amendment(contract_id: FixedHash, amendment: ContractAmendment) -> OutputFeatures {
Self {
output_type: OutputType::ContractAmendment,
sidechain_features: Some(
SideChainFeaturesBuilder::new(contract_id)
.with_contract_amendment(amendment)
.finish(),
),
..Default::default()
}
}

pub fn unique_asset_id(&self) -> Option<&[u8]> {
self.unique_id.as_deref()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ use std::{
io::Read,
};

use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
use serde_repr::{Deserialize_repr, Serialize_repr};

use crate::consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized};

#[derive(Debug, Clone, Copy, Hash, Deserialize_repr, Serialize_repr, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, Hash, Deserialize_repr, Serialize_repr, PartialEq, Eq, FromPrimitive)]
#[repr(u8)]
pub enum OutputType {
/// An standard non-coinbase output.
Expand All @@ -52,16 +54,18 @@ pub enum OutputType {
ContractConstitutionProposal = 6,
/// Output that indicates acceptance of an existing contract constitution amendment proposal.
ContractConstitutionChangeAcceptance = 7,
/// Output that defines and amendment of a contract constitution amendment
ContractAmendment = 8,

// TODO: Remove these deprecated flags
NonFungible = 8,
AssetRegistration = 9,
MintNonFungible = 10,
BurnNonFungible = 11,
SidechainInitialCheckpoint = 12,
SidechainCheckpoint = 13,
CommitteeInitialDefinition = 14,
CommitteeDefinition = 15,
NonFungible = 9,
AssetRegistration = 10,
MintNonFungible = 11,
BurnNonFungible = 12,
SidechainInitialCheckpoint = 13,
SidechainCheckpoint = 14,
CommitteeInitialDefinition = 15,
CommitteeDefinition = 16,
}

impl OutputType {
Expand All @@ -72,16 +76,8 @@ impl OutputType {

/// Returns the OutputType that corresponds to this OutputType. If the byte does not correspond to any OutputType,
/// None is returned.
pub fn from_byte(bit: u8) -> Option<Self> {
if !Self::is_valid_byte(bit) {
return None;
}
// SAFETY: bit has been checked for validity before transmute is called
Some(unsafe { std::mem::transmute(bit) })
}

fn is_valid_byte(bit: u8) -> bool {
bit <= 15
pub fn from_byte(value: u8) -> Option<Self> {
FromPrimitive::from_u8(value)
}
}

Expand Down Expand Up @@ -133,7 +129,8 @@ mod tests {
fn it_converts_from_byte_to_output_type() {
assert_eq!(OutputType::from_byte(0), Some(OutputType::Standard));
assert_eq!(OutputType::from_byte(1), Some(OutputType::Coinbase));
assert_eq!(OutputType::from_byte(15), Some(OutputType::CommitteeDefinition));
assert_eq!(OutputType::from_byte(16), None);
assert_eq!(OutputType::from_byte(15), Some(OutputType::CommitteeInitialDefinition));
assert_eq!(OutputType::from_byte(16), Some(OutputType::CommitteeDefinition));
assert_eq!(OutputType::from_byte(17), None);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ impl SideChainFeaturesBuilder {
self
}

pub fn with_contract_amendment(mut self, contract_amendment: ContractAmendment) -> Self {
self.features.amendment = Some(contract_amendment);
self
}

pub fn finish(self) -> SideChainFeatures {
self.features
}
Expand Down
19 changes: 19 additions & 0 deletions base_layer/wallet/src/assets/asset_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use tari_common_types::{
types::{Commitment, FixedHash, PublicKey, Signature, ASSET_CHECKPOINT_ID},
};
use tari_core::transactions::transaction_components::{
ContractAmendment,
ContractDefinition,
ContractUpdateProposal,
OutputFeatures,
Expand Down Expand Up @@ -359,6 +360,24 @@ impl<T: OutputManagerBackend + 'static> AssetManager<T> {

Ok((tx_id, transaction))
}

pub async fn create_contract_amendment(
&mut self,
contract_id: FixedHash,
amendment: ContractAmendment,
) -> Result<(TxId, Transaction), WalletError> {
let output = self
.output_manager
.create_output_with_features(0.into(), OutputFeatures::for_contract_amendment(contract_id, amendment))
.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<Asset, WalletError> {
Expand Down
22 changes: 22 additions & 0 deletions base_layer/wallet/src/assets/asset_manager_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use tari_common_types::{
types::{Commitment, FixedHash, PublicKey, Signature},
};
use tari_core::transactions::transaction_components::{
ContractAmendment,
ContractDefinition,
ContractUpdateProposal,
OutputFeatures,
Expand Down Expand Up @@ -284,4 +285,25 @@ impl AssetManagerHandle {
}),
}
}

pub async fn create_contract_amendment(
&mut self,
contract_id: &FixedHash,
amendment: &ContractAmendment,
) -> Result<(TxId, Transaction), WalletError> {
match self
.handle
.call(AssetManagerRequest::CreateContractAmendment {
contract_id: *contract_id,
contract_amendment: Box::new(amendment.clone()),
})
.await??
{
AssetManagerResponse::CreateContractAmendment { transaction, tx_id } => Ok((tx_id, *transaction)),
_ => Err(WalletError::UnexpectedApiResponse {
method: "create_contract_amendment".to_string(),
api: "AssetManagerService".to_string(),
}),
}
}
}
78 changes: 78 additions & 0 deletions base_layer/wallet/src/assets/contract_amendment_file_format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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::{CommitteeSignatures, ContractAmendment};
use tari_utilities::hex::Hex;

use super::ConstitutionDefinitionFileFormat;

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ContractAmendmentFileFormat {
pub proposal_id: u64,
pub validator_committee: Vec<PublicKey>,
pub validator_signatures: Vec<SignatureFileFormat>,
pub updated_constitution: ConstitutionDefinitionFileFormat,
pub activation_window: u64,
}

impl TryFrom<ContractAmendmentFileFormat> for ContractAmendment {
type Error = String;

fn try_from(value: ContractAmendmentFileFormat) -> Result<Self, Self::Error> {
let validator_signature_vec: Vec<Signature> = value
.validator_signatures
.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<_>, _>>()?;
let validator_signatures =
CommitteeSignatures::try_from(validator_signature_vec).map_err(|e| format!("{}", e))?;

Ok(Self {
proposal_id: value.proposal_id,
validator_committee: value.validator_committee.try_into().map_err(|e| format!("{}", e))?,
validator_signatures,
updated_constitution: value.updated_constitution.try_into()?,
activation_window: value.activation_window,
})
}
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SignatureFileFormat {
pub public_nonce: String,
pub signature: String,
}

impl TryFrom<SignatureFileFormat> for Signature {
type Error = String;

fn try_from(value: SignatureFileFormat) -> Result<Self, Self::Error> {
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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,19 @@ impl<T: OutputManagerBackend + 'static> AssetManagerService<T> {
tx_id,
})
},
AssetManagerRequest::CreateContractAmendment {
contract_id,
contract_amendment,
} => {
let (tx_id, transaction) = self
.manager
.create_contract_amendment(contract_id, *contract_amendment)
.await?;
Ok(AssetManagerResponse::CreateContractAmendment {
transaction: Box::new(transaction),
tx_id,
})
},
}
}
}
6 changes: 6 additions & 0 deletions base_layer/wallet/src/assets/infrastructure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use tari_common_types::{
types::{Commitment, FixedHash, PublicKey, Signature},
};
use tari_core::transactions::transaction_components::{
ContractAmendment,
ContractDefinition,
ContractUpdateProposal,
OutputFeatures,
Expand Down Expand Up @@ -90,6 +91,10 @@ pub enum AssetManagerRequest {
contract_id: FixedHash,
update_proposal: Box<ContractUpdateProposal>,
},
CreateContractAmendment {
contract_id: FixedHash,
contract_amendment: Box<ContractAmendment>,
},
}

pub enum AssetManagerResponse {
Expand All @@ -104,4 +109,5 @@ pub enum AssetManagerResponse {
CreateContractAcceptance { transaction: Box<Transaction>, tx_id: TxId },
CreateContractUpdateProposalAcceptance { transaction: Box<Transaction>, tx_id: TxId },
CreateContractUpdateProposal { transaction: Box<Transaction>, tx_id: TxId },
CreateContractAmendment { transaction: Box<Transaction>, tx_id: TxId },
}
2 changes: 2 additions & 0 deletions base_layer/wallet/src/assets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ pub use asset_manager_handle::AssetManagerHandle;
pub(crate) mod infrastructure;

mod constitution_definition_file_format;
mod contract_amendment_file_format;
mod contract_definition_file_format;
mod contract_update_proposal_file_format;

pub use constitution_definition_file_format::{ConstitutionChangeRulesFileFormat, ConstitutionDefinitionFileFormat};
pub use contract_amendment_file_format::ContractAmendmentFileFormat;
pub use contract_definition_file_format::{ContractDefinitionFileFormat, ContractSpecificationFileFormat};
pub use contract_update_proposal_file_format::ContractUpdateProposalFileFormat;
Loading

0 comments on commit edcce4a

Please sign in to comment.