Skip to content

Commit

Permalink
feat(wallet): new command to publish a contract definition transaction (
Browse files Browse the repository at this point in the history
#4133)

Description
---
* Created a new transaction output features for the Contract Definition
    * For simplicity, I decided to leave out fields that could be more coupled to the template format.
    * The `contract_id` is calculated as a hash of the contents of the contract definition, in the line of what is described in the RFCs. I used `ConsensusHashWriter` to make it totally consistent.
    * I created a new Output Features version to signal the consensus breaking changes.
    * Had to update the genesis block struct to include the new features
    * Had to update the gRPC types to include the new features
* Created a new command in the tari console wallet (`publish-contract-definition`):
    1. Reads the contract definition from a JSON file. I defined auxiliary structs (`base_layer/wallet/src/assets/contract_definition.rs`) to decouple the file format from the output features.
    2. Created a new transaction with the `CONTRACT_DEFNITION` flag and the new Contract Definition output features.
    3. Publishes the corresponding UTXO into the network.
* Right now the base layer does not perform any check in the values in the contract definition. In the future we want to implement custom consensus rules for contract definition (like avoiding duplicates, spending requirements, etc.) but I purposely left that out of this PR for simplicity. 

Motivation and Context
---
As a user, I want to be able to publish a new contract definition into the network through the console wallet.

How Has This Been Tested?
---
* Created a new unit test for the consensus encoding of the new Contract Definition output features
* Created a new integration test for the wallet CLI that reads the contract definition from a JSON file and successfully publishes the transaction
  • Loading branch information
mrnaveira authored May 26, 2022
1 parent 52ecb49 commit b4991a4
Show file tree
Hide file tree
Showing 24 changed files with 962 additions and 19 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 23 additions & 1 deletion applications/tari_app_grpc/proto/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ message OutputFeatures {

message SideChainFeatures {
bytes contract_id = 1;
// ContractDefinition definition = 2;
ContractDefinition definition = 2;
ContractConstitution constitution = 3;
}

Expand Down Expand Up @@ -306,6 +306,28 @@ message CommitteeDefinitionFeatures {
uint64 effective_sidechain_height = 2;
}

message ContractDefinition {
bytes contract_id = 1;
bytes contract_name = 2;
bytes contract_issuer = 3;
ContractSpecification contract_spec = 4;
}

message ContractSpecification {
bytes runtime = 1;
repeated PublicFunction public_functions = 2;
}

message PublicFunction {
bytes name = 1;
FunctionRef function = 2;
}

message FunctionRef {
bytes template_id = 1;
uint32 function_id = 2;
}

// 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
Expand Down
132 changes: 131 additions & 1 deletion applications/tari_app_grpc/src/conversions/sidechain_features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,19 @@

use std::convert::{TryFrom, TryInto};

use tari_common_types::types::PublicKey;
use tari_common_types::types::{FixedHash, PublicKey};
use tari_core::transactions::transaction_components::{
vec_into_fixed_string,
CheckpointParameters,
CommitteeMembers,
ConstitutionChangeFlags,
ConstitutionChangeRules,
ContractAcceptanceRequirements,
ContractConstitution,
ContractDefinition,
ContractSpecification,
FunctionRef,
PublicFunction,
RequirementsForConstitutionChange,
SideChainConsensus,
SideChainFeatures,
Expand All @@ -42,6 +47,7 @@ impl From<SideChainFeatures> for grpc::SideChainFeatures {
fn from(value: SideChainFeatures) -> Self {
Self {
contract_id: value.contract_id.to_vec(),
definition: value.definition.map(Into::into),
constitution: value.constitution.map(Into::into),
}
}
Expand All @@ -51,15 +57,139 @@ impl TryFrom<grpc::SideChainFeatures> for SideChainFeatures {
type Error = String;

fn try_from(features: grpc::SideChainFeatures) -> Result<Self, Self::Error> {
let definition = features.definition.map(ContractDefinition::try_from).transpose()?;
let constitution = features.constitution.map(ContractConstitution::try_from).transpose()?;

Ok(Self {
contract_id: features.contract_id.try_into().map_err(|_| "Invalid contract_id")?,
definition,
constitution,
})
}
}

//---------------------------------- ContractDefinition --------------------------------------------//

impl TryFrom<grpc::ContractDefinition> for ContractDefinition {
type Error = String;

fn try_from(value: grpc::ContractDefinition) -> Result<Self, Self::Error> {
let contract_id = FixedHash::try_from(value.contract_id).map_err(|err| format!("{:?}", err))?;

let contract_name = vec_into_fixed_string(value.contract_name);

let contract_issuer =
PublicKey::from_bytes(value.contract_issuer.as_bytes()).map_err(|err| format!("{:?}", err))?;

let contract_spec = value
.contract_spec
.map(ContractSpecification::try_from)
.ok_or_else(|| "contract_spec is missing".to_string())?
.map_err(|err| err)?;

Ok(Self {
contract_id,
contract_name,
contract_issuer,
contract_spec,
})
}
}

impl From<ContractDefinition> for grpc::ContractDefinition {
fn from(value: ContractDefinition) -> Self {
let contract_id = value.contract_id.as_bytes().to_vec();
let contract_name = value.contract_name.as_bytes().to_vec();
let contract_issuer = value.contract_issuer.as_bytes().to_vec();

Self {
contract_id,
contract_name,
contract_issuer,
contract_spec: Some(value.contract_spec.into()),
}
}
}

impl TryFrom<grpc::ContractSpecification> for ContractSpecification {
type Error = String;

fn try_from(value: grpc::ContractSpecification) -> Result<Self, Self::Error> {
let runtime = vec_into_fixed_string(value.runtime);
let public_functions = value
.public_functions
.into_iter()
.map(PublicFunction::try_from)
.collect::<Result<_, _>>()?;

Ok(Self {
runtime,
public_functions,
})
}
}

impl From<ContractSpecification> for grpc::ContractSpecification {
fn from(value: ContractSpecification) -> Self {
let public_functions = value.public_functions.into_iter().map(|f| f.into()).collect();
Self {
runtime: value.runtime.as_bytes().to_vec(),
public_functions,
}
}
}

impl TryFrom<grpc::PublicFunction> for PublicFunction {
type Error = String;

fn try_from(value: grpc::PublicFunction) -> Result<Self, Self::Error> {
let function = value
.function
.map(FunctionRef::try_from)
.ok_or_else(|| "function is missing".to_string())?
.map_err(|err| err)?;

Ok(Self {
name: vec_into_fixed_string(value.name),
function,
})
}
}

impl From<PublicFunction> for grpc::PublicFunction {
fn from(value: PublicFunction) -> Self {
Self {
name: value.name.as_bytes().to_vec(),
function: Some(value.function.into()),
}
}
}

impl TryFrom<grpc::FunctionRef> for FunctionRef {
type Error = String;

fn try_from(value: grpc::FunctionRef) -> Result<Self, Self::Error> {
let template_id = FixedHash::try_from(value.template_id).map_err(|err| format!("{:?}", err))?;
let function_id = u16::try_from(value.function_id).map_err(|_| "Invalid function_id: overflowed u16")?;

Ok(Self {
template_id,
function_id,
})
}
}

impl From<FunctionRef> for grpc::FunctionRef {
fn from(value: FunctionRef) -> Self {
let template_id = value.template_id.as_bytes().to_vec();

Self {
template_id,
function_id: value.function_id.into(),
}
}
}

//---------------------------------- ContractConstitution --------------------------------------------//
impl From<ContractConstitution> for grpc::ContractConstitution {
fn from(value: ContractConstitution) -> Self {
Expand Down
2 changes: 2 additions & 0 deletions applications/tari_console_wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ qrcode = { version = "0.12" }
regex = "1.5.4"
rpassword = "5.0"
rustyline = "9.0"
serde = "1.0.136"
serde_json = "1.0.79"
strum = "0.22"
strum_macros = "0.22"
thiserror = "1.0.26"
Expand Down
22 changes: 22 additions & 0 deletions applications/tari_console_wallet/src/automation/command_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ impl Display for ParsedCommand {
CreateInitialCheckpoint => "create-initial-checkpoint",
CreateCommitteeDefinition => "create-committee-definition",
RevalidateWalletDb => "revalidate-wallet-db",
PublishContractDefinition => "publish-contract-definition",
};

let args = self
Expand All @@ -94,6 +95,7 @@ pub enum ParsedArgument {
Address(Multiaddr),
Negotiated(bool),
Hash(Vec<u8>),
JSONFileName(String),
}

impl Display for ParsedArgument {
Expand All @@ -112,6 +114,7 @@ impl Display for ParsedArgument {
Address(v) => write!(f, "{}", v),
Negotiated(v) => write!(f, "{}", v),
Hash(v) => write!(f, "{}", v.to_hex()),
JSONFileName(v) => write!(f, "{}", v),
}
}
}
Expand Down Expand Up @@ -148,6 +151,7 @@ pub fn parse_command(command: &str) -> Result<ParsedCommand, ParseError> {
CreateInitialCheckpoint => parser_builder(args).pub_key().text().build()?,
CreateCommitteeDefinition => parser_builder(args).pub_key().pub_key_array().build()?,
RevalidateWalletDb => Vec::new(),
PublishContractDefinition => parse_publish_contract_definition(args)?,
};

Ok(ParsedCommand { command, args })
Expand Down Expand Up @@ -483,6 +487,24 @@ fn parse_coin_split(mut args: SplitWhitespace) -> Result<Vec<ParsedArgument>, Pa
Ok(parsed_args)
}

fn parse_publish_contract_definition(mut args: SplitWhitespace) -> Result<Vec<ParsedArgument>, ParseError> {
let mut parsed_args = Vec::new();

let usage = "Usage:\n publish-contract-definition\n publish-contract-definition --json-file <file name>";

let arg = args.next().ok_or_else(|| ParseError::Empty("json-file".to_string()))?;
if arg != "--json-file" {
return Err(ParseError::Empty(format!("'--json-file' qualifier\n {}", usage)));
}

let file_name = args
.next()
.ok_or_else(|| ParseError::Empty(format!("file name\n {}", usage)))?;
parsed_args.push(ParsedArgument::JSONFileName(file_name.to_string()));

Ok(parsed_args)
}

#[cfg(test)]
mod test {
use std::str::FromStr;
Expand Down
40 changes: 37 additions & 3 deletions applications/tari_console_wallet/src/automation/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

use std::{
fs::File,
io::{LineWriter, Write},
io::{BufReader, LineWriter, Write},
time::{Duration, Instant},
};

Expand All @@ -45,12 +45,12 @@ use tari_comms::{
use tari_comms_dht::{envelope::NodeDestination, DhtDiscoveryRequester};
use tari_core::transactions::{
tari_amount::{uT, MicroTari, Tari},
transaction_components::{TransactionOutput, UnblindedOutput},
transaction_components::{ContractDefinition, TransactionOutput, UnblindedOutput},
};
use tari_crypto::{keys::PublicKey as PublicKeyTrait, ristretto::pedersen::PedersenCommitmentFactory};
use tari_utilities::{hex::Hex, ByteArray, Hashable};
use tari_wallet::{
assets::KEY_MANAGER_ASSET_BRANCH,
assets::{ContractDefinitionFileFormat, KEY_MANAGER_ASSET_BRANCH},
error::WalletError,
key_manager_service::KeyManagerInterface,
output_manager_service::handle::OutputManagerHandle,
Expand Down Expand Up @@ -97,6 +97,7 @@ pub enum WalletCommand {
CreateInitialCheckpoint,
CreateCommitteeDefinition,
RevalidateWalletDb,
PublishContractDefinition,
}

#[derive(Debug)]
Expand Down Expand Up @@ -908,6 +909,39 @@ pub async fn command_runner(
.await
.map_err(CommandError::TransactionServiceError)?;
},
PublishContractDefinition => {
// open the JSON file with the contract definition values
let file_path = match parsed.args.get(0) {
Some(ParsedArgument::JSONFileName(ref file_path)) => Ok(file_path),
_ => Err(CommandError::Argument),
}?;
let file = File::open(file_path).map_err(|e| CommandError::JSONFile(e.to_string()))?;
let file_reader = BufReader::new(file);

// parse the JSON file
let contract_definition: ContractDefinitionFileFormat =
serde_json::from_reader(file_reader).map_err(|e| CommandError::JSONFile(e.to_string()))?;
let contract_definition_features = ContractDefinition::from(contract_definition);
let contract_id_hex = contract_definition_features.contract_id.to_vec().to_hex();

// create the contract definition transaction
let mut asset_manager = wallet.asset_manager.clone();
let (tx_id, transaction) = asset_manager
.create_contract_definition(&contract_definition_features)
.await?;

// publish the contract definition transaction
let message = format!("Contract definition for contract with id={}", contract_id_hex);
transaction_service
.submit_transaction(tx_id, transaction, 0.into(), message)
.await?;

println!(
"Contract definition transaction submitted with tx_id={} for contract with contract_id={}",
tx_id, contract_id_hex
);
println!("Done!");
},
}
}

Expand Down
2 changes: 2 additions & 0 deletions applications/tari_console_wallet/src/automation/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ pub enum CommandError {
HexError(#[from] HexError),
#[error("Error `{0}`")]
ShaError(String),
#[error("JSON file error `{0}`")]
JSONFile(String),
}

impl From<CommandError> for ExitError {
Expand Down
2 changes: 1 addition & 1 deletion base_layer/core/src/blocks/genesis_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ fn get_dibbler_genesis_block_raw() -> Block {
asset: None,
mint_non_fungible: None,
sidechain_checkpoint: None,
committee_definition: None
committee_definition: None,
},
Commitment::from_hex("e2b9ee8fdf05f9fa8fd7598d4568b539eef694e58cdae84c779140271a96d733 ").unwrap(),
BulletRangeProof::from_hex("0c02c6d9bdbd1c21b29ee0f83bed597ed07f71a60f99ddcbc02550059c4c08020438f8fc25c69160dc6af81f84c037fc79b9f4a7baa93ab4d6ef1640356d9b575efe1f3f8b40f6c64d1a964aab215491f79738ccac1712d756607626442dda37ac30010dc663985153786cc53c865c01bfec186a803c1edb1a34efa3088ec221016a63cdbd2b58fa4c258bfbf0cff6b793ab62f5db0fb781f046effb5f4e7c0a956c2e042e3f0c16a72f20a624625fa6dc0b742e49e0158a50c8abde54834e04bb35baef0c258da30b738256549e3a2612ff89b4f6bfe82d16aa10b38daabe0df6b922717cb4b1604ab97a2a5efa4d325beb56c5419cff185d61e1a0fc9e374098bf4a10404d788141e2c77de222d68c14b421b62f300898c25487f491aff26be85e54c011e90cc96aff6b31993ce74233674fb150de929fbc259bcc7808a84432cf28bf83c2a0fbf2b47a6244fbafa02ca4f5c9d46c5380fe8eaed734f1d56e769e59800137900cb5905191bbb463cbcb4ea0a2073d716f18878ed4455d19426a2c1133bf703510bf0f1b3b70e9e5ee6fbb70a8e710fd0a4b8f37eacfdeef3df66e461f16ffdb270a7181505b1358f56578840bbfa284444c35160794f0294300ecb3fde701a3f5ed9234e4c196b93fd70633069eeb184ab53685b5324c963a7428094f0c7d4306b5da6ef5fb68d085c32adabe004bebcbf335ee8fc92e5e034edcb035872d08f139e9445539241ff9b9fbebbc0e7b248cbd97fa7c6f3d7823085893c9ced1685d69d2a7cf111f81e086927565c301d4e33639def1139bd5245a0ae9085d5ba10cdc1f89fc7a7fa95cc3aa11784ec40ebf57475ffb4f2b2042018e3dbe905ebd5d0ebe533f36f43f709110372c94258a59e53c9b319adca30c8e9f4f92d5937f994ff36a5bb38a15682187dc8734162f45e169a97a36fb5a05").unwrap(),
Expand Down
Loading

0 comments on commit b4991a4

Please sign in to comment.