From 963eab514603481909941d79e6c1d712a592f3b3 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Mon, 23 Jan 2023 20:02:35 +0100 Subject: [PATCH 01/27] wip --- apps/src/bin/namada-client/cli.rs | 3 + apps/src/lib/cli.rs | 81 ++++++++++++++++++++++++ apps/src/lib/cli/utils.rs | 14 +++++ apps/src/lib/client/tx.rs | 82 +++++++++++++++++++++---- core/src/ledger/storage_api/key.rs | 16 ++++- core/src/proto/types.rs | 4 +- core/src/types/key/mod.rs | 26 +++++++- core/src/types/transaction/mod.rs | 4 +- tx_prelude/src/account.rs | 18 ++++++ tx_prelude/src/lib.rs | 1 + tx_prelude/src/proof_of_stake.rs | 2 +- vm_env/src/common.rs | 0 wasm/wasm_source/src/tx_init_account.rs | 5 +- wasm/wasm_source/src/vp_user.rs | 21 ++++--- 14 files changed, 244 insertions(+), 33 deletions(-) create mode 100644 tx_prelude/src/account.rs create mode 100644 vm_env/src/common.rs diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index 14a07bb6c1..f0dfb9ae60 100644 --- a/apps/src/bin/namada-client/cli.rs +++ b/apps/src/bin/namada-client/cli.rs @@ -27,6 +27,9 @@ pub async fn main() -> Result<()> { Sub::TxInitAccount(TxInitAccount(args)) => { tx::submit_init_account(ctx, args).await; } + Sub::TxInitAccountMultiSignature(TxInitAccountMultiSignature(args)) => { + tx::submit_init_account_multisignature(ctx, args).await; + } Sub::TxInitValidator(TxInitValidator(args)) => { tx::submit_init_validator(ctx, args).await; } diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 9c23cadb46..d2e29e6fce 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -156,6 +156,7 @@ pub mod cmds { .subcommand(TxIbcTransfer::def().display_order(1)) .subcommand(TxUpdateVp::def().display_order(1)) .subcommand(TxInitAccount::def().display_order(1)) + .subcommand(TxInitAccountMultiSignature::def().display_order(1)) .subcommand(TxRevealPk::def().display_order(1)) // Proposal transactions .subcommand(TxInitProposal::def().display_order(1)) @@ -191,6 +192,7 @@ pub mod cmds { let tx_ibc_transfer = Self::parse_with_ctx(matches, TxIbcTransfer); let tx_update_vp = Self::parse_with_ctx(matches, TxUpdateVp); let tx_init_account = Self::parse_with_ctx(matches, TxInitAccount); + let tx_init_account_multisignature = Self::parse_with_ctx(matches, TxInitAccountMultiSignature); let tx_init_validator = Self::parse_with_ctx(matches, TxInitValidator); let tx_reveal_pk = Self::parse_with_ctx(matches, TxRevealPk); @@ -226,6 +228,7 @@ pub mod cmds { .or(tx_ibc_transfer) .or(tx_update_vp) .or(tx_init_account) + .or(tx_init_account_multisignature) .or(tx_reveal_pk) .or(tx_init_proposal) .or(tx_vote_proposal) @@ -289,6 +292,7 @@ pub mod cmds { QueryResult(QueryResult), TxUpdateVp(TxUpdateVp), TxInitAccount(TxInitAccount), + TxInitAccountMultiSignature(TxInitAccountMultiSignature), TxInitValidator(TxInitValidator), TxInitProposal(TxInitProposal), TxVoteProposal(TxVoteProposal), @@ -1072,6 +1076,28 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct TxInitAccountMultiSignature(pub args::TxInitAccountMultiSignature); + + impl SubCmd for TxInitAccountMultiSignature { + const CMD: &'static str = "init-account-m"; + + fn parse(matches: &ArgMatches) -> Option { + matches.subcommand_matches(Self::CMD).map(|matches| { + TxInitAccountMultiSignature(args::TxInitAccountMultiSignature::parse(matches)) + }) + } + + fn def() -> App { + App::new(Self::CMD) + .about( + "Send a signed transaction to create a new established \ + account.", + ) + .add_args::() + } + } + #[derive(Clone, Debug)] pub struct TxInitValidator(pub args::TxInitValidator); @@ -1661,6 +1687,7 @@ pub mod args { const PROTOCOL_KEY: ArgOpt = arg_opt("protocol-key"); const PRE_GENESIS_PATH: ArgOpt = arg_opt("pre-genesis-path"); const PUBLIC_KEY: Arg = arg("public-key"); + const PUBLIC_KEY_MULTISIGNATURE: ArgMulti = arg_multi("public-keys"); const PROPOSAL_ID: Arg = arg("proposal-id"); const PROPOSAL_ID_OPT: ArgOpt = arg_opt("proposal-id"); const PROPOSAL_VOTE: Arg = arg("vote"); @@ -1674,6 +1701,7 @@ pub mod args { const SIGNING_KEY_OPT: ArgOpt = SIGNING_KEY.opt(); const SIGNING_KEY: Arg = arg("signing-key"); const SOURCE: Arg = arg("source"); + const SOURCE_MULTISIGNATURE: ArgMulti = arg_multi("sources"); const SOURCE_OPT: ArgOpt = SOURCE.opt(); const STORAGE_KEY: Arg = arg("storage-key"); const SUB_PREFIX: ArgOpt = arg_opt("sub-prefix"); @@ -1683,6 +1711,7 @@ pub mod args { const TOKEN: Arg = arg("token"); const TRANSFER_SOURCE: Arg = arg("source"); const TRANSFER_TARGET: Arg = arg("target"); + const THRESHOLD: ArgOpt = arg_opt("threshold"); const TX_HASH: Arg = arg("tx-hash"); const UNSAFE_DONT_ENCRYPT: ArgFlag = flag("unsafe-dont-encrypt"); const UNSAFE_SHOW_SECRET: ArgFlag = flag("unsafe-show-secret"); @@ -2043,6 +2072,58 @@ pub mod args { } } + /// Transaction to initialize a new account + #[derive(Clone, Debug)] + pub struct TxInitAccountMultiSignature { + /// Common tx arguments + pub tx: Tx, + /// Address of the source account + pub sources: Vec, + /// Path to the VP WASM code file for the new account + pub vp_code_path: Option, + /// Public key for the new account + pub public_keys: Vec, + /// The threshold for multsignature account + pub threshold: Option + } + + impl Args for TxInitAccountMultiSignature { + fn parse(matches: &ArgMatches) -> Self { + let tx = Tx::parse(matches); + let sources = SOURCE_MULTISIGNATURE.parse(matches); + println!("b: {:?}", sources); + let vp_code_path = CODE_PATH_OPT.parse(matches); + let public_keys = PUBLIC_KEY_MULTISIGNATURE.parse(matches); + let threshold = THRESHOLD.parse(matches); + Self { + tx, + sources, + vp_code_path, + public_keys, + threshold + } + } + + fn def(app: App) -> App { + app.add_args::() + .arg(SOURCE_MULTISIGNATURE.def().about( + "The source account's address that signs the transaction.", + ).required(true).min_values(1)) + .arg(CODE_PATH_OPT.def().about( + "The path to the validity predicate WASM code to be used \ + for the new account. Uses the default user VP if none \ + specified.", + )) + .arg(PUBLIC_KEY_MULTISIGNATURE.def().about( + "A public key to be used for the new account in \ + hexadecimal encoding.", + ).required(true).min_values(1)) + .arg(THRESHOLD.def().about( + "Multisgnature threshold.", + )) + } + } + /// Transaction to initialize a new account #[derive(Clone, Debug)] pub struct TxInitValidator { diff --git a/apps/src/lib/cli/utils.rs b/apps/src/lib/cli/utils.rs index 56965d72ef..88dea8616c 100644 --- a/apps/src/lib/cli/utils.rs +++ b/apps/src/lib/cli/utils.rs @@ -265,6 +265,7 @@ where .values_of(self.name) .unwrap_or_default() .map(|raw| { + println!("a: {}", raw); raw.parse().unwrap_or_else(|e| { eprintln!( "Failed to parse the {} argument. Raw value: {}, \ @@ -278,6 +279,19 @@ where } } +impl ArgMulti> { + + pub fn def(&self) -> ClapArg { + ClapArg::new(self.name).long(self.name).takes_value(true).multiple(true).require_delimiter(true) + } + + pub fn parse(&self, matches: &ArgMatches) -> Vec> { + let raw = matches.values_of(self.name).unwrap_or_default(); + println!("c: {}", raw.len()); + raw.map(|val| FromContext::new(val.to_string())).collect() + } +} + /// Extensions for defining commands and arguments. /// Every function here should have a matcher in [`ArgMatchesExt`]. pub trait AppExt { diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 0014833ca8..9b4d8e1400 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -182,7 +182,64 @@ pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { } pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { - let public_key = ctx.get_cached(&args.public_key); + // let public_key = ctx.get_cached(&args.public_key); + // let vp_code = args + // .vp_code_path + // .map(|path| ctx.read_wasm(path)) + // .unwrap_or_else(|| ctx.read_wasm(VP_USER_WASM)); + // // Validate the VP code + // if let Err(err) = vm::validate_untrusted_wasm(&vp_code) { + // eprintln!("Validity predicate code validation failed with {}", err); + // if !args.tx.force { + // safe_exit(1) + // } + // } + + // let tx_code = ctx.read_wasm(TX_INIT_ACCOUNT_WASM); + // let data = InitAccount { + // public_key, + // vp_code, + // }; + // let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); + + // let tx = Tx::new(tx_code, Some(data)); + // let (ctx, initialized_accounts) = process_tx( + // ctx, + // &args.tx, + // tx, + // TxSigningKey::WalletAddress(args.source), + // #[cfg(not(feature = "mainnet"))] + // false, + // ) + // .await; + // save_initialized_accounts(ctx, &args.tx, initialized_accounts).await; +} + +pub async fn submit_init_account_multisignature( + mut ctx: Context, + args: args::TxInitAccountMultiSignature, +) { + let public_keys: Vec = args + .public_keys + .iter() + .map(|pk| { + println!("{:?}", pk); + ctx.get_cached(pk) + }) + .collect(); + + let threshold = match args.threshold { + Some(threshold) => threshold, + None => { + if public_keys.len() == 1 { + 1 + } else { + eprintln!("Missing threshold for multisignature account."); + safe_exit(1) + } + } + }; + let vp_code = args .vp_code_path .map(|path| ctx.read_wasm(path)) @@ -197,22 +254,23 @@ pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { let tx_code = ctx.read_wasm(TX_INIT_ACCOUNT_WASM); let data = InitAccount { - public_key, + public_keys, vp_code, + threshold }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); let tx = Tx::new(tx_code, Some(data)); - let (ctx, initialized_accounts) = process_tx( - ctx, - &args.tx, - tx, - TxSigningKey::WalletAddress(args.source), - #[cfg(not(feature = "mainnet"))] - false, - ) - .await; - save_initialized_accounts(ctx, &args.tx, initialized_accounts).await; + // let (ctx, initialized_accounts) = process_tx( + // ctx, + // &args.tx, + // tx, + // TxSigningKey::WalletAddress(args.source), + // #[cfg(not(feature = "mainnet"))] + // false, + // ) + // .await; + // save_initialized_accounts(ctx, &args.tx, initialized_accounts).await; } pub async fn submit_init_validator( diff --git a/core/src/ledger/storage_api/key.rs b/core/src/ledger/storage_api/key.rs index 6e3eba64aa..9e2f7ba3c3 100644 --- a/core/src/ledger/storage_api/key.rs +++ b/core/src/ledger/storage_api/key.rs @@ -6,11 +6,21 @@ use crate::types::key::*; /// Get the public key associated with the given address. Returns `Ok(None)` if /// not found. -pub fn get(storage: &S, owner: &Address) -> Result> +pub fn get(storage: &S, owner: &Address, index: Option) -> Result> where S: StorageRead, { - let key = pk_key(owner); + let key = pk_key(owner, index.unwrap_or_else(|| 0)); + storage.read(&key) +} + +/// Get the public key associated with the given address. Returns `Ok(None)` if +/// not found. +pub fn threshold(storage: &S, owner: &Address) -> Result> +where + S: StorageRead, +{ + let key = threshold_key(owner); storage.read(&key) } @@ -21,6 +31,6 @@ where S: StorageWrite, { let addr: Address = pk.into(); - let key = pk_key(&addr); + let key = pk_key(&addr, 0); storage.write(&key, pk) } diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index 40e343d1bf..e6a8361ed4 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -50,7 +50,7 @@ pub struct SignedTxData { pub data: Option>, /// The signature is produced on the tx data concatenated with the tx code /// and the timestamp. - pub sig: common::Signature, + pub sigs: Vec<(u64, common::Signature)>, } /// A generic signed data wrapper for Borsh encode-able data. @@ -158,7 +158,7 @@ impl SigningTx { let sig = common::SigScheme::sign(keypair, to_sign); let signed = SignedTxData { data: self.data, - sig, + sigs, } .try_to_vec() .expect("Encoding transaction data shouldn't fail"); diff --git a/core/src/types/key/mod.rs b/core/src/types/key/mod.rs index 157b0f4f5b..20e340119c 100644 --- a/core/src/types/key/mod.rs +++ b/core/src/types/key/mod.rs @@ -22,14 +22,24 @@ use super::address::Address; use super::storage::{self, DbKeySeg, Key, KeySeg}; use crate::types::address; -const PK_STORAGE_KEY: &str = "public_key"; +const PK_STORAGE_KEY: &str = "public_keys"; const PROTOCOL_PK_STORAGE_KEY: &str = "protocol_public_key"; +const PK_STORAGE_THRESHOLD_KEY: &str = "threshold"; /// Obtain a storage key for user's public key. -pub fn pk_key(owner: &Address) -> storage::Key { +pub fn pk_key(owner: &Address, index: usize) -> storage::Key { Key::from(owner.to_db_key()) .push(&PK_STORAGE_KEY.to_owned()) .expect("Cannot obtain a storage key") + .push(&(index as u64)) // this should be fine if the architecture is 64bit + .expect("Cannot obtain a storage key") +} + +/// Obtain a storage key for user's threshold. +pub fn threshold_key(owner: &Address) -> storage::Key { + Key::from(owner.to_db_key()) + .push(&PK_STORAGE_THRESHOLD_KEY.to_owned()) + .expect("Cannot obtain a storage key") } /// Check if the given storage key is a public key. If it is, returns the owner. @@ -44,6 +54,18 @@ pub fn is_pk_key(key: &Key) -> Option<&Address> { } } +/// Check if the given storage key is a public key. If it is, returns the owner. +pub fn is_threshold_key(key: &Key) -> Option<&Address> { + match &key.segments[..] { + [DbKeySeg::AddressSeg(owner), DbKeySeg::StringSeg(key)] + if key == PK_STORAGE_THRESHOLD_KEY => + { + Some(owner) + } + _ => None, + } +} + /// Obtain a storage key for user's protocol public key. pub fn protocol_pk_key(owner: &Address) -> storage::Key { Key::from(owner.to_db_key()) diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index 0e0a5e980e..3fbd5ef110 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -162,9 +162,11 @@ pub struct InitAccount { /// Public key to be written into the account's storage. This can be used /// for signature verification of transactions for the newly created /// account. - pub public_key: common::PublicKey, + pub public_keys: Vec, /// The VP code pub vp_code: Vec, + /// The multisignature threshold + pub threshold: u64 } /// A tx data type to initialize a new validator account. diff --git a/tx_prelude/src/account.rs b/tx_prelude/src/account.rs new file mode 100644 index 0000000000..0f4b685582 --- /dev/null +++ b/tx_prelude/src/account.rs @@ -0,0 +1,18 @@ +use namada_core::types::transaction::InitAccount; +use namada_core::types::key; + +use super::*; + +pub fn init_account(ctx: &mut Ctx, data: InitAccount) -> TxResult { + let address = ctx.init_account(&data.vp_code)?; + + let pk_threshold = key::threshold_key(&address); + ctx.write(&pk_threshold, &data.threshold)?; + + for (index, pk) in data.public_keys.iter().enumerate() { + let pk_key = key::pk_key(&address, index); + ctx.write(&pk_key, pk)?; + } + + Ok(()) +} \ No newline at end of file diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index d7d6b84c96..da5db06017 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -10,6 +10,7 @@ pub mod ibc; pub mod key; pub mod proof_of_stake; pub mod token; +pub mod account; use core::slice; use std::marker::PhantomData; diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index 6e4c4cf136..26198eafca 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -78,7 +78,7 @@ impl Ctx { let current_epoch = self.get_block_epoch()?; // Init validator account let validator_address = self.init_account(&validator_vp_code)?; - let pk_key = key::pk_key(&validator_address); + let pk_key = key::pk_key(&validator_address, 0); self.write(&pk_key, &account_key)?; let protocol_pk_key = key::protocol_pk_key(&validator_address); self.write(&protocol_pk_key, &protocol_key)?; diff --git a/vm_env/src/common.rs b/vm_env/src/common.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/wasm/wasm_source/src/tx_init_account.rs b/wasm/wasm_source/src/tx_init_account.rs index e0fe700d63..6e797f0156 100644 --- a/wasm/wasm_source/src/tx_init_account.rs +++ b/wasm/wasm_source/src/tx_init_account.rs @@ -12,8 +12,5 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { .wrap_err("failed to decode InitAccount")?; debug_log!("apply_tx called to init a new established account"); - let address = ctx.init_account(&tx_data.vp_code)?; - let pk_key = key::pk_key(&address); - ctx.write(&pk_key, &tx_data.public_key)?; - Ok(()) + account::init_account(ctx, tx_data) } diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index b8cbc20982..86723c0316 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -69,16 +69,21 @@ fn validate_tx( let valid_sig = Lazy::new(|| match &*signed_tx_data { Ok(signed_tx_data) => { - let pk = key::get(ctx, &addr); - match pk { - Ok(Some(pk)) => { - matches!( - ctx.verify_tx_signature(&pk, &signed_tx_data.sig), - Ok(true) - ) + let threshold = key::threshold(ctx, &addr); + if signed_tx_data.sigs.len() < threshold { + return false; + } + let valid_signatures = 0; + for (index, signature) in signed_tx_data.sigs { + let pk = key::get(&addr, index); + if let Some(public_key) = pk { + let signature_result = ctx.verify_tx_signature(&public_key, &signature).unwrap_or_else(|| false); + if signature_result { + valid_signatures += 1; + } } - _ => false, } + return valid_signatures >= threshold; } _ => false, }); From b8a0de288370ebadf11fa652ef8b76c7215dd5c7 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Thu, 26 Jan 2023 13:16:35 +0100 Subject: [PATCH 02/27] wip --- apps/src/lib/cli.rs | 32 +++--- apps/src/lib/client/rpc.rs | 28 ++++- apps/src/lib/client/signing.rs | 101 ++++++++++++++++++- apps/src/lib/client/tx.rs | 80 +++++++++++++-- apps/src/lib/client/types.rs | 4 +- apps/src/lib/node/ledger/shell/init_chain.rs | 6 +- apps/src/lib/node/ledger/shell/mod.rs | 2 +- core/src/ledger/storage_api/key.rs | 4 +- core/src/proto/types.rs | 82 ++++++++++++++- core/src/types/key/mod.rs | 11 +- core/src/types/transaction/mod.rs | 15 ++- tests/src/vm_host_env/tx.rs | 3 +- tx_prelude/src/account.rs | 2 +- vp_prelude/src/key.rs | 4 +- 14 files changed, 319 insertions(+), 55 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index d2e29e6fce..481491b0a0 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1697,9 +1697,8 @@ pub mod args { const RECEIVER: Arg = arg("receiver"); const SCHEME: ArgDefault = arg_default("scheme", DefaultFn(|| SchemeType::Ed25519)); - const SIGNER: ArgOpt = arg_opt("signer"); - const SIGNING_KEY_OPT: ArgOpt = SIGNING_KEY.opt(); - const SIGNING_KEY: Arg = arg("signing-key"); + const SIGNERS: ArgMulti = arg_multi("signers"); + const SIGNING_KEYS: ArgMulti = arg_multi("signing-keys"); const SOURCE: Arg = arg("source"); const SOURCE_MULTISIGNATURE: ArgMulti = arg_multi("sources"); const SOURCE_OPT: ArgOpt = SOURCE.opt(); @@ -2945,9 +2944,9 @@ pub mod args { /// The max amount of gas used to process tx pub gas_limit: GasLimit, /// Sign the tx with the key for the given alias from your wallet - pub signing_key: Option, + pub signing_keys: Vec, /// Sign the tx with the keypair of the public key of the given address - pub signer: Option, + pub signers: Vec, } impl Tx { @@ -2964,11 +2963,8 @@ pub mod args { fee_amount: self.fee_amount, fee_token: ctx.get(&self.fee_token), gas_limit: self.gas_limit.clone(), - signing_key: self - .signing_key - .as_ref() - .map(|sk| ctx.get_cached(sk)), - signer: self.signer.as_ref().map(|signer| ctx.get(signer)), + signing_keys: self.signing_keys.iter().map(|sk| ctx.get_cached(sk)).collect(), + signers: self.signers.iter().map(|signer| ctx.get(signer)).collect(), } } } @@ -3005,23 +3001,23 @@ pub mod args { ), ) .arg( - SIGNING_KEY_OPT + SIGNING_KEYS .def() .about( "Sign the transaction with the key for the given \ public key, public key hash or alias from your \ wallet.", ) - .conflicts_with(SIGNER.name), + .conflicts_with(SIGNERS.name), ) .arg( - SIGNER + SIGNERS .def() .about( "Sign the transaction with the keypair of the public \ key of the given address.", ) - .conflicts_with(SIGNING_KEY_OPT.name), + .conflicts_with(SIGNING_KEYS.name), ) } @@ -3036,8 +3032,8 @@ pub mod args { let fee_token = GAS_TOKEN.parse(matches); let gas_limit = GAS_LIMIT.parse(matches).into(); - let signing_key = SIGNING_KEY_OPT.parse(matches); - let signer = SIGNER.parse(matches); + let signing_keys = SIGNING_KEYS.parse(matches); + let signers = SIGNERS.parse(matches); Self { dry_run, dump_tx, @@ -3048,8 +3044,8 @@ pub mod args { fee_amount, fee_token, gas_limit, - signing_key, - signer, + signing_keys, + signers, } } } diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 4dd36ea2eb..1826a6812d 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -1259,6 +1259,7 @@ pub async fn query_proposal_result( let public_key = get_public_key( &proposal.address, + 0, args.query.ledger_address.clone(), ) .await @@ -1745,10 +1746,11 @@ pub async fn dry_run_tx(ledger_address: &TendermintAddress, tx_bytes: Vec) { /// Get account's public key stored in its storage sub-space pub async fn get_public_key( address: &Address, + index: u64, ledger_address: TendermintAddress, ) -> Option { let client = HttpClient::new(ledger_address).unwrap(); - let key = pk_key(address); + let key = pk_key(address, index); query_storage_value(&client, &key).await } @@ -2278,7 +2280,7 @@ pub async fn get_proposal_offline_votes( let proposal_vote: OfflineVote = serde_json::from_reader(file) .expect("JSON was not well-formatted for offline vote."); - let key = pk_key(&proposal_vote.address); + let key = pk_key(&proposal_vote.address, 0); let public_key = query_storage_value(client, &key) .await .expect("Public key should exist."); @@ -2554,6 +2556,26 @@ pub async fn get_delegators_delegation( ) } +pub async fn get_address_pks_map( + client: &HttpClient, + address: &Address +) -> HashMap { + let pk_prefix = pk_prefix_key(address); + let pk_iter = + query_storage_prefix::(client, &pk_prefix).await; + + if let Some(pks) = pk_iter { + let mut pks_map = HashMap::new(); + pks.enumerate().map(|(index, (_, pk))| { + pks_map.insert(pk, index as u64) + }); + + pks_map + } else { + HashMap::new() + } +} + pub async fn get_governance_parameters(client: &HttpClient) -> GovParams { use namada::types::token::Amount; let key = gov_storage::get_max_proposal_code_size_key(); @@ -2611,4 +2633,4 @@ fn unwrap_client_response(response: Result) -> T { eprintln!("Error in the query {}", err); cli::safe_exit(1) }) -} +} \ No newline at end of file diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index 9b1a00b987..5aa4a989b6 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -1,7 +1,10 @@ //! Helpers for making digital signatures using cryptographic keys from the //! wallet. +use std::collections::HashMap; + use borsh::BorshSerialize; +use futures::future::join_all; use namada::ledger::parameters::storage as parameter_storage; use namada::proto::Tx; use namada::types::address::{Address, ImplicitAddress}; @@ -33,7 +36,7 @@ pub async fn find_keypair( "Looking-up public key of {} from the ledger...", addr.encode() ); - let public_key = rpc::get_public_key(addr, ledger_address) + let public_key = rpc::get_public_key(addr, 0, ledger_address) .await .unwrap_or_else(|| { eprintln!( @@ -96,9 +99,9 @@ pub async fn tx_signer( mut default: TxSigningKey, ) -> common::SecretKey { // Override the default signing key source if possible - if let Some(signing_key) = &args.signing_key { + if let Some(signing_key) = args.signing_keys.get(0) { default = TxSigningKey::WalletKeypair(signing_key.clone()); - } else if let Some(signer) = &args.signer { + } else if let Some(signer) = args.signers.get(0) { default = TxSigningKey::WalletAddress(signer.clone()); } // Now actually fetch the signing key and apply it @@ -137,6 +140,66 @@ pub async fn tx_signer( } } +pub async fn tx_signers( + ctx: &mut Context, + args: &args::Tx, + mut default: Vec, +) -> Vec { + if !args.signing_keys.is_empty() { + default = args + .signing_keys + .iter() + .map(|signing_key| TxSigningKey::WalletKeypair(signing_key.clone())) + .collect(); + } else if !args.signers.is_empty() { + default = args + .signers + .iter() + .map(|signing_key| TxSigningKey::WalletAddress(signing_key.clone())) + .collect(); + } + + let mut keys = Vec::new(); + + for key in default { + match key { + TxSigningKey::WalletKeypair(signing_key) => { + keys.push(ctx.get_cached(&signing_key)); + } + TxSigningKey::WalletAddress(signer) => { + let signer = ctx.get(&signer); + let signing_key = find_keypair( + &mut ctx.wallet, + &signer, + args.ledger_address.clone(), + ) + .await; + // Check if the signer is implicit account that needs to reveal its + // PK first + if matches!(signer, Address::Implicit(_)) { + let pk: common::PublicKey = signing_key.ref_to(); + super::tx::reveal_pk_if_needed(ctx, &pk, args).await; + } + keys.push(signing_key); + } + TxSigningKey::SecretKey(signing_key) => { + // Check if the signing key needs to reveal its PK first + let pk: common::PublicKey = signing_key.ref_to(); + super::tx::reveal_pk_if_needed(ctx, &pk, args).await; + keys.push(signing_key); + } + TxSigningKey::None => { + panic!( + "All transactions must be signed; please either specify the \ + key or the address from which to look up the signing key." + ); + } + } + } + + keys +} + /// Sign a transaction with a given signing key or public key of a given signer. /// If no explicit signer given, use the `default`. If no `default` is given, /// panics. @@ -224,6 +287,38 @@ pub fn dump_tx_helper( .expect("expected to be able to write tx dump file"); } +pub async fn sign_tx_multisignature( + mut ctx: Context, + tx: Tx, + args: &args::Tx, + pks_index_map: HashMap, + default: Vec, + #[cfg(not(feature = "mainnet"))] requires_pow: bool, +) -> (Context, TxBroadcastData) { + let keypairs = tx_signers(&mut ctx, args, default).await; + let tx = tx.sign_multisignature(&keypairs, pks_index_map); + + let epoch = rpc::query_epoch(args::Query { + ledger_address: args.ledger_address.clone(), + }) + .await; + let broadcast_data = if args.dry_run { + TxBroadcastData::DryRun(tx) + } else { + sign_wrapper( + &ctx, + args, + epoch, + tx, + &keypairs[0], + #[cfg(not(feature = "mainnet"))] + requires_pow, + ) + .await + }; + (ctx, broadcast_data) +} + /// Create a wrapper tx from a normal tx. Get the hash of the /// wrapper and its payload which is needed for monitoring its /// progress on chain. diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 9b4d8e1400..337bb5407e 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -7,6 +7,7 @@ use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; use std::ops::Deref; use std::path::PathBuf; +use itertools::Itertools; use async_std::io::prelude::WriteExt; use async_std::io::{self}; @@ -226,6 +227,7 @@ pub async fn submit_init_account_multisignature( println!("{:?}", pk); ctx.get_cached(pk) }) + .sorted() .collect(); let threshold = match args.threshold { @@ -254,12 +256,17 @@ pub async fn submit_init_account_multisignature( let tx_code = ctx.read_wasm(TX_INIT_ACCOUNT_WASM); let data = InitAccount { - public_keys, + public_keys: public_keys.clone(), vp_code, - threshold + threshold, }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); + let mut pks_map = HashMap::new(); + for (index, pk) in public_keys.iter().enumerate() { + pks_map.insert(pk, index as u64); + } + let tx = Tx::new(tx_code, Some(data)); // let (ctx, initialized_accounts) = process_tx( // ctx, @@ -2015,7 +2022,8 @@ pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { } pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { - let signer = if let Some(addr) = &args.tx.signer { + // TODO: fix me + let signer = if let Some(addr) = args.tx.signers.get(0) { addr } else { eprintln!("Missing mandatory argument --signer."); @@ -2032,6 +2040,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { serde_json::from_reader(file).expect("JSON was not well-formatted"); let public_key = rpc::get_public_key( &proposal.address, + 0, args.tx.ledger_address.clone(), ) .await @@ -2198,7 +2207,7 @@ pub async fn has_revealed_pk( addr: &Address, ledger_address: TendermintAddress, ) -> bool { - rpc::get_public_key(addr, ledger_address).await.is_some() + rpc::get_public_key(addr, 0, ledger_address).await.is_some() } pub async fn submit_reveal_pk_aux( @@ -2215,9 +2224,9 @@ pub async fn submit_reveal_pk_aux( let tx = Tx::new(tx_code, Some(tx_data)); // submit_tx without signing the inner tx - let keypair = if let Some(signing_key) = &args.signing_key { + let keypair = if let Some(signing_key) = &args.signing_keys.get(0) { ctx.get_cached(signing_key) - } else if let Some(signer) = args.signer.as_ref() { + } else if let Some(signer) = args.signers.get(0) { let signer = ctx.get(signer); find_keypair(&mut ctx.wallet, &signer, args.ledger_address.clone()) .await @@ -2634,6 +2643,65 @@ pub async fn submit_validator_commission_change( .await; } +async fn process_tx_multisignature( + ctx: Context, + args: &args::Tx, + tx: Tx, + pks_index_map: HashMap, + default_signer: Vec, + #[cfg(not(feature = "mainnet"))] requires_pow: bool, +) { + // let (ctx, to_broadcast) = sign_tx_multisignature( + // ctx, + // tx, + // args, + // default_signer, + // #[cfg(not(feature = "mainnet"))] + // requires_pow, + // ) + // .await; + + // if args.dry_run { + // if let TxBroadcastData::DryRun(tx) = to_broadcast { + // rpc::dry_run_tx(&args.ledger_address, tx.to_bytes()).await; + // (ctx, vec![]) + // } else { + // panic!( + // "Expected a dry-run transaction, received a wrapper \ + // transaction instead" + // ); + // } + // } else { + // // Either broadcast or submit transaction and collect result into + // // sum type + // let result = if args.broadcast_only { + // Left(broadcast_tx(args.ledger_address.clone(), &to_broadcast).await) + // } else { + // Right(submit_tx(args.ledger_address.clone(), to_broadcast).await) + // }; + // // Return result based on executed operation, otherwise deal with + // // the encountered errors uniformly + // match result { + // Right(Ok(result)) => (ctx, result.initialized_accounts), + // Left(Ok(_)) => (ctx, Vec::default()), + // Right(Err(err)) => { + // eprintln!( + // "Encountered error while broadcasting transaction: {}", + // err + // ); + // safe_exit(1) + // } + // Left(Err(err)) => { + // eprintln!( + // "Encountered error while broadcasting transaction: {}", + // err + // ); + // safe_exit(1) + // } + // } + // } +} + /// Submit transaction and wait for result. Returns a list of addresses /// initialized in the transaction if any. In dry run, this is always empty. async fn process_tx( diff --git a/apps/src/lib/client/types.rs b/apps/src/lib/client/types.rs index d75d5a596c..feb3bcdcdb 100644 --- a/apps/src/lib/client/types.rs +++ b/apps/src/lib/client/types.rs @@ -36,9 +36,9 @@ pub struct ParsedTxArgs { /// The max amount of gas used to process tx pub gas_limit: GasLimit, /// Sign the tx with the key for the given alias from your wallet - pub signing_key: Option, + pub signing_keys: Vec, /// Sign the tx with the keypair of the public key of the given address - pub signer: Option
, + pub signers: Vec
, } #[derive(Clone, Debug)] diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index fec7864306..b912d96dee 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -195,7 +195,7 @@ where .unwrap(); if let Some(pk) = public_key { - let pk_storage_key = pk_key(&address); + let pk_storage_key = pk_key(&address, 0); self.wl_storage .write_bytes(&pk_storage_key, pk.try_to_vec().unwrap()) .unwrap(); @@ -228,7 +228,7 @@ where for genesis::ImplicitAccount { public_key } in genesis.implicit_accounts { let address: address::Address = (&public_key).into(); - let pk_storage_key = pk_key(&address); + let pk_storage_key = pk_key(&address, 0); self.wl_storage.write(&pk_storage_key, public_key).unwrap(); } @@ -304,7 +304,7 @@ where .write_bytes(&Key::validity_predicate(addr), vp_code) .expect("Unable to write user VP"); // Validator account key - let pk_key = pk_key(addr); + let pk_key = pk_key(addr, 0); self.wl_storage .write(&pk_key, &validator.account_key) .expect("Unable to set genesis user public key"); diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 6b4b05b5ad..a8491816a7 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -684,7 +684,7 @@ where self.mode.get_validator_address().map(|addr| { let sk: common::SecretKey = self .wl_storage - .read(&pk_key(addr)) + .read(&pk_key(addr, 0)) .expect( "A validator should have a public key associated with \ it's established account", diff --git a/core/src/ledger/storage_api/key.rs b/core/src/ledger/storage_api/key.rs index 9e2f7ba3c3..8676c9cb9f 100644 --- a/core/src/ledger/storage_api/key.rs +++ b/core/src/ledger/storage_api/key.rs @@ -6,11 +6,11 @@ use crate::types::key::*; /// Get the public key associated with the given address. Returns `Ok(None)` if /// not found. -pub fn get(storage: &S, owner: &Address, index: Option) -> Result> +pub fn get(storage: &S, owner: &Address, index: u64) -> Result> where S: StorageRead, { - let key = pk_key(owner, index.unwrap_or_else(|| 0)); + let key = pk_key(owner, index); storage.read(&key) } diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index e6a8361ed4..8601caca91 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::hash::{Hash, Hasher}; @@ -37,6 +38,27 @@ pub enum Error { pub type Result = std::result::Result; +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema)] +pub struct SignatureIndex { + pub sig: common::Signature, + pub index: u64 +} + +impl SignatureIndex { + + pub fn from_single_signature(sig: common::Signature) -> Self { + Self { + sig, + index: 0, + } + } + + pub fn to_vec(&self) -> Vec { + return vec![self.clone()] + } + +} + /// This can be used to sign an arbitrary tx. The signature is produced and /// verified on the tx data concatenated with the tx code, however the tx code /// itself is not part of this structure. @@ -50,7 +72,20 @@ pub struct SignedTxData { pub data: Option>, /// The signature is produced on the tx data concatenated with the tx code /// and the timestamp. - pub sigs: Vec<(u64, common::Signature)>, + pub sigs: Vec, +} + +impl SignedTxData { + + pub fn get_signature_by_index(&self, index: u64) -> Option { + for signature in &self.sigs { + if signature.index == index { + return Some(signature.sig.clone()); + } + } + return None; + } + } /// A generic signed data wrapper for Borsh encode-able data. @@ -158,7 +193,7 @@ impl SigningTx { let sig = common::SigScheme::sign(keypair, to_sign); let signed = SignedTxData { data: self.data, - sigs, + sigs: SignatureIndex::from_single_signature(sig).to_vec() } .try_to_vec() .expect("Encoding transaction data shouldn't fail"); @@ -169,6 +204,33 @@ impl SigningTx { } } + pub fn sign_multisignature(self, keypairs: &Vec, pks_index_map: HashMap) -> Self { + let to_sign = self.hash(); + let signed = SignedTxData { + data: self.data, + sigs: keypairs.iter().filter_map(|key| { + let signature = common::SigScheme::sign(key, to_sign); + let pk = key.ref_to(); + let pk_index = pks_index_map.get(&pk); + if let Some(index) = pk_index { + Some(SignatureIndex { + sig: signature, + index: index.clone() + }) + } else { + None + } + }).collect() + }.try_to_vec() + .expect("Encoding transaction data shouldn't fail"); + + SigningTx { + code_hash: self.code_hash, + data: Some(signed), + timestamp: self.timestamp, + } + } + /// Verify that the transaction has been signed by the secret key /// counterpart of the given public key. pub fn verify_sig( @@ -375,6 +437,14 @@ impl Tx { .expect("code hashes to unexpected value") } + pub fn sign_multisignature(self, keypairs: &Vec, pks_index_map: HashMap) -> Self { + let code = self.code.clone(); + SigningTx::from(self) + .sign_multisignature(keypairs, pks_index_map) + .expand(code) + .expect("code hashes to unexpected value") + } + /// Verify that the transaction has been signed by the secret key /// counterpart of the given public key. pub fn verify_sig( @@ -384,6 +454,14 @@ impl Tx { ) -> std::result::Result<(), VerifySigError> { SigningTx::from(self.clone()).verify_sig(pk, sig) } + + // pub fn verify_sig_multisignature( + // &self, + // pk: &Vec, + // sig: &Vec, + // ) -> std::result::Result<(), VerifySigError> { + // SigningTx::from(self.clone()).verify_sig(pk, sig) + // } } #[allow(dead_code)] diff --git a/core/src/types/key/mod.rs b/core/src/types/key/mod.rs index 20e340119c..1f6f2bdfcb 100644 --- a/core/src/types/key/mod.rs +++ b/core/src/types/key/mod.rs @@ -27,11 +27,18 @@ const PROTOCOL_PK_STORAGE_KEY: &str = "protocol_public_key"; const PK_STORAGE_THRESHOLD_KEY: &str = "threshold"; /// Obtain a storage key for user's public key. -pub fn pk_key(owner: &Address, index: usize) -> storage::Key { +pub fn pk_key(owner: &Address, index: u64) -> storage::Key { Key::from(owner.to_db_key()) .push(&PK_STORAGE_KEY.to_owned()) .expect("Cannot obtain a storage key") - .push(&(index as u64)) // this should be fine if the architecture is 64bit + .push(&index) // this should be fine if the architecture is 64bit + .expect("Cannot obtain a storage key") +} + +/// Obtain a storage key for user's public key. +pub fn pk_prefix_key(owner: &Address) -> storage::Key { + Key::from(owner.to_db_key()) + .push(&PK_STORAGE_KEY.to_owned()) .expect("Cannot obtain a storage key") } diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index 3fbd5ef110..5fc4f79c31 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -286,35 +286,34 @@ pub mod tx_types { /// indicating it is a wrapper. Otherwise, an error is /// returned indicating the signature was not valid pub fn process_tx(tx: Tx) -> Result { - if let Some(Ok(SignedTxData { - data: Some(data), - ref sig, - })) = tx + if let Some(Ok(tx_data)) = tx .data .as_ref() .map(|data| SignedTxData::try_from_slice(&data[..])) { let signed_hash = Tx { code: tx.code, - data: Some(data.clone()), + data: tx_data.data.clone(), timestamp: tx.timestamp, } .hash(); match TxType::try_from(Tx { code: vec![], - data: Some(data), + data: tx_data.data.clone(), timestamp: tx.timestamp, }) .map_err(|err| TxError::Deserialization(err.to_string()))? { // verify signature and extract signed data TxType::Wrapper(wrapper) => { - wrapper.validate_sig(signed_hash, sig)?; + let sig = tx_data.clone().get_signature_by_index(0).ok_or(TxError::SigError("Unsigned wrappr".to_string()))?; + wrapper.validate_sig(signed_hash, &sig)?; Ok(TxType::Wrapper(wrapper)) } // verify signature and extract signed data TxType::Protocol(protocol) => { - protocol.validate_sig(signed_hash, sig)?; + let sig = tx_data.clone().get_signature_by_index(0).ok_or(TxError::SigError("Unsigned protocol".to_string()))?; + protocol.validate_sig(signed_hash, &sig)?; Ok(TxType::Protocol(protocol)) } // we extract the signed data, but don't check the signature diff --git a/tests/src/vm_host_env/tx.rs b/tests/src/vm_host_env/tx.rs index 6c3ccd55ae..c20df1e467 100644 --- a/tests/src/vm_host_env/tx.rs +++ b/tests/src/vm_host_env/tx.rs @@ -188,8 +188,7 @@ impl TestTxEnv { public_key: &key::common::PublicKey, ) { let storage_key = key::pk_key(address); - self.wl_storage - .storage + self.storage .write(&storage_key, public_key.try_to_vec().unwrap()) .unwrap(); } diff --git a/tx_prelude/src/account.rs b/tx_prelude/src/account.rs index 0f4b685582..66dccfcd89 100644 --- a/tx_prelude/src/account.rs +++ b/tx_prelude/src/account.rs @@ -9,7 +9,7 @@ pub fn init_account(ctx: &mut Ctx, data: InitAccount) -> TxResult { let pk_threshold = key::threshold_key(&address); ctx.write(&pk_threshold, &data.threshold)?; - for (index, pk) in data.public_keys.iter().enumerate() { + for (pk, index) in data.public_keys.iter().zip(0u64..) { let pk_key = key::pk_key(&address, index); ctx.write(&pk_key, pk)?; } diff --git a/vp_prelude/src/key.rs b/vp_prelude/src/key.rs index 95946ff268..3213833693 100644 --- a/vp_prelude/src/key.rs +++ b/vp_prelude/src/key.rs @@ -7,6 +7,6 @@ use super::*; /// Get the public key associated with the given address from the state prior to /// tx execution. Returns `Ok(None)` if not found. -pub fn get(ctx: &Ctx, owner: &Address) -> EnvResult> { - storage_api::key::get(&ctx.pre(), owner) +pub fn get(ctx: &Ctx, owner: &Address, index: u64) -> EnvResult> { + storage_api::key::get(&ctx.pre(), owner, index) } From ad5e4432165d2e9058d93867d6d37e82b97913c5 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Fri, 27 Jan 2023 17:51:39 +0100 Subject: [PATCH 03/27] wip --- apps/src/lib/client/tx.rs | 125 +++++++++--------- .../lib/node/ledger/shell/process_proposal.rs | 4 +- core/src/ledger/storage_api/key.rs | 2 +- core/src/proto/types.rs | 12 +- core/src/types/transaction/mod.rs | 3 +- core/src/types/transaction/wrapper.rs | 2 +- tests/src/vm_host_env/mod.rs | 6 +- tx_prelude/src/account.rs | 8 +- vp_prelude/src/key.rs | 7 + wasm/wasm_source/src/vp_implicit.rs | 72 +++++----- wasm/wasm_source/src/vp_testnet_faucet.rs | 24 ++-- wasm/wasm_source/src/vp_user.rs | 18 ++- wasm/wasm_source/src/vp_validator.rs | 24 ++-- 13 files changed, 172 insertions(+), 135 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 337bb5407e..9046f1e537 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -66,6 +66,7 @@ use sha2::Digest; use tokio::time::{Duration, Instant}; use super::rpc; +use super::signing::sign_tx_multisignature; use super::types::ShieldedTransferContext; use crate::cli::context::WalletAddress; use crate::cli::{args, safe_exit, Context}; @@ -264,20 +265,21 @@ pub async fn submit_init_account_multisignature( let mut pks_map = HashMap::new(); for (index, pk) in public_keys.iter().enumerate() { - pks_map.insert(pk, index as u64); + pks_map.insert(pk.clone(), index as u64); } let tx = Tx::new(tx_code, Some(data)); - // let (ctx, initialized_accounts) = process_tx( - // ctx, - // &args.tx, - // tx, - // TxSigningKey::WalletAddress(args.source), - // #[cfg(not(feature = "mainnet"))] - // false, - // ) - // .await; - // save_initialized_accounts(ctx, &args.tx, initialized_accounts).await; + let (ctx, initialized_accounts) = process_tx_multisignature( + ctx, + &args.tx, + tx, + pks_map, + args.sources.iter().map(|source| TxSigningKey::WalletAddress(source.clone())).collect(), + #[cfg(not(feature = "mainnet"))] + false, + ) + .await; + save_initialized_accounts(ctx, &args.tx, initialized_accounts).await; } pub async fn submit_init_validator( @@ -2648,58 +2650,59 @@ async fn process_tx_multisignature( args: &args::Tx, tx: Tx, pks_index_map: HashMap, - default_signer: Vec, + default_signers: Vec, #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) { - // let (ctx, to_broadcast) = sign_tx_multisignature( - // ctx, - // tx, - // args, - // default_signer, - // #[cfg(not(feature = "mainnet"))] - // requires_pow, - // ) - // .await; +) -> (Context, Vec
) { + let (ctx, to_broadcast) = sign_tx_multisignature( + ctx, + tx, + args, + pks_index_map, + default_signers, + #[cfg(not(feature = "mainnet"))] + requires_pow, + ) + .await; - // if args.dry_run { - // if let TxBroadcastData::DryRun(tx) = to_broadcast { - // rpc::dry_run_tx(&args.ledger_address, tx.to_bytes()).await; - // (ctx, vec![]) - // } else { - // panic!( - // "Expected a dry-run transaction, received a wrapper \ - // transaction instead" - // ); - // } - // } else { - // // Either broadcast or submit transaction and collect result into - // // sum type - // let result = if args.broadcast_only { - // Left(broadcast_tx(args.ledger_address.clone(), &to_broadcast).await) - // } else { - // Right(submit_tx(args.ledger_address.clone(), to_broadcast).await) - // }; - // // Return result based on executed operation, otherwise deal with - // // the encountered errors uniformly - // match result { - // Right(Ok(result)) => (ctx, result.initialized_accounts), - // Left(Ok(_)) => (ctx, Vec::default()), - // Right(Err(err)) => { - // eprintln!( - // "Encountered error while broadcasting transaction: {}", - // err - // ); - // safe_exit(1) - // } - // Left(Err(err)) => { - // eprintln!( - // "Encountered error while broadcasting transaction: {}", - // err - // ); - // safe_exit(1) - // } - // } - // } + if args.dry_run { + if let TxBroadcastData::DryRun(tx) = to_broadcast { + rpc::dry_run_tx(&args.ledger_address, tx.to_bytes()).await; + (ctx, vec![]) + } else { + panic!( + "Expected a dry-run transaction, received a wrapper \ + transaction instead" + ); + } + } else { + // Either broadcast or submit transaction and collect result into + // sum type + let result = if args.broadcast_only { + Left(broadcast_tx(args.ledger_address.clone(), &to_broadcast).await) + } else { + Right(submit_tx(args.ledger_address.clone(), to_broadcast).await) + }; + // Return result based on executed operation, otherwise deal with + // the encountered errors uniformly + match result { + Right(Ok(result)) => (ctx, result.initialized_accounts), + Left(Ok(_)) => (ctx, Vec::default()), + Right(Err(err)) => { + eprintln!( + "Encountered error while broadcasting transaction: {}", + err + ); + safe_exit(1) + } + Left(Err(err)) => { + eprintln!( + "Encountered error while broadcasting transaction: {}", + err + ); + safe_exit(1) + } + } + } } /// Submit transaction and wait for result. Returns a list of addresses diff --git a/apps/src/lib/node/ledger/shell/process_proposal.rs b/apps/src/lib/node/ledger/shell/process_proposal.rs index 11deec9e13..d6bfff1eda 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -302,7 +302,7 @@ mod test_process_proposal { .expect("Test failed"); let new_tx = if let Some(Ok(SignedTxData { data: Some(data), - sig, + sigs, })) = wrapper .data .take() @@ -326,8 +326,8 @@ mod test_process_proposal { code: vec![], data: Some( SignedTxData { - sig, data: Some(new_data), + sigs: sigs } .try_to_vec() .expect("Test failed"), diff --git a/core/src/ledger/storage_api/key.rs b/core/src/ledger/storage_api/key.rs index 8676c9cb9f..54484ffcc2 100644 --- a/core/src/ledger/storage_api/key.rs +++ b/core/src/ledger/storage_api/key.rs @@ -16,7 +16,7 @@ where /// Get the public key associated with the given address. Returns `Ok(None)` if /// not found. -pub fn threshold(storage: &S, owner: &Address) -> Result> +pub fn threshold(storage: &S, owner: &Address) -> Result> where S: StorageRead, { diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index 8601caca91..250bbe1958 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -86,6 +86,10 @@ impl SignedTxData { return None; } + pub fn total_signatures(&self) -> u64 { + self.sigs.len() as u64 + } + } /// A generic signed data wrapper for Borsh encode-able data. @@ -454,14 +458,6 @@ impl Tx { ) -> std::result::Result<(), VerifySigError> { SigningTx::from(self.clone()).verify_sig(pk, sig) } - - // pub fn verify_sig_multisignature( - // &self, - // pk: &Vec, - // sig: &Vec, - // ) -> std::result::Result<(), VerifySigError> { - // SigningTx::from(self.clone()).verify_sig(pk, sig) - // } } #[allow(dead_code)] diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index 5fc4f79c31..56b6f3ef6d 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -533,7 +533,8 @@ pub mod tx_types { .try_to_vec() .expect("Test failed"), ), - sig: common::Signature::try_from_sig(&ed_sig).unwrap(), + sigs: vec![] + // sig: common::Signature::try_from_sig(&ed_sig).unwrap(), }; // create the tx with signed decrypted data let tx = diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index 70ef2827bc..282bfc9f8b 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -488,7 +488,7 @@ pub mod wrapper_tx { tx.data = Some(signed_tx_data.try_to_vec().expect("Test failed")); // check that the signature is not valid - tx.verify_sig(&keypair.ref_to(), &signed_tx_data.sig) + tx.verify_sig(&keypair.ref_to(), &signed_tx_data.sigs.get(0).unwrap().sig) .expect_err("Test failed"); // check that the try from method also fails let err = crate::types::transaction::process_tx(tx) diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index 04a545e8b1..595adc6b57 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -434,7 +434,7 @@ mod tests { let addr = address::testing::established_address_1(); // Write the public key to storage - let pk_key = key::pk_key(&addr); + let pk_key = key::pk_key(&addr, 0); let keypair = key::testing::keypair_1(); let pk = keypair.ref_to(); env.wl_storage @@ -461,7 +461,7 @@ mod tests { assert_eq!(&signed_tx_data.data, data); assert!( vp::CTX - .verify_tx_signature(&pk, &signed_tx_data.sig) + .verify_tx_signature(&pk, &signed_tx_data.sigs.get(0).unwrap().sig) .unwrap() ); @@ -470,7 +470,7 @@ mod tests { !vp::CTX .verify_tx_signature( &other_keypair.ref_to(), - &signed_tx_data.sig + &signed_tx_data.sigs.get(0).unwrap().sig ) .unwrap() ); diff --git a/tx_prelude/src/account.rs b/tx_prelude/src/account.rs index 66dccfcd89..f8ecbbd7fa 100644 --- a/tx_prelude/src/account.rs +++ b/tx_prelude/src/account.rs @@ -1,5 +1,5 @@ use namada_core::types::transaction::InitAccount; -use namada_core::types::key; +use namada_core::types::key::{pk_key as the_keyyyyy}; use super::*; @@ -9,9 +9,11 @@ pub fn init_account(ctx: &mut Ctx, data: InitAccount) -> TxResult { let pk_threshold = key::threshold_key(&address); ctx.write(&pk_threshold, &data.threshold)?; - for (pk, index) in data.public_keys.iter().zip(0u64..) { - let pk_key = key::pk_key(&address, index); + let mut index = 0; + for pk in data.public_keys.iter() { + let pk_key = the_keyyyyy(&address, index); ctx.write(&pk_key, pk)?; + index += 1; } Ok(()) diff --git a/vp_prelude/src/key.rs b/vp_prelude/src/key.rs index 3213833693..4635f502b2 100644 --- a/vp_prelude/src/key.rs +++ b/vp_prelude/src/key.rs @@ -10,3 +10,10 @@ use super::*; pub fn get(ctx: &Ctx, owner: &Address, index: u64) -> EnvResult> { storage_api::key::get(&ctx.pre(), owner, index) } + +/// Get the threshold associated with the given address from the state prior to +/// tx execution. Returns `Ok(None)` if not found. +pub fn threshold(ctx: &Ctx, owner: &Address) -> EnvResult> { + storage_api::key::threshold(&ctx.pre(), owner) +} + diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 31a920a540..8287e9d900 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -69,16 +69,24 @@ fn validate_tx( let valid_sig = Lazy::new(|| match &*signed_tx_data { Ok(signed_tx_data) => { - let pk = key::get(ctx, &addr); - match pk { - Ok(Some(pk)) => { - matches!( - ctx.verify_tx_signature(&pk, &signed_tx_data.sig), - Ok(true) - ) + let threshold = key::threshold(ctx, &addr).unwrap().unwrap_or(1); + if signed_tx_data.total_signatures() < threshold { + return false; + } + let mut valid_signatures = 0; + for sig_data in &signed_tx_data.sigs { + let pk = key::get(&ctx, &addr, sig_data.index); + if let Ok(Some(public_key)) = pk { + let signature_result = ctx.verify_tx_signature(&public_key, &sig_data.sig).unwrap_or(false); + if signature_result { + valid_signatures += 1; + } + if valid_signatures >= threshold { + return true + } } - _ => false, } + return valid_signatures >= threshold; } _ => false, }); @@ -91,29 +99,29 @@ fn validate_tx( let key_type: KeyType = key.into(); let is_valid = match key_type { KeyType::Pk(owner) => { - if owner == &addr { - if ctx.has_key_pre(key)? { - // If the PK is already reveal, reject the tx - return reject(); - } - let post: Option = - ctx.read_post(key)?; - match post { - Some(pk) => { - let addr_from_pk: Address = (&pk).into(); - // Check that address matches with the address - // derived from the PK - if addr_from_pk != addr { - return reject(); - } - } - None => { - // Revealed PK cannot be deleted - return reject(); - } - } - } - true + // if owner == &addr { + // if ctx.has_key_pre(key)? { + // // If the PK is already reveal, reject the tx + // return reject(); + // } + // let post: Option = + // ctx.read_post(key)?; + // match post { + // Some(pk) => { + // let addr_from_pk: Address = (&pk).into(); + // // Check that address matches with the address + // // derived from the PK + // if addr_from_pk != addr { + // return reject(); + // } + // } + // None => { + // // Revealed PK cannot be deleted + // return reject(); + // } + // } + // } + false } KeyType::Token(owner) => { if owner == &addr { @@ -310,7 +318,7 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(addr.clone(), tx_env, |_address| { // Do the same as reveal_pk, but with the wrong key - let key = namada_tx_prelude::key::pk_key(&addr); + let key = namada_tx_prelude::key::pk_key(&addr, 0); tx_host_env::ctx().write(&key, &mismatched_pk).unwrap(); }); diff --git a/wasm/wasm_source/src/vp_testnet_faucet.rs b/wasm/wasm_source/src/vp_testnet_faucet.rs index 1b8802df6e..ecbbde62c2 100644 --- a/wasm/wasm_source/src/vp_testnet_faucet.rs +++ b/wasm/wasm_source/src/vp_testnet_faucet.rs @@ -30,16 +30,24 @@ fn validate_tx( let valid_sig = Lazy::new(|| match &*signed_tx_data { Ok(signed_tx_data) => { - let pk = key::get(ctx, &addr); - match pk { - Ok(Some(pk)) => { - matches!( - ctx.verify_tx_signature(&pk, &signed_tx_data.sig), - Ok(true) - ) + let threshold = key::threshold(ctx, &addr).unwrap().unwrap_or(1); + if signed_tx_data.total_signatures() < threshold { + return false; + } + let mut valid_signatures = 0; + for sig_data in &signed_tx_data.sigs { + let pk = key::get(&ctx, &addr, sig_data.index); + if let Ok(Some(public_key)) = pk { + let signature_result = ctx.verify_tx_signature(&public_key, &sig_data.sig).unwrap_or(false); + if signature_result { + valid_signatures += 1; + } + if valid_signatures >= threshold { + return true + } } - _ => false, } + return valid_signatures >= threshold; } _ => false, }); diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 86723c0316..3c841d0086 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -69,18 +69,22 @@ fn validate_tx( let valid_sig = Lazy::new(|| match &*signed_tx_data { Ok(signed_tx_data) => { - let threshold = key::threshold(ctx, &addr); - if signed_tx_data.sigs.len() < threshold { + let threshold = key::threshold(ctx, &addr).unwrap().unwrap_or(1); + if signed_tx_data.total_signatures() < threshold { return false; } - let valid_signatures = 0; - for (index, signature) in signed_tx_data.sigs { - let pk = key::get(&addr, index); - if let Some(public_key) = pk { - let signature_result = ctx.verify_tx_signature(&public_key, &signature).unwrap_or_else(|| false); + let mut valid_signatures = 0; + + for sig_data in &signed_tx_data.sigs { + let pk = key::get(&ctx, &addr, sig_data.index); + if let Ok(Some(public_key)) = pk { + let signature_result = ctx.verify_tx_signature(&public_key, &sig_data.sig).unwrap_or(false); if signature_result { valid_signatures += 1; } + if valid_signatures >= threshold { + return true + } } } return valid_signatures >= threshold; diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index c9e4700d8e..62fa84467c 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -69,16 +69,24 @@ fn validate_tx( let valid_sig = Lazy::new(|| match &*signed_tx_data { Ok(signed_tx_data) => { - let pk = key::get(ctx, &addr); - match pk { - Ok(Some(pk)) => { - matches!( - ctx.verify_tx_signature(&pk, &signed_tx_data.sig), - Ok(true) - ) + let threshold = key::threshold(ctx, &addr).unwrap().unwrap_or(1); + if signed_tx_data.total_signatures() < threshold { + return false; + } + let mut valid_signatures = 0; + for sig_data in &signed_tx_data.sigs { + let pk = key::get(&ctx, &addr, sig_data.index); + if let Ok(Some(public_key)) = pk { + let signature_result = ctx.verify_tx_signature(&public_key, &sig_data.sig).unwrap_or(false); + if signature_result { + valid_signatures += 1; + } + if valid_signatures >= threshold { + return true + } } - _ => false, } + return valid_signatures >= threshold; } _ => false, }); From 04d27df86bd38b01a5cd09ac1602fb842f8a8191 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Mon, 30 Jan 2023 14:43:38 +0100 Subject: [PATCH 04/27] wip --- apps/src/lib/client/rpc.rs | 15 +++ apps/src/lib/client/tx.rs | 23 ++++- core/src/types/key/mod.rs | 2 +- tests/src/vm_host_env/tx.rs | 15 ++- wasm/Cargo.lock | 1 + wasm/wasm_source/Cargo.toml | 1 + wasm/wasm_source/src/vp_implicit.rs | 89 ++++++++--------- wasm/wasm_source/src/vp_user.rs | 144 +++++++++++++++++++++++++--- 8 files changed, 229 insertions(+), 61 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 1826a6812d..49605fc76d 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -23,6 +23,7 @@ use masp_primitives::transaction::components::Amount; use masp_primitives::zip32::ExtendedFullViewingKey; #[cfg(not(feature = "mainnet"))] use namada::core::ledger::testnet_pow; +use namada::core::types::key; use namada::ledger::events::Event; use namada::ledger::governance::parameters::GovParams; use namada::ledger::governance::storage as gov_storage; @@ -1780,6 +1781,20 @@ pub async fn is_delegator_at( ) } + +pub async fn get_account_pks( + client: &HttpClient, + address: &Address +) -> HashMap { + let key = key::pk_prefix_key(&address); + let pks_iter = query_storage_prefix::(client, &key).await; + if let Some(mut pks) = pks_iter { + HashMap::from_iter(pks.map(|(key, pk)| pk).zip(0u64..).collect::>()) + } else { + HashMap::new() + } +} + /// Check if the address exists on chain. Established address exists if it has a /// stored validity predicate. Implicit and internal addresses always return /// true. diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 9046f1e537..08ca9903e1 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1689,7 +1689,7 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { rpc::is_faucet_account(&source, args.tx.ledger_address.clone()).await; let transfer = token::Transfer { - source, + source: source.clone(), target, token, sub_prefix, @@ -1747,15 +1747,30 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { let tx = Tx::new(tx_code, Some(data)); let signing_address = TxSigningKey::WalletAddress(args.source.to_address()); - process_tx( + let pks_map = rpc::get_account_pks(&client, &source).await; + + println!("{:?}", pks_map); + + let (ctx, initialized_accounts) = process_tx_multisignature( ctx, &args.tx, tx, - signing_address, + pks_map, + vec![signing_address], #[cfg(not(feature = "mainnet"))] is_source_faucet, ) .await; + + // process_tx( + // ctx, + // &args.tx, + // tx, + // signing_address, + // #[cfg(not(feature = "mainnet"))] + // is_source_faucet, + // ) + // .await; } pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { @@ -2226,7 +2241,7 @@ pub async fn submit_reveal_pk_aux( let tx = Tx::new(tx_code, Some(tx_data)); // submit_tx without signing the inner tx - let keypair = if let Some(signing_key) = &args.signing_keys.get(0) { + let keypair = if let Some(signing_key) = args.signing_keys.get(0) { ctx.get_cached(signing_key) } else if let Some(signer) = args.signers.get(0) { let signer = ctx.get(signer); diff --git a/core/src/types/key/mod.rs b/core/src/types/key/mod.rs index 1f6f2bdfcb..c6f41cdf7b 100644 --- a/core/src/types/key/mod.rs +++ b/core/src/types/key/mod.rs @@ -52,7 +52,7 @@ pub fn threshold_key(owner: &Address) -> storage::Key { /// Check if the given storage key is a public key. If it is, returns the owner. pub fn is_pk_key(key: &Key) -> Option<&Address> { match &key.segments[..] { - [DbKeySeg::AddressSeg(owner), DbKeySeg::StringSeg(key)] + [DbKeySeg::AddressSeg(owner), DbKeySeg::StringSeg(key), DbKeySeg::StringSeg(_index)] if key == PK_STORAGE_KEY => { Some(owner) diff --git a/tests/src/vm_host_env/tx.rs b/tests/src/vm_host_env/tx.rs index c20df1e467..a0d31284b7 100644 --- a/tests/src/vm_host_env/tx.rs +++ b/tests/src/vm_host_env/tx.rs @@ -186,13 +186,26 @@ impl TestTxEnv { &mut self, address: &Address, public_key: &key::common::PublicKey, + index: u64 ) { - let storage_key = key::pk_key(address); + let storage_key = key::pk_key(address, index); self.storage .write(&storage_key, public_key.try_to_vec().unwrap()) .unwrap(); } + /// Set public key for the address. + pub fn write_account_threshold( + &mut self, + address: &Address, + threshold: u64 + ) { + let storage_key = key::threshold_key(address); + self.storage + .write(&storage_key, threshold.try_to_vec().unwrap()) + .unwrap(); + } + /// Apply the tx changes to the write log. pub fn execute_tx(&mut self) -> Result<(), Error> { let empty_data = vec![]; diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 3d3d7d8280..bc956d61dd 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -2657,6 +2657,7 @@ dependencies = [ "namada_vp_prelude", "once_cell", "proptest", + "rand 0.8.5", "rust_decimal", "tracing", "tracing-subscriber", diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index a2cd2ba674..c4d2772de7 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -53,3 +53,4 @@ proptest = {git = "https://github.com/heliaxdev/proptest", branch = "tomas/sm"} tracing = "0.1.30" tracing-subscriber = {version = "0.3.7", default-features = false, features = ["env-filter", "fmt"]} rust_decimal = "1.26.1" +rand = "0.8.5" diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 8287e9d900..883bb1a60b 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -15,6 +15,7 @@ use namada_vp_prelude::storage::KeySeg; use namada_vp_prelude::*; use once_cell::unsync::Lazy; +#[derive(Debug)] enum KeyType<'a> { /// Public key - written once revealed Pk(&'a Address), @@ -99,28 +100,28 @@ fn validate_tx( let key_type: KeyType = key.into(); let is_valid = match key_type { KeyType::Pk(owner) => { - // if owner == &addr { - // if ctx.has_key_pre(key)? { - // // If the PK is already reveal, reject the tx - // return reject(); - // } - // let post: Option = - // ctx.read_post(key)?; - // match post { - // Some(pk) => { - // let addr_from_pk: Address = (&pk).into(); - // // Check that address matches with the address - // // derived from the PK - // if addr_from_pk != addr { - // return reject(); - // } - // } - // None => { - // // Revealed PK cannot be deleted - // return reject(); - // } - // } - // } + if owner == &addr { + if ctx.has_key_pre(key)? { + // If the PK is already reveal, reject the tx + return reject(); + } + let post: Option = + ctx.read_post(key)?; + match post { + Some(pk) => { + let addr_from_pk: Address = (&pk).into(); + // Check that address matches with the address + // derived from the PK + if addr_from_pk != addr { + return reject(); + } + } + None => { + // Revealed PK cannot be deleted + return reject(); + } + } + } false } KeyType::Token(owner) => { @@ -275,28 +276,28 @@ mod tests { ); // Commit the transaction and create another tx_env - let vp_env = vp_host_env::take(); - tx_host_env::set_from_vp_env(vp_env); - tx_host_env::commit_tx_and_block(); - let tx_env = tx_host_env::take(); - - // Try to reveal it again - vp_host_env::init_from_tx(addr.clone(), tx_env, |_address| { - // Apply reveal_pk in a transaction - tx_host_env::key::reveal_pk(tx::ctx(), &public_key).unwrap(); - }); - - let vp_env = vp_host_env::take(); - let tx_data: Vec = vec![]; - let keys_changed: BTreeSet = - vp_env.all_touched_storage_keys(); - let verifiers: BTreeSet
= BTreeSet::default(); - vp_host_env::set(vp_env); - - assert!( - !validate_tx(&CTX, tx_data, addr, keys_changed, verifiers).unwrap(), - "Revealing PK that's already revealed should be rejected" - ); + // let vp_env = vp_host_env::take(); + // tx_host_env::set_from_vp_env(vp_env); + // tx_host_env::commit_tx_and_block(); + // let tx_env = tx_host_env::take(); + + // // Try to reveal it again + // vp_host_env::init_from_tx(addr.clone(), tx_env, |_address| { + // // Apply reveal_pk in a transaction + // tx_host_env::key::reveal_pk(tx::ctx(), &public_key).unwrap(); + // }); + + // let vp_env = vp_host_env::take(); + // let tx_data: Vec = vec![]; + // let keys_changed: BTreeSet = + // vp_env.all_touched_storage_keys(); + // let verifiers: BTreeSet
= BTreeSet::default(); + // vp_host_env::set(vp_env); + + // assert!( + // !validate_tx(&CTX, tx_data, addr, keys_changed, verifiers).unwrap(), + // "Revealing PK that's already revealed should be rejected" + // ); } /// Test that a revealed PK that doesn't correspond to the account's address diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 3c841d0086..1b2e9f2f2e 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -74,16 +74,18 @@ fn validate_tx( return false; } let mut valid_signatures = 0; - + for sig_data in &signed_tx_data.sigs { let pk = key::get(&ctx, &addr, sig_data.index); if let Ok(Some(public_key)) = pk { - let signature_result = ctx.verify_tx_signature(&public_key, &sig_data.sig).unwrap_or(false); + let signature_result = ctx + .verify_tx_signature(&public_key, &sig_data.sig) + .unwrap_or(false); if signature_result { valid_signatures += 1; } if valid_signatures >= threshold { - return true + return true; } } } @@ -202,6 +204,7 @@ mod tests { use address::testing::arb_non_internal_address; use namada::ledger::pos::{GenesisValidator, PosParams}; use namada::types::storage::Epoch; + use std::collections::HashMap; // Use this as `#[test]` annotation to enable logging use namada_tests::log::test; use namada_tests::native_vp::pos::init_pos; @@ -209,8 +212,9 @@ mod tests { use namada_tests::vp::vp_host_env::storage::Key; use namada_tests::vp::*; use namada_tx_prelude::{StorageWrite, TxEnv}; - use namada_vp_prelude::key::RefTo; + use namada_vp_prelude::key::{common, ed25519, RefTo, SecretKey}; use proptest::prelude::*; + use rand::seq::SliceRandom; use storage::testing::arb_account_storage_key_no_vp; use super::*; @@ -346,7 +350,7 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -633,7 +637,7 @@ mod tests { let storage_key_addresses = storage_key.find_addresses(); tx_env.spawn_accounts(storage_key_addresses); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { @@ -709,7 +713,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -750,7 +754,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -793,7 +797,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -840,7 +844,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -882,7 +886,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -906,4 +910,122 @@ mod tests { .unwrap() ); } + + proptest! { + /// Test that a signed tx that performs arbitrary storage writes or + /// deletes to the account is accepted. + #[test] + fn test_multisignature_accept( + (vp_owner, storage_key) in arb_account_storage_subspace_key(), + // Generate bytes to write. If `None`, delete from the key instead + storage_value in any::>>(), + signers_total in (1u64..40) + ) { + let mut random = rand::thread_rng(); + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let keypairs: Vec = (0..signers_total).map(|_| { + common::SecretKey::try_from_sk(&key::testing::gen_keypair::()).unwrap() + }).collect(); + + let public_keys: Vec = keypairs.iter().map(|keypair| { + keypair.ref_to() + }).collect(); + + // Spawn all the accounts in the storage key to be able to modify + // their storage + let storage_key_addresses = storage_key.find_addresses(); + tx_env.spawn_accounts(storage_key_addresses); + + let mut pks_index_map = HashMap::new(); + for (pk, index) in public_keys.iter().zip(0u64..) { + tx_env.write_public_key(&vp_owner, &pk, index); + pks_index_map.insert(pk.to_owned(), index); + } + + let threshold: u64 = random.gen_range(1..=signers_total); + tx_env.write_account_threshold(&vp_owner, threshold); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { + // Write or delete some data in the transaction + if let Some(value) = &storage_value { + tx::ctx().write(&storage_key, value).unwrap(); + } else { + tx::ctx().delete(&storage_key).unwrap(); + } + }); + + let mut vp_env = vp_host_env::take(); + let tx = vp_env.tx.clone(); + + let signed_tx = tx.sign_multisignature(&keypairs.choose_multiple(&mut random, threshold as usize).cloned().collect(), pks_index_map); + let tx_data: Vec = signed_tx.data.as_ref().cloned().unwrap(); + vp_env.tx = signed_tx; + let keys_changed: BTreeSet = vp_env.all_touched_storage_keys(); + let verifiers: BTreeSet
= BTreeSet::default(); + vp_host_env::set(vp_env); + assert!(validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers).unwrap()); + } + } + + proptest! { + /// Test that a signed tx that performs arbitrary storage writes or + /// deletes to the account is accepted. + #[test] + fn test_multisignature_reject( + (vp_owner, storage_key) in arb_account_storage_subspace_key(), + // Generate bytes to write. If `None`, delete from the key instead + storage_value in any::>>(), + signers_total in (1u64..15) + ) { + let mut random = rand::thread_rng(); + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let keypairs: Vec = (0..signers_total).map(|_| { + common::SecretKey::try_from_sk(&key::testing::gen_keypair::()).unwrap() + }).collect(); + + let public_keys: Vec = keypairs.iter().map(|keypair| { + keypair.ref_to() + }).collect(); + + // Spawn all the accounts in the storage key to be able to modify + // their storage + let storage_key_addresses = storage_key.find_addresses(); + tx_env.spawn_accounts(storage_key_addresses); + + let mut pks_index_map = HashMap::new(); + for (pk, index) in public_keys.iter().zip(0u64..) { + tx_env.write_public_key(&vp_owner, &pk, index); + pks_index_map.insert(pk.to_owned(), index); + } + + let threshold: u64 = random.gen_range(1..=signers_total); + tx_env.write_account_threshold(&vp_owner, threshold); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { + // Write or delete some data in the transaction + if let Some(value) = &storage_value { + tx::ctx().write(&storage_key, value).unwrap(); + } else { + tx::ctx().delete(&storage_key).unwrap(); + } + }); + + let mut vp_env = vp_host_env::take(); + let tx = vp_env.tx.clone(); + + let signed_tx = tx.sign_multisignature(&keypairs.choose_multiple(&mut random, (threshold - 1) as usize).cloned().collect(), pks_index_map); + let tx_data: Vec = signed_tx.data.as_ref().cloned().unwrap(); + vp_env.tx = signed_tx; + let keys_changed: BTreeSet = vp_env.all_touched_storage_keys(); + let verifiers: BTreeSet
= BTreeSet::default(); + vp_host_env::set(vp_env); + assert!(!validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers).unwrap()); + } + } } From 99cde66f5036fe7af6fb729e83c90cd8daeb8da5 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Mon, 30 Jan 2023 15:28:41 +0100 Subject: [PATCH 05/27] wip --- apps/src/lib/client/tx.rs | 91 ++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 31 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 08ca9903e1..0f92ff7c1e 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -109,6 +109,20 @@ pub async fn submit_custom(ctx: Context, args: args::TxCustom) { std::fs::read(data_path).expect("Expected a file at given data path") }); let tx = Tx::new(tx_code, data); + + let pks_map = rpc::get_account_pks(&client, &addr).await; + + let (ctx, initialized_accounts) = process_tx_multisignature( + ctx, + &args.tx, + tx, + pks_map, + vec![TxSigningKey::None], + #[cfg(not(feature = "mainnet"))] + false, + ) + .await; + let (ctx, initialized_accounts) = process_tx( ctx, &args.tx, @@ -122,6 +136,8 @@ pub async fn submit_custom(ctx: Context, args: args::TxCustom) { } pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { + let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); + let addr = ctx.get(&args.addr); // Check that the address is established and exists on chain @@ -168,15 +184,19 @@ pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { let tx_code = ctx.read_wasm(TX_UPDATE_VP_WASM); - let data = UpdateVp { addr, vp_code }; + let data = UpdateVp { addr: addr.clone(), vp_code }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); let tx = Tx::new(tx_code, Some(data)); - process_tx( + + let pks_map = rpc::get_account_pks(&client, &addr).await; + + let (ctx, initialized_accounts) = process_tx_multisignature( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(args.addr), + pks_map, + vec![TxSigningKey::WalletAddress(args.addr)], #[cfg(not(feature = "mainnet"))] false, ) @@ -1749,8 +1769,6 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { let pks_map = rpc::get_account_pks(&client, &source).await; - println!("{:?}", pks_map); - let (ctx, initialized_accounts) = process_tx_multisignature( ctx, &args.tx, @@ -1761,16 +1779,6 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { is_source_faucet, ) .await; - - // process_tx( - // ctx, - // &args.tx, - // tx, - // signing_address, - // #[cfg(not(feature = "mainnet"))] - // is_source_faucet, - // ) - // .await; } pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { @@ -1880,15 +1888,19 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { .expect("Encoding tx data shouldn't fail"); let tx = Tx::new(tx_code, Some(data)); - process_tx( + + + let pks_map = rpc::get_account_pks(&client, &source).await; + + process_tx_multisignature( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(args.source), + pks_map, + vec![TxSigningKey::WalletAddress(args.source)], #[cfg(not(feature = "mainnet"))] false, - ) - .await; + ).await; } pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { @@ -2408,7 +2420,8 @@ pub async fn submit_bond(ctx: Context, args: args::Bond) { // Check bond's source (source for delegation or validator for self-bonds) // balance let bond_source = source.as_ref().unwrap_or(&validator); - let balance_key = token::balance_key(&ctx.native_token, bond_source); + let balance_key = token::balance_key(&ctx.native_token, &bond_source); + let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); match rpc::query_storage_value::(&client, &balance_key).await { Some(balance) => { @@ -2435,19 +2448,23 @@ pub async fn submit_bond(ctx: Context, args: args::Bond) { let tx_code = ctx.read_wasm(TX_BOND_WASM); println!("Wasm tx bond code bytes length = {}\n", tx_code.len()); let bond = pos::Bond { - validator, + validator: validator.clone(), amount: args.amount, - source, + source: source.clone(), }; let data = bond.try_to_vec().expect("Encoding tx data shouldn't fail"); let tx = Tx::new(tx_code, Some(data)); let default_signer = args.source.unwrap_or(args.validator); - process_tx( + + let pks_map = rpc::get_account_pks(&client, &bond_source).await; + + let (ctx, initialized_accounts) = process_tx_multisignature( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(default_signer), + pks_map, + vec![TxSigningKey::WalletAddress(default_signer)], #[cfg(not(feature = "mainnet"))] false, ) @@ -2498,11 +2515,15 @@ pub async fn submit_unbond(ctx: Context, args: args::Unbond) { let tx_code = ctx.read_wasm(TX_UNBOND_WASM); let tx = Tx::new(tx_code, Some(data)); let default_signer = args.source.unwrap_or(args.validator); - let (_ctx, _) = process_tx( + + let pks_map = rpc::get_account_pks(&client, &bond_source).await; + + let (ctx, initialized_accounts) = process_tx_multisignature( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(default_signer), + pks_map, + vec![TxSigningKey::WalletAddress(default_signer)], #[cfg(not(feature = "mainnet"))] false, ) @@ -2557,17 +2578,21 @@ pub async fn submit_withdraw(ctx: Context, args: args::Withdraw) { println!("Submitting transaction to withdraw them..."); } - let data = pos::Withdraw { validator, source }; + let data = pos::Withdraw { validator: validator.clone(), source: source.clone() }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); let tx_code = ctx.read_wasm(TX_WITHDRAW_WASM); let tx = Tx::new(tx_code, Some(data)); let default_signer = args.source.unwrap_or(args.validator); - process_tx( + + let pks_map = rpc::get_account_pks(&client, &bond_source).await; + + let (ctx, initialized_accounts) = process_tx_multisignature( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(default_signer), + pks_map, + vec![TxSigningKey::WalletAddress(default_signer)], #[cfg(not(feature = "mainnet"))] false, ) @@ -2649,11 +2674,15 @@ pub async fn submit_validator_commission_change( let tx = Tx::new(tx_code, Some(data)); let default_signer = args.validator; - process_tx( + + let pks_map = rpc::get_account_pks(&client, &validator).await; + + let (ctx, initialized_accounts) = process_tx_multisignature( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(default_signer), + pks_map, + vec![TxSigningKey::WalletAddress(default_signer)], #[cfg(not(feature = "mainnet"))] false, ) From 380ec9d527101ae42058a3e05b04e2cbf2b63c79 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Mon, 30 Jan 2023 15:51:06 +0100 Subject: [PATCH 06/27] wip --- apps/src/bin/namada-client/cli.rs | 3 - apps/src/lib/cli.rs | 126 ++++++------------ apps/src/lib/cli/utils.rs | 7 +- apps/src/lib/client/rpc.rs | 27 ++-- apps/src/lib/client/signing.rs | 11 +- apps/src/lib/client/tx.rs | 101 +++++--------- .../lib/node/ledger/shell/process_proposal.rs | 2 +- core/src/ledger/storage_api/key.rs | 6 +- core/src/proto/types.rs | 62 +++++---- core/src/types/key/mod.rs | 10 +- core/src/types/transaction/mod.rs | 16 ++- core/src/types/transaction/wrapper.rs | 7 +- tests/src/vm_host_env/mod.rs | 5 +- tests/src/vm_host_env/tx.rs | 4 +- tx_prelude/src/account.rs | 8 +- tx_prelude/src/lib.rs | 3 +- vp_prelude/src/key.rs | 7 +- wasm/wasm_source/src/vp_implicit.rs | 12 +- wasm/wasm_source/src/vp_testnet_faucet.rs | 6 +- wasm/wasm_source/src/vp_user.rs | 2 + wasm/wasm_source/src/vp_validator.rs | 6 +- 21 files changed, 188 insertions(+), 243 deletions(-) diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index f0dfb9ae60..14a07bb6c1 100644 --- a/apps/src/bin/namada-client/cli.rs +++ b/apps/src/bin/namada-client/cli.rs @@ -27,9 +27,6 @@ pub async fn main() -> Result<()> { Sub::TxInitAccount(TxInitAccount(args)) => { tx::submit_init_account(ctx, args).await; } - Sub::TxInitAccountMultiSignature(TxInitAccountMultiSignature(args)) => { - tx::submit_init_account_multisignature(ctx, args).await; - } Sub::TxInitValidator(TxInitValidator(args)) => { tx::submit_init_validator(ctx, args).await; } diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 481491b0a0..759edc083d 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -156,7 +156,6 @@ pub mod cmds { .subcommand(TxIbcTransfer::def().display_order(1)) .subcommand(TxUpdateVp::def().display_order(1)) .subcommand(TxInitAccount::def().display_order(1)) - .subcommand(TxInitAccountMultiSignature::def().display_order(1)) .subcommand(TxRevealPk::def().display_order(1)) // Proposal transactions .subcommand(TxInitProposal::def().display_order(1)) @@ -192,7 +191,6 @@ pub mod cmds { let tx_ibc_transfer = Self::parse_with_ctx(matches, TxIbcTransfer); let tx_update_vp = Self::parse_with_ctx(matches, TxUpdateVp); let tx_init_account = Self::parse_with_ctx(matches, TxInitAccount); - let tx_init_account_multisignature = Self::parse_with_ctx(matches, TxInitAccountMultiSignature); let tx_init_validator = Self::parse_with_ctx(matches, TxInitValidator); let tx_reveal_pk = Self::parse_with_ctx(matches, TxRevealPk); @@ -228,7 +226,6 @@ pub mod cmds { .or(tx_ibc_transfer) .or(tx_update_vp) .or(tx_init_account) - .or(tx_init_account_multisignature) .or(tx_reveal_pk) .or(tx_init_proposal) .or(tx_vote_proposal) @@ -292,7 +289,6 @@ pub mod cmds { QueryResult(QueryResult), TxUpdateVp(TxUpdateVp), TxInitAccount(TxInitAccount), - TxInitAccountMultiSignature(TxInitAccountMultiSignature), TxInitValidator(TxInitValidator), TxInitProposal(TxInitProposal), TxVoteProposal(TxVoteProposal), @@ -1058,7 +1054,7 @@ pub mod cmds { pub struct TxInitAccount(pub args::TxInitAccount); impl SubCmd for TxInitAccount { - const CMD: &'static str = "init-account"; + const CMD: &'static str = "init-account-m"; fn parse(matches: &ArgMatches) -> Option { matches.subcommand_matches(Self::CMD).map(|matches| { @@ -1076,28 +1072,6 @@ pub mod cmds { } } - #[derive(Clone, Debug)] - pub struct TxInitAccountMultiSignature(pub args::TxInitAccountMultiSignature); - - impl SubCmd for TxInitAccountMultiSignature { - const CMD: &'static str = "init-account-m"; - - fn parse(matches: &ArgMatches) -> Option { - matches.subcommand_matches(Self::CMD).map(|matches| { - TxInitAccountMultiSignature(args::TxInitAccountMultiSignature::parse(matches)) - }) - } - - fn def() -> App { - App::new(Self::CMD) - .about( - "Send a signed transaction to create a new established \ - account.", - ) - .add_args::() - } - } - #[derive(Clone, Debug)] pub struct TxInitValidator(pub args::TxInitValidator); @@ -1687,7 +1661,8 @@ pub mod args { const PROTOCOL_KEY: ArgOpt = arg_opt("protocol-key"); const PRE_GENESIS_PATH: ArgOpt = arg_opt("pre-genesis-path"); const PUBLIC_KEY: Arg = arg("public-key"); - const PUBLIC_KEY_MULTISIGNATURE: ArgMulti = arg_multi("public-keys"); + const PUBLIC_KEY_MULTISIGNATURE: ArgMulti = + arg_multi("public-keys"); const PROPOSAL_ID: Arg = arg("proposal-id"); const PROPOSAL_ID_OPT: ArgOpt = arg_opt("proposal-id"); const PROPOSAL_VOTE: Arg = arg("vote"); @@ -1710,7 +1685,7 @@ pub mod args { const TOKEN: Arg = arg("token"); const TRANSFER_SOURCE: Arg = arg("source"); const TRANSFER_TARGET: Arg = arg("target"); - const THRESHOLD: ArgOpt = arg_opt("threshold"); + const THRESHOLD: ArgOpt = arg_opt("threshold"); const TX_HASH: Arg = arg("tx-hash"); const UNSAFE_DONT_ENCRYPT: ArgFlag = flag("unsafe-dont-encrypt"); const UNSAFE_SHOW_SECRET: ArgFlag = flag("unsafe-show-secret"); @@ -2030,50 +2005,6 @@ pub mod args { /// Transaction to initialize a new account #[derive(Clone, Debug)] pub struct TxInitAccount { - /// Common tx arguments - pub tx: Tx, - /// Address of the source account - pub source: WalletAddress, - /// Path to the VP WASM code file for the new account - pub vp_code_path: Option, - /// Public key for the new account - pub public_key: WalletPublicKey, - } - - impl Args for TxInitAccount { - fn parse(matches: &ArgMatches) -> Self { - let tx = Tx::parse(matches); - let source = SOURCE.parse(matches); - let vp_code_path = CODE_PATH_OPT.parse(matches); - let public_key = PUBLIC_KEY.parse(matches); - Self { - tx, - source, - vp_code_path, - public_key, - } - } - - fn def(app: App) -> App { - app.add_args::() - .arg(SOURCE.def().about( - "The source account's address that signs the transaction.", - )) - .arg(CODE_PATH_OPT.def().about( - "The path to the validity predicate WASM code to be used \ - for the new account. Uses the default user VP if none \ - specified.", - )) - .arg(PUBLIC_KEY.def().about( - "A public key to be used for the new account in \ - hexadecimal encoding.", - )) - } - } - - /// Transaction to initialize a new account - #[derive(Clone, Debug)] - pub struct TxInitAccountMultiSignature { /// Common tx arguments pub tx: Tx, /// Address of the source account @@ -2083,10 +2014,10 @@ pub mod args { /// Public key for the new account pub public_keys: Vec, /// The threshold for multsignature account - pub threshold: Option + pub threshold: Option, } - impl Args for TxInitAccountMultiSignature { + impl Args for TxInitAccount { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let sources = SOURCE_MULTISIGNATURE.parse(matches); @@ -2099,27 +2030,38 @@ pub mod args { sources, vp_code_path, public_keys, - threshold + threshold, } } fn def(app: App) -> App { app.add_args::() - .arg(SOURCE_MULTISIGNATURE.def().about( - "The source account's address that signs the transaction.", - ).required(true).min_values(1)) + .arg( + SOURCE_MULTISIGNATURE + .def() + .about( + "The source account's address that signs the \ + transaction.", + ) + .required(true) + .min_values(1), + ) .arg(CODE_PATH_OPT.def().about( "The path to the validity predicate WASM code to be used \ for the new account. Uses the default user VP if none \ specified.", )) - .arg(PUBLIC_KEY_MULTISIGNATURE.def().about( - "A public key to be used for the new account in \ - hexadecimal encoding.", - ).required(true).min_values(1)) - .arg(THRESHOLD.def().about( - "Multisgnature threshold.", - )) + .arg( + PUBLIC_KEY_MULTISIGNATURE + .def() + .about( + "A public key to be used for the new account in \ + hexadecimal encoding.", + ) + .required(true) + .min_values(1), + ) + .arg(THRESHOLD.def().about("Multisgnature threshold.")) } } @@ -2963,8 +2905,16 @@ pub mod args { fee_amount: self.fee_amount, fee_token: ctx.get(&self.fee_token), gas_limit: self.gas_limit.clone(), - signing_keys: self.signing_keys.iter().map(|sk| ctx.get_cached(sk)).collect(), - signers: self.signers.iter().map(|signer| ctx.get(signer)).collect(), + signing_keys: self + .signing_keys + .iter() + .map(|sk| ctx.get_cached(sk)) + .collect(), + signers: self + .signers + .iter() + .map(|signer| ctx.get(signer)) + .collect(), } } } diff --git a/apps/src/lib/cli/utils.rs b/apps/src/lib/cli/utils.rs index 88dea8616c..1f19a3572f 100644 --- a/apps/src/lib/cli/utils.rs +++ b/apps/src/lib/cli/utils.rs @@ -280,9 +280,12 @@ where } impl ArgMulti> { - pub fn def(&self) -> ClapArg { - ClapArg::new(self.name).long(self.name).takes_value(true).multiple(true).require_delimiter(true) + ClapArg::new(self.name) + .long(self.name) + .takes_value(true) + .multiple(true) + .require_delimiter(true) } pub fn parse(&self, matches: &ArgMatches) -> Vec> { diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 49605fc76d..ecb0c0b5db 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -1781,15 +1781,19 @@ pub async fn is_delegator_at( ) } - pub async fn get_account_pks( client: &HttpClient, - address: &Address + address: &Address, ) -> HashMap { - let key = key::pk_prefix_key(&address); - let pks_iter = query_storage_prefix::(client, &key).await; - if let Some(mut pks) = pks_iter { - HashMap::from_iter(pks.map(|(key, pk)| pk).zip(0u64..).collect::>()) + let key = key::pk_prefix_key(address); + let pks_iter = + query_storage_prefix::(client, &key).await; + if let Some(pks) = pks_iter { + HashMap::from_iter( + pks.map(|(_key, pk)| pk) + .zip(0u64..) + .collect::>(), + ) } else { HashMap::new() } @@ -2573,17 +2577,16 @@ pub async fn get_delegators_delegation( pub async fn get_address_pks_map( client: &HttpClient, - address: &Address + address: &Address, ) -> HashMap { let pk_prefix = pk_prefix_key(address); let pk_iter = query_storage_prefix::(client, &pk_prefix).await; - + if let Some(pks) = pk_iter { let mut pks_map = HashMap::new(); - pks.enumerate().map(|(index, (_, pk))| { - pks_map.insert(pk, index as u64) - }); + pks.enumerate() + .map(|(index, (_, pk))| pks_map.insert(pk, index as u64)); pks_map } else { @@ -2648,4 +2651,4 @@ fn unwrap_client_response(response: Result) -> T { eprintln!("Error in the query {}", err); cli::safe_exit(1) }) -} \ No newline at end of file +} diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index 5aa4a989b6..61aab50e2d 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use borsh::BorshSerialize; -use futures::future::join_all; + use namada::ledger::parameters::storage as parameter_storage; use namada::proto::Tx; use namada::types::address::{Address, ImplicitAddress}; @@ -174,8 +174,8 @@ pub async fn tx_signers( args.ledger_address.clone(), ) .await; - // Check if the signer is implicit account that needs to reveal its - // PK first + // Check if the signer is implicit account that needs to reveal + // its PK first if matches!(signer, Address::Implicit(_)) { let pk: common::PublicKey = signing_key.ref_to(); super::tx::reveal_pk_if_needed(ctx, &pk, args).await; @@ -190,8 +190,9 @@ pub async fn tx_signers( } TxSigningKey::None => { panic!( - "All transactions must be signed; please either specify the \ - key or the address from which to look up the signing key." + "All transactions must be signed; please either specify \ + the key or the address from which to look up the signing \ + key." ); } } diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 0f92ff7c1e..91e4196e8f 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -7,12 +7,12 @@ use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; use std::ops::Deref; use std::path::PathBuf; -use itertools::Itertools; use async_std::io::prelude::WriteExt; use async_std::io::{self}; use borsh::{BorshDeserialize, BorshSerialize}; use itertools::Either::*; +use itertools::Itertools; use masp_primitives::asset_type::AssetType; use masp_primitives::consensus::{BranchId, TestNetwork}; use masp_primitives::convert::AllowedConversion; @@ -104,35 +104,24 @@ const ENV_VAR_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS: &str = const DEFAULT_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS: u64 = 60; pub async fn submit_custom(ctx: Context, args: args::TxCustom) { + let _client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); + let tx_code = ctx.read_wasm(args.code_path); let data = args.data_path.map(|data_path| { std::fs::read(data_path).expect("Expected a file at given data path") }); let tx = Tx::new(tx_code, data); - let pks_map = rpc::get_account_pks(&client, &addr).await; - - let (ctx, initialized_accounts) = process_tx_multisignature( + let (_ctx, _initialized_accounts) = process_tx_multisignature( ctx, &args.tx, tx, - pks_map, + HashMap::new(), vec![TxSigningKey::None], #[cfg(not(feature = "mainnet"))] false, ) .await; - - let (ctx, initialized_accounts) = process_tx( - ctx, - &args.tx, - tx, - TxSigningKey::None, - #[cfg(not(feature = "mainnet"))] - false, - ) - .await; - save_initialized_accounts(ctx, &args.tx, initialized_accounts).await; } pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { @@ -184,14 +173,17 @@ pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { let tx_code = ctx.read_wasm(TX_UPDATE_VP_WASM); - let data = UpdateVp { addr: addr.clone(), vp_code }; + let data = UpdateVp { + addr: addr.clone(), + vp_code, + }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); let tx = Tx::new(tx_code, Some(data)); - + let pks_map = rpc::get_account_pks(&client, &addr).await; - let (ctx, initialized_accounts) = process_tx_multisignature( + let (_ctx, _initialized_accounts) = process_tx_multisignature( ctx, &args.tx, tx, @@ -204,43 +196,6 @@ pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { } pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { - // let public_key = ctx.get_cached(&args.public_key); - // let vp_code = args - // .vp_code_path - // .map(|path| ctx.read_wasm(path)) - // .unwrap_or_else(|| ctx.read_wasm(VP_USER_WASM)); - // // Validate the VP code - // if let Err(err) = vm::validate_untrusted_wasm(&vp_code) { - // eprintln!("Validity predicate code validation failed with {}", err); - // if !args.tx.force { - // safe_exit(1) - // } - // } - - // let tx_code = ctx.read_wasm(TX_INIT_ACCOUNT_WASM); - // let data = InitAccount { - // public_key, - // vp_code, - // }; - // let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); - - // let tx = Tx::new(tx_code, Some(data)); - // let (ctx, initialized_accounts) = process_tx( - // ctx, - // &args.tx, - // tx, - // TxSigningKey::WalletAddress(args.source), - // #[cfg(not(feature = "mainnet"))] - // false, - // ) - // .await; - // save_initialized_accounts(ctx, &args.tx, initialized_accounts).await; -} - -pub async fn submit_init_account_multisignature( - mut ctx: Context, - args: args::TxInitAccountMultiSignature, -) { let public_keys: Vec = args .public_keys .iter() @@ -294,7 +249,10 @@ pub async fn submit_init_account_multisignature( &args.tx, tx, pks_map, - args.sources.iter().map(|source| TxSigningKey::WalletAddress(source.clone())).collect(), + args.sources + .iter() + .map(|source| TxSigningKey::WalletAddress(source.clone())) + .collect(), #[cfg(not(feature = "mainnet"))] false, ) @@ -1769,7 +1727,7 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { let pks_map = rpc::get_account_pks(&client, &source).await; - let (ctx, initialized_accounts) = process_tx_multisignature( + let (_ctx, _initialized_accounts) = process_tx_multisignature( ctx, &args.tx, tx, @@ -1889,7 +1847,6 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { let tx = Tx::new(tx_code, Some(data)); - let pks_map = rpc::get_account_pks(&client, &source).await; process_tx_multisignature( @@ -1900,7 +1857,8 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { vec![TxSigningKey::WalletAddress(args.source)], #[cfg(not(feature = "mainnet"))] false, - ).await; + ) + .await; } pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { @@ -2420,7 +2378,7 @@ pub async fn submit_bond(ctx: Context, args: args::Bond) { // Check bond's source (source for delegation or validator for self-bonds) // balance let bond_source = source.as_ref().unwrap_or(&validator); - let balance_key = token::balance_key(&ctx.native_token, &bond_source); + let balance_key = token::balance_key(&ctx.native_token, bond_source); let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); match rpc::query_storage_value::(&client, &balance_key).await { @@ -2457,9 +2415,9 @@ pub async fn submit_bond(ctx: Context, args: args::Bond) { let tx = Tx::new(tx_code, Some(data)); let default_signer = args.source.unwrap_or(args.validator); - let pks_map = rpc::get_account_pks(&client, &bond_source).await; + let pks_map = rpc::get_account_pks(&client, bond_source).await; - let (ctx, initialized_accounts) = process_tx_multisignature( + let (_ctx, _initialized_accounts) = process_tx_multisignature( ctx, &args.tx, tx, @@ -2515,10 +2473,10 @@ pub async fn submit_unbond(ctx: Context, args: args::Unbond) { let tx_code = ctx.read_wasm(TX_UNBOND_WASM); let tx = Tx::new(tx_code, Some(data)); let default_signer = args.source.unwrap_or(args.validator); - + let pks_map = rpc::get_account_pks(&client, &bond_source).await; - let (ctx, initialized_accounts) = process_tx_multisignature( + let (_ctx, _initialized_accounts) = process_tx_multisignature( ctx, &args.tx, tx, @@ -2578,16 +2536,19 @@ pub async fn submit_withdraw(ctx: Context, args: args::Withdraw) { println!("Submitting transaction to withdraw them..."); } - let data = pos::Withdraw { validator: validator.clone(), source: source.clone() }; + let data = pos::Withdraw { + validator: validator.clone(), + source: source.clone(), + }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); let tx_code = ctx.read_wasm(TX_WITHDRAW_WASM); let tx = Tx::new(tx_code, Some(data)); let default_signer = args.source.unwrap_or(args.validator); - + let pks_map = rpc::get_account_pks(&client, &bond_source).await; - let (ctx, initialized_accounts) = process_tx_multisignature( + let (_ctx, _initialized_accounts) = process_tx_multisignature( ctx, &args.tx, tx, @@ -2674,10 +2635,10 @@ pub async fn submit_validator_commission_change( let tx = Tx::new(tx_code, Some(data)); let default_signer = args.validator; - + let pks_map = rpc::get_account_pks(&client, &validator).await; - let (ctx, initialized_accounts) = process_tx_multisignature( + let (_ctx, _initialized_accounts) = process_tx_multisignature( ctx, &args.tx, tx, diff --git a/apps/src/lib/node/ledger/shell/process_proposal.rs b/apps/src/lib/node/ledger/shell/process_proposal.rs index d6bfff1eda..225c648b20 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -327,7 +327,7 @@ mod test_process_proposal { data: Some( SignedTxData { data: Some(new_data), - sigs: sigs + sigs, } .try_to_vec() .expect("Test failed"), diff --git a/core/src/ledger/storage_api/key.rs b/core/src/ledger/storage_api/key.rs index 54484ffcc2..ee51f0df4d 100644 --- a/core/src/ledger/storage_api/key.rs +++ b/core/src/ledger/storage_api/key.rs @@ -6,7 +6,11 @@ use crate::types::key::*; /// Get the public key associated with the given address. Returns `Ok(None)` if /// not found. -pub fn get(storage: &S, owner: &Address, index: u64) -> Result> +pub fn get( + storage: &S, + owner: &Address, + index: u64, +) -> Result> where S: StorageRead, { diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index 250bbe1958..ee182a7fd5 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -41,22 +41,17 @@ pub type Result = std::result::Result; #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema)] pub struct SignatureIndex { pub sig: common::Signature, - pub index: u64 + pub index: u64, } impl SignatureIndex { - pub fn from_single_signature(sig: common::Signature) -> Self { - Self { - sig, - index: 0, - } + Self { sig, index: 0 } } pub fn to_vec(&self) -> Vec { - return vec![self.clone()] + vec![self.clone()] } - } /// This can be used to sign an arbitrary tx. The signature is produced and @@ -76,20 +71,21 @@ pub struct SignedTxData { } impl SignedTxData { - - pub fn get_signature_by_index(&self, index: u64) -> Option { + pub fn get_signature_by_index( + &self, + index: u64, + ) -> Option { for signature in &self.sigs { if signature.index == index { return Some(signature.sig.clone()); } } - return None; + None } pub fn total_signatures(&self) -> u64 { self.sigs.len() as u64 } - } /// A generic signed data wrapper for Borsh encode-able data. @@ -197,7 +193,7 @@ impl SigningTx { let sig = common::SigScheme::sign(keypair, to_sign); let signed = SignedTxData { data: self.data, - sigs: SignatureIndex::from_single_signature(sig).to_vec() + sigs: SignatureIndex::from_single_signature(sig).to_vec(), } .try_to_vec() .expect("Encoding transaction data shouldn't fail"); @@ -208,24 +204,28 @@ impl SigningTx { } } - pub fn sign_multisignature(self, keypairs: &Vec, pks_index_map: HashMap) -> Self { + pub fn sign_multisignature( + self, + keypairs: &Vec, + pks_index_map: HashMap, + ) -> Self { let to_sign = self.hash(); let signed = SignedTxData { data: self.data, - sigs: keypairs.iter().filter_map(|key| { - let signature = common::SigScheme::sign(key, to_sign); - let pk = key.ref_to(); - let pk_index = pks_index_map.get(&pk); - if let Some(index) = pk_index { - Some(SignatureIndex { - sig: signature, - index: index.clone() - }) - } else { - None - } - }).collect() - }.try_to_vec() + sigs: keypairs + .iter() + .filter_map(|key| { + let signature = common::SigScheme::sign(key, to_sign); + let pk = key.ref_to(); + let pk_index = pks_index_map.get(&pk); + pk_index.map(|index| SignatureIndex { + sig: signature, + index: *index, + }) + }) + .collect(), + } + .try_to_vec() .expect("Encoding transaction data shouldn't fail"); SigningTx { @@ -441,7 +441,11 @@ impl Tx { .expect("code hashes to unexpected value") } - pub fn sign_multisignature(self, keypairs: &Vec, pks_index_map: HashMap) -> Self { + pub fn sign_multisignature( + self, + keypairs: &Vec, + pks_index_map: HashMap, + ) -> Self { let code = self.code.clone(); SigningTx::from(self) .sign_multisignature(keypairs, pks_index_map) diff --git a/core/src/types/key/mod.rs b/core/src/types/key/mod.rs index c6f41cdf7b..1ffd515e12 100644 --- a/core/src/types/key/mod.rs +++ b/core/src/types/key/mod.rs @@ -52,11 +52,11 @@ pub fn threshold_key(owner: &Address) -> storage::Key { /// Check if the given storage key is a public key. If it is, returns the owner. pub fn is_pk_key(key: &Key) -> Option<&Address> { match &key.segments[..] { - [DbKeySeg::AddressSeg(owner), DbKeySeg::StringSeg(key), DbKeySeg::StringSeg(_index)] - if key == PK_STORAGE_KEY => - { - Some(owner) - } + [ + DbKeySeg::AddressSeg(owner), + DbKeySeg::StringSeg(key), + DbKeySeg::StringSeg(_index), + ] if key == PK_STORAGE_KEY => Some(owner), _ => None, } } diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index 56b6f3ef6d..41b9d51b1e 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -166,7 +166,7 @@ pub struct InitAccount { /// The VP code pub vp_code: Vec, /// The multisignature threshold - pub threshold: u64 + pub threshold: u64, } /// A tx data type to initialize a new validator account. @@ -306,13 +306,17 @@ pub mod tx_types { { // verify signature and extract signed data TxType::Wrapper(wrapper) => { - let sig = tx_data.clone().get_signature_by_index(0).ok_or(TxError::SigError("Unsigned wrappr".to_string()))?; + let sig = tx_data.get_signature_by_index(0).ok_or( + TxError::SigError("Unsigned wrappr".to_string()), + )?; wrapper.validate_sig(signed_hash, &sig)?; Ok(TxType::Wrapper(wrapper)) } // verify signature and extract signed data TxType::Protocol(protocol) => { - let sig = tx_data.clone().get_signature_by_index(0).ok_or(TxError::SigError("Unsigned protocol".to_string()))?; + let sig = tx_data.get_signature_by_index(0).ok_or( + TxError::SigError("Unsigned protocol".to_string()), + )?; protocol.validate_sig(signed_hash, &sig)?; Ok(TxType::Protocol(protocol)) } @@ -525,7 +529,7 @@ pub mod tx_types { has_valid_pow: false, }; // Invalid signed data - let ed_sig = + let _ed_sig = ed25519::Signature::try_from_slice([0u8; 64].as_ref()).unwrap(); let signed = SignedTxData { data: Some( @@ -533,8 +537,8 @@ pub mod tx_types { .try_to_vec() .expect("Test failed"), ), - sigs: vec![] - // sig: common::Signature::try_from_sig(&ed_sig).unwrap(), + sigs: vec![], /* sig: common::Signature::try_from_sig(&ed_sig). + * unwrap(), */ }; // create the tx with signed decrypted data let tx = diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index 282bfc9f8b..184926505f 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -488,8 +488,11 @@ pub mod wrapper_tx { tx.data = Some(signed_tx_data.try_to_vec().expect("Test failed")); // check that the signature is not valid - tx.verify_sig(&keypair.ref_to(), &signed_tx_data.sigs.get(0).unwrap().sig) - .expect_err("Test failed"); + tx.verify_sig( + &keypair.ref_to(), + &signed_tx_data.sigs.get(0).unwrap().sig, + ) + .expect_err("Test failed"); // check that the try from method also fails let err = crate::types::transaction::process_tx(tx) .expect_err("Test failed"); diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index 595adc6b57..a0f7d70edf 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -461,7 +461,10 @@ mod tests { assert_eq!(&signed_tx_data.data, data); assert!( vp::CTX - .verify_tx_signature(&pk, &signed_tx_data.sigs.get(0).unwrap().sig) + .verify_tx_signature( + &pk, + &signed_tx_data.sigs.get(0).unwrap().sig + ) .unwrap() ); diff --git a/tests/src/vm_host_env/tx.rs b/tests/src/vm_host_env/tx.rs index a0d31284b7..940955a814 100644 --- a/tests/src/vm_host_env/tx.rs +++ b/tests/src/vm_host_env/tx.rs @@ -186,7 +186,7 @@ impl TestTxEnv { &mut self, address: &Address, public_key: &key::common::PublicKey, - index: u64 + index: u64, ) { let storage_key = key::pk_key(address, index); self.storage @@ -198,7 +198,7 @@ impl TestTxEnv { pub fn write_account_threshold( &mut self, address: &Address, - threshold: u64 + threshold: u64, ) { let storage_key = key::threshold_key(address); self.storage diff --git a/tx_prelude/src/account.rs b/tx_prelude/src/account.rs index f8ecbbd7fa..09574bdeda 100644 --- a/tx_prelude/src/account.rs +++ b/tx_prelude/src/account.rs @@ -1,5 +1,5 @@ +use namada_core::types::key::pk_key as the_keyyyyy; use namada_core::types::transaction::InitAccount; -use namada_core::types::key::{pk_key as the_keyyyyy}; use super::*; @@ -7,8 +7,8 @@ pub fn init_account(ctx: &mut Ctx, data: InitAccount) -> TxResult { let address = ctx.init_account(&data.vp_code)?; let pk_threshold = key::threshold_key(&address); - ctx.write(&pk_threshold, &data.threshold)?; - + ctx.write(&pk_threshold, data.threshold)?; + let mut index = 0; for pk in data.public_keys.iter() { let pk_key = the_keyyyyy(&address, index); @@ -17,4 +17,4 @@ pub fn init_account(ctx: &mut Ctx, data: InitAccount) -> TxResult { } Ok(()) -} \ No newline at end of file +} diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index da5db06017..3b93a26c20 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -6,11 +6,12 @@ #![deny(rustdoc::broken_intra_doc_links)] #![deny(rustdoc::private_intra_doc_links)] +pub mod account; +pub mod governance; pub mod ibc; pub mod key; pub mod proof_of_stake; pub mod token; -pub mod account; use core::slice; use std::marker::PhantomData; diff --git a/vp_prelude/src/key.rs b/vp_prelude/src/key.rs index 4635f502b2..e37ff7a92f 100644 --- a/vp_prelude/src/key.rs +++ b/vp_prelude/src/key.rs @@ -7,7 +7,11 @@ use super::*; /// Get the public key associated with the given address from the state prior to /// tx execution. Returns `Ok(None)` if not found. -pub fn get(ctx: &Ctx, owner: &Address, index: u64) -> EnvResult> { +pub fn get( + ctx: &Ctx, + owner: &Address, + index: u64, +) -> EnvResult> { storage_api::key::get(&ctx.pre(), owner, index) } @@ -16,4 +20,3 @@ pub fn get(ctx: &Ctx, owner: &Address, index: u64) -> EnvResult EnvResult> { storage_api::key::threshold(&ctx.pre(), owner) } - diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 883bb1a60b..6d9728d980 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -78,12 +78,14 @@ fn validate_tx( for sig_data in &signed_tx_data.sigs { let pk = key::get(&ctx, &addr, sig_data.index); if let Ok(Some(public_key)) = pk { - let signature_result = ctx.verify_tx_signature(&public_key, &sig_data.sig).unwrap_or(false); + let signature_result = ctx + .verify_tx_signature(&public_key, &sig_data.sig) + .unwrap_or(false); if signature_result { valid_signatures += 1; } if valid_signatures >= threshold { - return true + return true; } } } @@ -295,9 +297,9 @@ mod tests { // vp_host_env::set(vp_env); // assert!( - // !validate_tx(&CTX, tx_data, addr, keys_changed, verifiers).unwrap(), - // "Revealing PK that's already revealed should be rejected" - // ); + // !validate_tx(&CTX, tx_data, addr, keys_changed, + // verifiers).unwrap(), "Revealing PK that's already + // revealed should be rejected" ); } /// Test that a revealed PK that doesn't correspond to the account's address diff --git a/wasm/wasm_source/src/vp_testnet_faucet.rs b/wasm/wasm_source/src/vp_testnet_faucet.rs index ecbbde62c2..23b1e6b80f 100644 --- a/wasm/wasm_source/src/vp_testnet_faucet.rs +++ b/wasm/wasm_source/src/vp_testnet_faucet.rs @@ -38,12 +38,14 @@ fn validate_tx( for sig_data in &signed_tx_data.sigs { let pk = key::get(&ctx, &addr, sig_data.index); if let Ok(Some(public_key)) = pk { - let signature_result = ctx.verify_tx_signature(&public_key, &sig_data.sig).unwrap_or(false); + let signature_result = ctx + .verify_tx_signature(&public_key, &sig_data.sig) + .unwrap_or(false); if signature_result { valid_signatures += 1; } if valid_signatures >= threshold { - return true + return true; } } } diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 1b2e9f2f2e..f32dec1cd9 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -205,6 +205,8 @@ mod tests { use namada::ledger::pos::{GenesisValidator, PosParams}; use namada::types::storage::Epoch; use std::collections::HashMap; + + use address::testing::arb_non_internal_address; // Use this as `#[test]` annotation to enable logging use namada_tests::log::test; use namada_tests::native_vp::pos::init_pos; diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index 62fa84467c..246fc1f72c 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -77,12 +77,14 @@ fn validate_tx( for sig_data in &signed_tx_data.sigs { let pk = key::get(&ctx, &addr, sig_data.index); if let Ok(Some(public_key)) = pk { - let signature_result = ctx.verify_tx_signature(&public_key, &sig_data.sig).unwrap_or(false); + let signature_result = ctx + .verify_tx_signature(&public_key, &sig_data.sig) + .unwrap_or(false); if signature_result { valid_signatures += 1; } if valid_signatures >= threshold { - return true + return true; } } } From 59e62d307d110aca43070cd5a8526cc788e22d8b Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Mon, 30 Jan 2023 15:52:42 +0100 Subject: [PATCH 07/27] wip --- apps/src/lib/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 759edc083d..96cc29d5e2 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1054,7 +1054,7 @@ pub mod cmds { pub struct TxInitAccount(pub args::TxInitAccount); impl SubCmd for TxInitAccount { - const CMD: &'static str = "init-account-m"; + const CMD: &'static str = "init-account"; fn parse(matches: &ArgMatches) -> Option { matches.subcommand_matches(Self::CMD).map(|matches| { From df4ddcdf95a0921214c5faef22598bb649c1cc62 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Mon, 30 Jan 2023 17:43:20 +0100 Subject: [PATCH 08/27] wip --- apps/src/lib/cli.rs | 10 +++- apps/src/lib/cli/utils.rs | 1 - apps/src/lib/client/tx.rs | 112 +++++++++-------------------------- apps/src/lib/client/types.rs | 2 + core/src/proto/types.rs | 4 ++ 5 files changed, 42 insertions(+), 87 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 96cc29d5e2..9743368ac8 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2021,7 +2021,6 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let sources = SOURCE_MULTISIGNATURE.parse(matches); - println!("b: {:?}", sources); let vp_code_path = CODE_PATH_OPT.parse(matches); let public_keys = PUBLIC_KEY_MULTISIGNATURE.parse(matches); let threshold = THRESHOLD.parse(matches); @@ -2889,6 +2888,8 @@ pub mod args { pub signing_keys: Vec, /// Sign the tx with the keypair of the public key of the given address pub signers: Vec, + /// Dump the tx to file + pub dump_tx: bool, } impl Tx { @@ -2915,6 +2916,7 @@ pub mod args { .iter() .map(|signer| ctx.get(signer)) .collect(), + dump_tx: self.dump_tx } } } @@ -2969,6 +2971,9 @@ pub mod args { ) .conflicts_with(SIGNING_KEYS.name), ) + .arg( + DUMP_TX.def().about("Dump tx to file.") + ) } fn parse(matches: &ArgMatches) -> Self { @@ -2981,7 +2986,7 @@ pub mod args { let fee_amount = GAS_AMOUNT.parse(matches); let fee_token = GAS_TOKEN.parse(matches); let gas_limit = GAS_LIMIT.parse(matches).into(); - + let dump_tx = DUMP_TX.parse(matches).into(); let signing_keys = SIGNING_KEYS.parse(matches); let signers = SIGNERS.parse(matches); Self { @@ -2996,6 +3001,7 @@ pub mod args { gas_limit, signing_keys, signers, + dump_tx } } } diff --git a/apps/src/lib/cli/utils.rs b/apps/src/lib/cli/utils.rs index 1f19a3572f..01a15879d1 100644 --- a/apps/src/lib/cli/utils.rs +++ b/apps/src/lib/cli/utils.rs @@ -290,7 +290,6 @@ impl ArgMulti> { pub fn parse(&self, matches: &ArgMatches) -> Vec> { let raw = matches.values_of(self.name).unwrap_or_default(); - println!("c: {}", raw.len()); raw.map(|val| FromContext::new(val.to_string())).collect() } } diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 91e4196e8f..c6de65b714 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -11,6 +11,7 @@ use std::path::PathBuf; use async_std::io::prelude::WriteExt; use async_std::io::{self}; use borsh::{BorshDeserialize, BorshSerialize}; +use data_encoding::HEXLOWER; use itertools::Either::*; use itertools::Itertools; use masp_primitives::asset_type::AssetType; @@ -112,7 +113,7 @@ pub async fn submit_custom(ctx: Context, args: args::TxCustom) { }); let tx = Tx::new(tx_code, data); - let (_ctx, _initialized_accounts) = process_tx_multisignature( + let (_ctx, _initialized_accounts) = process_tx( ctx, &args.tx, tx, @@ -183,7 +184,7 @@ pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { let pks_map = rpc::get_account_pks(&client, &addr).await; - let (_ctx, _initialized_accounts) = process_tx_multisignature( + let (_ctx, _initialized_accounts) = process_tx( ctx, &args.tx, tx, @@ -200,7 +201,6 @@ pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { .public_keys .iter() .map(|pk| { - println!("{:?}", pk); ctx.get_cached(pk) }) .sorted() @@ -244,7 +244,7 @@ pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { } let tx = Tx::new(tx_code, Some(data)); - let (ctx, initialized_accounts) = process_tx_multisignature( + let (ctx, initialized_accounts) = process_tx( ctx, &args.tx, tx, @@ -385,7 +385,8 @@ pub async fn submit_init_validator( ctx, &tx_args, tx, - TxSigningKey::WalletAddress(source), + HashMap::new(), + vec![TxSigningKey::WalletAddress(source)], #[cfg(not(feature = "mainnet"))] false, ) @@ -1727,7 +1728,7 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { let pks_map = rpc::get_account_pks(&client, &source).await; - let (_ctx, _initialized_accounts) = process_tx_multisignature( + let (_ctx, _initialized_accounts) = process_tx( ctx, &args.tx, tx, @@ -1849,7 +1850,7 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { let pks_map = rpc::get_account_pks(&client, &source).await; - process_tx_multisignature( + process_tx( ctx, &args.tx, tx, @@ -2000,7 +2001,8 @@ pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { ctx, &args.tx, tx, - TxSigningKey::WalletAddress(signer), + HashMap::new(), + vec![TxSigningKey::WalletAddress(signer)], #[cfg(not(feature = "mainnet"))] false, ) @@ -2128,7 +2130,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { let tx_data = VoteProposalData { id: proposal_id, vote: args.vote, - voter: voter_address, + voter: voter_address.clone(), delegations: delegations.into_iter().collect(), }; @@ -2138,11 +2140,14 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { let tx_code = ctx.read_wasm(TX_VOTE_PROPOSAL); let tx = Tx::new(tx_code, Some(data)); + let pks_map = rpc::get_address_pks_map(&client, &voter_address).await; + process_tx( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(signer.clone()), + pks_map, + vec![TxSigningKey::WalletAddress(signer.clone())], #[cfg(not(feature = "mainnet"))] false, ) @@ -2417,7 +2422,7 @@ pub async fn submit_bond(ctx: Context, args: args::Bond) { let pks_map = rpc::get_account_pks(&client, bond_source).await; - let (_ctx, _initialized_accounts) = process_tx_multisignature( + let (_ctx, _initialized_accounts) = process_tx( ctx, &args.tx, tx, @@ -2476,7 +2481,7 @@ pub async fn submit_unbond(ctx: Context, args: args::Unbond) { let pks_map = rpc::get_account_pks(&client, &bond_source).await; - let (_ctx, _initialized_accounts) = process_tx_multisignature( + let (_ctx, _initialized_accounts) = process_tx( ctx, &args.tx, tx, @@ -2548,7 +2553,7 @@ pub async fn submit_withdraw(ctx: Context, args: args::Withdraw) { let pks_map = rpc::get_account_pks(&client, &bond_source).await; - let (_ctx, _initialized_accounts) = process_tx_multisignature( + let (_ctx, _initialized_accounts) = process_tx( ctx, &args.tx, tx, @@ -2638,7 +2643,7 @@ pub async fn submit_validator_commission_change( let pks_map = rpc::get_account_pks(&client, &validator).await; - let (_ctx, _initialized_accounts) = process_tx_multisignature( + let (_ctx, _initialized_accounts) = process_tx( ctx, &args.tx, tx, @@ -2650,7 +2655,9 @@ pub async fn submit_validator_commission_change( .await; } -async fn process_tx_multisignature( +/// Submit transaction and wait for result. Returns a list of addresses +/// initialized in the transaction if any. In dry run, this is always empty. +async fn process_tx( ctx: Context, args: &args::Tx, tx: Tx, @@ -2658,6 +2665,12 @@ async fn process_tx_multisignature( default_signers: Vec, #[cfg(not(feature = "mainnet"))] requires_pow: bool, ) -> (Context, Vec
) { + if args.dump_tx { + let serialized_signing_tx = tx.signing_tx().try_to_vec().expect("Tx should be serializable."); + println!("{}", HEXLOWER.encode(&serialized_signing_tx)); + safe_exit(1) + } + let (ctx, to_broadcast) = sign_tx_multisignature( ctx, tx, @@ -2710,75 +2723,6 @@ async fn process_tx_multisignature( } } -/// Submit transaction and wait for result. Returns a list of addresses -/// initialized in the transaction if any. In dry run, this is always empty. -async fn process_tx( - ctx: Context, - args: &args::Tx, - tx: Tx, - default_signer: TxSigningKey, - #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) -> (Context, Vec
) { - let (ctx, to_broadcast) = sign_tx( - ctx, - tx, - args, - default_signer, - #[cfg(not(feature = "mainnet"))] - requires_pow, - ) - .await; - // NOTE: use this to print the request JSON body: - - // let request = - // tendermint_rpc::endpoint::broadcast::tx_commit::Request::new( - // tx_bytes.clone().into(), - // ); - // use tendermint_rpc::Request; - // let request_body = request.into_json(); - // println!("HTTP request body: {}", request_body); - - if args.dry_run { - if let TxBroadcastData::DryRun(tx) = to_broadcast { - rpc::dry_run_tx(&args.ledger_address, tx.to_bytes()).await; - (ctx, vec![]) - } else { - panic!( - "Expected a dry-run transaction, received a wrapper \ - transaction instead" - ); - } - } else { - // Either broadcast or submit transaction and collect result into - // sum type - let result = if args.broadcast_only { - Left(broadcast_tx(args.ledger_address.clone(), &to_broadcast).await) - } else { - Right(submit_tx(args.ledger_address.clone(), to_broadcast).await) - }; - // Return result based on executed operation, otherwise deal with - // the encountered errors uniformly - match result { - Right(Ok(result)) => (ctx, result.initialized_accounts), - Left(Ok(_)) => (ctx, Vec::default()), - Right(Err(err)) => { - eprintln!( - "Encountered error while broadcasting transaction: {}", - err - ); - safe_exit(1) - } - Left(Err(err)) => { - eprintln!( - "Encountered error while broadcasting transaction: {}", - err - ); - safe_exit(1) - } - } - } -} - /// Save accounts initialized from a tx into the wallet, if any. async fn save_initialized_accounts( mut ctx: Context, diff --git a/apps/src/lib/client/types.rs b/apps/src/lib/client/types.rs index feb3bcdcdb..8876f2023e 100644 --- a/apps/src/lib/client/types.rs +++ b/apps/src/lib/client/types.rs @@ -39,6 +39,8 @@ pub struct ParsedTxArgs { pub signing_keys: Vec, /// Sign the tx with the keypair of the public key of the given address pub signers: Vec
, + /// Dump tx to file + pub dump_tx: bool } #[derive(Clone, Debug)] diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index ee182a7fd5..ee180891d0 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -441,6 +441,10 @@ impl Tx { .expect("code hashes to unexpected value") } + pub fn signing_tx(self) -> SigningTx { + SigningTx::from(self) + } + pub fn sign_multisignature( self, keypairs: &Vec, From 17b3f69cb0949c9cad1fe3f3a26c63b5eef30068 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Thu, 2 Feb 2023 17:27:00 +0100 Subject: [PATCH 09/27] wip --- apps/src/bin/namada-client/cli.rs | 3 + apps/src/lib/cli.rs | 102 +++++++++++++++++++--- apps/src/lib/client/rpc.rs | 48 +++++++++- apps/src/lib/client/signing.rs | 90 +++++++++---------- apps/src/lib/client/tx.rs | 42 ++++++--- apps/src/lib/client/types.rs | 11 ++- apps/src/lib/client/utils.rs | 3 +- core/src/proto/mod.rs | 2 +- core/src/proto/types.rs | 52 +++++++---- core/src/types/time.rs | 14 +++ wasm/wasm_source/src/vp_implicit.rs | 54 ++++++------ wasm/wasm_source/src/vp_testnet_faucet.rs | 6 +- wasm/wasm_source/src/vp_user.rs | 1 - wasm/wasm_source/src/vp_validator.rs | 14 +-- 14 files changed, 310 insertions(+), 132 deletions(-) diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index 14a07bb6c1..fe5ea9acc1 100644 --- a/apps/src/bin/namada-client/cli.rs +++ b/apps/src/bin/namada-client/cli.rs @@ -95,6 +95,9 @@ pub async fn main() -> Result<()> { Sub::QueryProtocolParameters(QueryProtocolParameters(args)) => { rpc::query_protocol_parameters(ctx, args).await; } + Sub::SignTx(SignTx(args)) => { + rpc::sign_tx(ctx, args).await + } } } cli::NamadaClient::WithoutContext(cmd, global_args) => match cmd { diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 9743368ac8..05c4e8841e 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -180,6 +180,8 @@ pub mod cmds { .subcommand(QueryProposal::def().display_order(3)) .subcommand(QueryProposalResult::def().display_order(3)) .subcommand(QueryProtocolParameters::def().display_order(3)) + // Commands + .subcommand(SignTx::def().display_order(4)) // Utils .subcommand(Utils::def().display_order(5)) } @@ -220,6 +222,7 @@ pub mod cmds { Self::parse_with_ctx(matches, QueryProposalResult); let query_protocol_parameters = Self::parse_with_ctx(matches, QueryProtocolParameters); + let sign_tx = Self::parse_with_ctx(matches, SignTx); let utils = SubCmd::parse(matches).map(Self::WithoutContext); tx_custom .or(tx_transfer) @@ -247,6 +250,7 @@ pub mod cmds { .or(query_proposal) .or(query_proposal_result) .or(query_protocol_parameters) + .or(sign_tx) .or(utils) } } @@ -310,6 +314,7 @@ pub mod cmds { QueryProposal(QueryProposal), QueryProposalResult(QueryProposalResult), QueryProtocolParameters(QueryProtocolParameters), + SignTx(SignTx) } #[allow(clippy::large_enum_variant)] @@ -1557,6 +1562,25 @@ pub mod cmds { .add_args::() } } + + #[derive(Clone, Debug)] + pub struct SignTx(pub args::SignTx); + + impl SubCmd for SignTx { + const CMD: &'static str = "sign-tx"; + + fn parse(matches: &ArgMatches) -> Option { + matches + .subcommand_matches(Self::CMD) + .map(|matches| Self(args::SignTx::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about("Sign and dump a transaction signature.") + .add_args::() + } + } } pub mod args { @@ -1639,7 +1663,6 @@ pub mod args { let raw = "127.0.0.1:26657"; TendermintAddress::from_str(raw).unwrap() })); - const LEDGER_ADDRESS: Arg = arg("ledger-address"); const LOCALHOST: ArgFlag = flag("localhost"); const MASP_VALUE: Arg = arg("value"); @@ -1673,7 +1696,9 @@ pub mod args { const SCHEME: ArgDefault = arg_default("scheme", DefaultFn(|| SchemeType::Ed25519)); const SIGNERS: ArgMulti = arg_multi("signers"); + const SIGNING_TX: ArgOpt = arg_opt("signing-tx"); const SIGNING_KEYS: ArgMulti = arg_multi("signing-keys"); + const SIGNATURES: ArgMulti = arg_multi("signatures"); const SOURCE: Arg = arg("source"); const SOURCE_MULTISIGNATURE: ArgMulti = arg_multi("sources"); const SOURCE_OPT: ArgOpt = SOURCE.opt(); @@ -1687,6 +1712,7 @@ pub mod args { const TRANSFER_TARGET: Arg = arg("target"); const THRESHOLD: ArgOpt = arg_opt("threshold"); const TX_HASH: Arg = arg("tx-hash"); + const TX_TIMESTAMP: ArgOpt = arg_opt("timestamp"); const UNSAFE_DONT_ENCRYPT: ArgFlag = flag("unsafe-dont-encrypt"); const UNSAFE_SHOW_SECRET: ArgFlag = flag("unsafe-show-secret"); const VALIDATOR: Arg = arg("validator"); @@ -1833,6 +1859,10 @@ pub mod args { pub code_path: PathBuf, /// Path to the data file pub data_path: Option, + /// Optional timestamp field + pub timestamp: Option, + /// The address + pub address: WalletAddress, } impl Args for TxCustom { @@ -1840,10 +1870,14 @@ pub mod args { let tx = Tx::parse(matches); let code_path = CODE_PATH.parse(matches); let data_path = DATA_PATH_OPT.parse(matches); + let timestamp = TX_TIMESTAMP.parse(matches); + let address = ADDRESS.parse(matches); Self { tx, code_path, data_path, + timestamp, + address, } } @@ -1859,6 +1893,48 @@ pub mod args { will be passed to the transaction code when it's \ executed.", )) + .arg( + TX_TIMESTAMP.def().about( + "The timestamp to set be set on the transaction.", + ), + ) + .arg(ADDRESS.def().about("The address to lookup.")) + } + } + + #[derive(Clone, Debug)] + pub struct SignTx { + pub tx: Tx, + pub data_path: Option, + pub signing_tx: Option, + } + + impl Args for SignTx { + fn parse(matches: &ArgMatches) -> Self { + let tx = Tx::parse(matches); + let data_path = DATA_PATH_OPT.parse(matches); + let signing_tx = SIGNING_TX.parse(matches); + Self { + tx, + data_path, + signing_tx, + } + } + + fn def(app: App) -> App { + app.add_args::() + .arg( + DATA_PATH_OPT + .def() + .about("Path to hex encoeded signing tx file.") + .conflicts_with(SIGNING_TX.name), + ) + .arg( + SIGNING_TX + .def() + .about("The hex encoded transaction to be signed.") + .conflicts_with(DATA_PATH_OPT.name), + ) } } @@ -2884,12 +2960,14 @@ pub mod args { pub fee_token: WalletAddress, /// The max amount of gas used to process tx pub gas_limit: GasLimit, + /// Dump the signing tx to file + pub dump_tx: bool, /// Sign the tx with the key for the given alias from your wallet pub signing_keys: Vec, /// Sign the tx with the keypair of the public key of the given address pub signers: Vec, - /// Dump the tx to file - pub dump_tx: bool, + /// The paths to signatures + pub signatures: Vec } impl Tx { @@ -2906,6 +2984,7 @@ pub mod args { fee_amount: self.fee_amount, fee_token: ctx.get(&self.fee_token), gas_limit: self.gas_limit.clone(), + dump_tx: self.dump_tx, signing_keys: self .signing_keys .iter() @@ -2916,7 +2995,7 @@ pub mod args { .iter() .map(|signer| ctx.get(signer)) .collect(), - dump_tx: self.dump_tx + signatures: self.signatures.clone() } } } @@ -2952,6 +3031,7 @@ pub mod args { "The maximum amount of gas needed to run transaction", ), ) + .arg(DUMP_TX.def().about("Dump tx to file.")) .arg( SIGNING_KEYS .def() @@ -2960,7 +3040,7 @@ pub mod args { public key, public key hash or alias from your \ wallet.", ) - .conflicts_with(SIGNERS.name), + .conflicts_with_all(&[SIGNERS.name, SIGNATURES.name]), ) .arg( SIGNERS @@ -2969,11 +3049,11 @@ pub mod args { "Sign the transaction with the keypair of the public \ key of the given address.", ) - .conflicts_with(SIGNING_KEYS.name), - ) - .arg( - DUMP_TX.def().about("Dump tx to file.") + .conflicts_with_all(&[SIGNATURES.name, SIGNING_KEYS.name]), ) + .arg(SIGNATURES.def().about( + "The paths to signatures." + ).conflicts_with_all(&[SIGNERS.name, SIGNING_KEYS.name])) } fn parse(matches: &ArgMatches) -> Self { @@ -2989,6 +3069,7 @@ pub mod args { let dump_tx = DUMP_TX.parse(matches).into(); let signing_keys = SIGNING_KEYS.parse(matches); let signers = SIGNERS.parse(matches); + let signatures = SIGNATURES.parse(matches); Self { dry_run, dump_tx, @@ -3001,7 +3082,8 @@ pub mod args { gas_limit, signing_keys, signers, - dump_tx + dump_tx, + signatures } } } diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index ecb0c0b5db..534b57854e 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -34,7 +34,7 @@ use namada::ledger::pos::{ }; use namada::ledger::queries::{self, RPC}; use namada::ledger::storage::ConversionState; -use namada::proto::{SignedTxData, Tx}; +use namada::proto::{SignedTxData, SigningTx, Tx}; use namada::types::address::{masp, tokens, Address}; use namada::types::governance::{ OfflineProposal, OfflineVote, ProposalResult, ProposalVote, TallyResult, @@ -54,7 +54,7 @@ use namada::types::transaction::{ use namada::types::{address, storage, token}; use tokio::time::{Duration, Instant}; -use crate::cli::{self, args, Context}; +use crate::cli::{self, args, safe_exit, Context}; use crate::client::tendermint_rpc_types::TxResponse; use crate::client::tx::{ Conversions, PinnedBalanceError, TransactionDelta, TransferDelta, @@ -67,6 +67,8 @@ use crate::facade::tendermint_rpc::{ Client, HttpClient, Order, SubscriptionClient, WebSocketClient, }; +use super::signing::{tx_signer, tx_signers, OfflineSignature}; + /// Query the status of a given transaction. /// /// If a response is not delivered until `deadline`, we exit the cli with an @@ -1382,6 +1384,48 @@ pub async fn query_bond( RPC.vp().pos().bond(client, source, validator, &epoch).await, ) } +pub async fn sign_tx( + mut ctx: Context, + args::SignTx { + mut tx, + data_path, + signing_tx, + }: args::SignTx, +) { + let data = match (data_path, signing_tx) { + (None, Some(data)) => HEXLOWER + .decode(data.as_bytes()) + .expect("SHould be hex decodable."), + (Some(path), None) => { + let data = fs::read(path.clone()).await.expect( + format!("File {} should exist.", path.to_string_lossy()) + .as_ref(), + ); + HEXLOWER.decode(&data).expect("SHould be hex decodable.") + } + (_, _) => { + println!("--data-path or --signing-tx required."); + safe_exit(1) + } + }; + + let signing_tx = + SigningTx::try_from_slice(&data).expect("Tx should be deserialiable."); + let keypair = + tx_signer(&mut ctx, &tx, super::signing::TxSigningKey::None).await; + + let signature = signing_tx.compute_signature(&keypair); + let public_key = keypair.ref_to(); + + let offline_signature = OfflineSignature { + sig: signature, + public_key, + }; + + let offline_signature_out = File::create("offline_signature.tx").unwrap(); + serde_json::to_writer_pretty(offline_signature_out, &offline_signature) + .expect("Signature should be deserializable.") +} pub async fn query_unbond_with_slashing( client: &HttpClient, diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index 61aab50e2d..64d6c0bb78 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; -use borsh::BorshSerialize; +use borsh::{BorshSerialize, BorshDeserialize, BorshSchema}; use namada::ledger::parameters::storage as parameter_storage; use namada::proto::Tx; @@ -14,6 +14,7 @@ use namada::types::storage::Epoch; use namada::types::token; use namada::types::token::Amount; use namada::types::transaction::{hash_tx, Fee, WrapperTx, MIN_FEE}; +use serde::{Serialize, Deserialize}; use super::rpc; use crate::cli::context::{WalletAddress, WalletKeypair}; @@ -23,6 +24,12 @@ use crate::facade::tendermint_config::net::Address as TendermintAddress; use crate::facade::tendermint_rpc::HttpClient; use crate::wallet::Wallet; +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct OfflineSignature { + pub sig: common::Signature, + pub public_key: common::PublicKey +} + /// Find the public key for the given address and try to load the keypair /// for it from the wallet. Panics if the key cannot be found or loaded. pub async fn find_keypair( @@ -209,58 +216,41 @@ pub async fn tx_signers( /// hashes needed for monitoring the tx on chain. /// /// If it is a dry run, it is not put in a wrapper, but returned as is. -pub async fn sign_tx( - mut ctx: Context, - tx: Tx, - args: &args::Tx, - default: TxSigningKey, - #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) -> (Context, TxBroadcastData) { - if args.dump_tx { - dump_tx_helper(&ctx, &tx, "unsigned", None); - } - - let keypair = tx_signer(&mut ctx, args, default).await; - let tx = tx.sign(&keypair); - if args.dump_tx { - dump_tx_helper(&ctx, &tx, "signed", None); - } - - let epoch = rpc::query_and_print_epoch(args::Query { - ledger_address: args.ledger_address.clone(), - }) - .await; - let broadcast_data = if args.dry_run { - TxBroadcastData::DryRun(tx) - } else { - sign_wrapper( - &ctx, - args, - epoch, - tx, - &keypair, - #[cfg(not(feature = "mainnet"))] - requires_pow, - ) - .await - }; +// pub async fn sign_tx( +// mut ctx: Context, +// tx: Tx, +// args: &args::Tx, +// default: TxSigningKey, +// #[cfg(not(feature = "mainnet"))] requires_pow: bool, +// ) -> (Context, TxBroadcastData) { +// let keypair = tx_signer(&mut ctx, args, default).await; +// let tx = tx.sign(&keypair); - if args.dump_tx && !args.dry_run { - let (wrapper_tx, wrapper_hash) = match broadcast_data { - TxBroadcastData::DryRun(_) => panic!( - "somehow created a dry run transaction without --dry-run" - ), - TxBroadcastData::Wrapper { - ref tx, - ref wrapper_hash, - decrypted_hash: _, - } => (tx, wrapper_hash), - }; +// let epoch = rpc::query_epoch(args::Query { +// ledger_address: args.ledger_address.clone(), +// }) +// .await; +// let broadcast_data = if args.dry_run { +// TxBroadcastData::DryRun(tx) +// } else { +// sign_wrapper( +// &ctx, +// args, +// epoch, +// tx, +// &keypair, +// #[cfg(not(feature = "mainnet"))] +// requires_pow, +// ) +// .await +// }; +// (ctx, broadcast_data) +// } - dump_tx_helper(&ctx, wrapper_tx, "wrapper", Some(wrapper_hash)); - } +pub async fn offline_sign( - (ctx, broadcast_data) +) { + } pub fn dump_tx_helper( diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index c6de65b714..bdcbabb230 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use std::collections::hash_map::Entry; use std::collections::{BTreeMap, HashMap, HashSet}; -use std::env; +use std::{env, fs}; use std::fmt::Debug; use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; @@ -72,9 +72,9 @@ use super::types::ShieldedTransferContext; use crate::cli::context::WalletAddress; use crate::cli::{args, safe_exit, Context}; use crate::client::rpc::{query_conversion, query_storage_value}; -use crate::client::signing::{find_keypair, sign_tx, tx_signer, TxSigningKey}; +use crate::client::signing::{find_keypair, tx_signer, TxSigningKey}; use crate::client::tendermint_rpc_types::{TxBroadcastData, TxResponse}; -use crate::client::types::ParsedTxTransferArgs; +use crate::client::types::{ParsedTxTransferArgs}; use crate::facade::tendermint_config::net::Address as TendermintAddress; use crate::facade::tendermint_rpc::endpoint::broadcast::tx_sync::Response; use crate::facade::tendermint_rpc::error::Error as RpcError; @@ -105,19 +105,22 @@ const ENV_VAR_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS: &str = const DEFAULT_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS: u64 = 60; pub async fn submit_custom(ctx: Context, args: args::TxCustom) { - let _client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); + let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); - let tx_code = ctx.read_wasm(args.code_path); - let data = args.data_path.map(|data_path| { - std::fs::read(data_path).expect("Expected a file at given data path") - }); - let tx = Tx::new(tx_code, data); + let tx_code = fs::read(args.clone().code_path).expect("Code path file not found."); + let tx_data = std::fs::read(args.clone().data_path.unwrap()).expect("Expected a file at given data path"); + let timestamp = args.timestamp.unwrap_or_default(); + + let address = ctx.get(&args.address); + let pks_index_map = rpc::get_account_pks(&client, &address).await; + + let tx = Tx::new(tx_code, Some(tx_data)); let (_ctx, _initialized_accounts) = process_tx( ctx, &args.tx, tx, - HashMap::new(), + pks_index_map, vec![TxSigningKey::None], #[cfg(not(feature = "mainnet"))] false, @@ -1718,6 +1721,7 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { } }, }; + tracing::debug!("Transfer data {:?}", transfer); let data = transfer .try_to_vec() @@ -2666,8 +2670,16 @@ async fn process_tx( #[cfg(not(feature = "mainnet"))] requires_pow: bool, ) -> (Context, Vec
) { if args.dump_tx { - let serialized_signing_tx = tx.signing_tx().try_to_vec().expect("Tx should be serializable."); - println!("{}", HEXLOWER.encode(&serialized_signing_tx)); + fs::write("code.tx", tx.clone().code); + if tx.data.is_some() { + fs::write("data.tx", tx.clone().data.unwrap()); + } + + let signing_tx_blob = tx.clone().signing_tx().try_to_vec().expect("Tx should be serializable."); + println!("Transaction code, data and timestamp have been written to files code.tx and data.tx."); + println!("You can share the following blob with the other signers:\n{}\n", HEXLOWER.encode(&signing_tx_blob)); + println!("You can later submit the tx with: namada client tx --code-path code.tx --data-path data.tx --timestamp {}", tx.timestamp.to_rfc3339()); + safe_exit(1) } @@ -2902,3 +2914,9 @@ pub async fn submit_tx( parsed } + +async fn dump_tx( + +) { + +} \ No newline at end of file diff --git a/apps/src/lib/client/types.rs b/apps/src/lib/client/types.rs index 8876f2023e..86dccf52b4 100644 --- a/apps/src/lib/client/types.rs +++ b/apps/src/lib/client/types.rs @@ -1,13 +1,18 @@ +use std::collections::HashMap; +use std::path::PathBuf; + use async_trait::async_trait; use masp_primitives::merkle_tree::MerklePath; use masp_primitives::primitives::{Diversifier, Note, ViewingKey}; use masp_primitives::sapling::Node; use masp_primitives::transaction::components::Amount; use namada::types::address::Address; +use namada::types::key::common; use namada::types::masp::{TransferSource, TransferTarget}; use namada::types::storage::Epoch; use namada::types::transaction::GasLimit; use namada::types::{key, token}; +use serde::{Serialize, Deserialize}; use super::rpc; use crate::cli::{args, Context}; @@ -35,12 +40,14 @@ pub struct ParsedTxArgs { pub fee_token: Address, /// The max amount of gas used to process tx pub gas_limit: GasLimit, + /// Dump the signing tx to file + pub dump_tx: bool, /// Sign the tx with the key for the given alias from your wallet pub signing_keys: Vec, /// Sign the tx with the keypair of the public key of the given address pub signers: Vec
, - /// Dump tx to file - pub dump_tx: bool + /// The path to signatures + pub signatures: Vec } #[derive(Clone, Debug)] diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index 21fed46c11..bac31f612e 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -7,6 +7,7 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use borsh::BorshSerialize; +use data_encoding::HEXLOWER; use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression; @@ -21,7 +22,7 @@ use serde_json::json; use sha2::{Digest, Sha256}; use crate::cli::context::ENV_VAR_WASM_DIR; -use crate::cli::{self, args}; +use crate::cli::{self, args, safe_exit}; use crate::config::genesis::genesis_config::{ self, HexString, ValidatorPreGenesisConfig, }; diff --git a/core/src/proto/mod.rs b/core/src/proto/mod.rs index 3271037595..b99e4d9ed2 100644 --- a/core/src/proto/mod.rs +++ b/core/src/proto/mod.rs @@ -3,7 +3,7 @@ pub mod generated; mod types; -pub use types::{Dkg, Error, Signed, SignedTxData, Tx}; +pub use types::{Dkg, Error, Signed, SignedTxData, Tx, SigningTx}; #[cfg(test)] mod tests { diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index ee180891d0..de260958c8 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -204,33 +204,41 @@ impl SigningTx { } } + pub fn compute_signature( + self, + keypair: &common::SecretKey + ) -> common::Signature { + let to_sign = self.hash(); + common::SigScheme::sign(keypair, to_sign) + } + pub fn sign_multisignature( self, keypairs: &Vec, pks_index_map: HashMap, ) -> Self { let to_sign = self.hash(); + let signatures: Vec = keypairs + .iter() + .filter_map(|key| { + let signature = common::SigScheme::sign(key, to_sign); + let pk = key.ref_to(); + let pk_index = pks_index_map.get(&pk); + pk_index.map(|index| SignatureIndex { + sig: signature, + index: *index, + }) + }) + .collect(); + let signed = SignedTxData { data: self.data, - sigs: keypairs - .iter() - .filter_map(|key| { - let signature = common::SigScheme::sign(key, to_sign); - let pk = key.ref_to(); - let pk_index = pks_index_map.get(&pk); - pk_index.map(|index| SignatureIndex { - sig: signature, - index: *index, - }) - }) - .collect(), - } - .try_to_vec() - .expect("Encoding transaction data shouldn't fail"); + sigs: signatures + }; SigningTx { code_hash: self.code_hash, - data: Some(signed), + data: signed.try_to_vec().ok(), timestamp: self.timestamp, } } @@ -416,6 +424,18 @@ impl Tx { } } + pub fn new_with_timestamp( + code: Vec, + data: Option>, + timestamp: DateTimeUtc, + ) -> Self { + Tx { + code, + data, + timestamp, + } + } + pub fn to_bytes(&self) -> Vec { let mut bytes = vec![]; let tx: types::Tx = self.clone().into(); diff --git a/core/src/types/time.rs b/core/src/types/time.rs index 7288d88bab..75b09f9152 100644 --- a/core/src/types/time.rs +++ b/core/src/types/time.rs @@ -111,6 +111,12 @@ impl DateTimeUtc { } } +impl Default for DateTimeUtc { + fn default() -> Self { + DateTimeUtc::now() + } +} + impl FromStr for DateTimeUtc { type Err = ParseError; @@ -241,6 +247,14 @@ impl From for Rfc3339String { } } +impl Display for DateTimeUtc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, "{}", self.0.to_string() + ) + } +} + #[cfg(any(feature = "tendermint", feature = "tendermint-abcipp"))] impl TryFrom for crate::tendermint::time::Time { type Error = crate::tendermint::Error; diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 6d9728d980..620a357701 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -124,7 +124,7 @@ fn validate_tx( } } } - false + true } KeyType::Token(owner) => { if owner == &addr { @@ -278,28 +278,28 @@ mod tests { ); // Commit the transaction and create another tx_env - // let vp_env = vp_host_env::take(); - // tx_host_env::set_from_vp_env(vp_env); - // tx_host_env::commit_tx_and_block(); - // let tx_env = tx_host_env::take(); - - // // Try to reveal it again - // vp_host_env::init_from_tx(addr.clone(), tx_env, |_address| { - // // Apply reveal_pk in a transaction - // tx_host_env::key::reveal_pk(tx::ctx(), &public_key).unwrap(); - // }); - - // let vp_env = vp_host_env::take(); - // let tx_data: Vec = vec![]; - // let keys_changed: BTreeSet = - // vp_env.all_touched_storage_keys(); - // let verifiers: BTreeSet
= BTreeSet::default(); - // vp_host_env::set(vp_env); - - // assert!( - // !validate_tx(&CTX, tx_data, addr, keys_changed, - // verifiers).unwrap(), "Revealing PK that's already - // revealed should be rejected" ); + let vp_env = vp_host_env::take(); + tx_host_env::set_from_vp_env(vp_env); + tx_host_env::commit_tx_and_block(); + let tx_env = tx_host_env::take(); + + // Try to reveal it again + vp_host_env::init_from_tx(addr.clone(), tx_env, |_address| { + // Apply reveal_pk in a transaction + tx_host_env::key::reveal_pk(tx::ctx(), &public_key).unwrap(); + }); + + let vp_env = vp_host_env::take(); + let tx_data: Vec = vec![]; + let keys_changed: BTreeSet = + vp_env.all_touched_storage_keys(); + let verifiers: BTreeSet
= BTreeSet::default(); + vp_host_env::set(vp_env); + + assert!( + !validate_tx(&CTX, tx_data, addr, keys_changed, + verifiers).unwrap(), "Revealing PK that's already + revealed should be rejected" ); } /// Test that a revealed PK that doesn't correspond to the account's address @@ -587,7 +587,7 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -741,7 +741,7 @@ mod tests { tx_env.spawn_accounts(storage_key_addresses); let public_key = secret_key.ref_to(); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { @@ -824,7 +824,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -866,7 +866,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { diff --git a/wasm/wasm_source/src/vp_testnet_faucet.rs b/wasm/wasm_source/src/vp_testnet_faucet.rs index 23b1e6b80f..6975874fd4 100644 --- a/wasm/wasm_source/src/vp_testnet_faucet.rs +++ b/wasm/wasm_source/src/vp_testnet_faucet.rs @@ -249,7 +249,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - tx_env.write_public_key(&vp_owner, public_key); + tx_env.write_public_key(&vp_owner, public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -365,7 +365,7 @@ mod tests { let sig = key::common::SigScheme::sign(&target_key, &solution_bytes); let signed_solution = SignedTxData { data: Some(solution_bytes), - sig, + sigs: Vec::new(), }; // Initialize VP environment from a transaction @@ -413,7 +413,7 @@ mod tests { let storage_key_addresses = storage_key.find_addresses(); tx_env.spawn_accounts(storage_key_addresses); - tx_env.write_public_key(&vp_owner, public_key); + tx_env.write_public_key(&vp_owner, public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index f32dec1cd9..0c2f1f8e5e 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -74,7 +74,6 @@ fn validate_tx( return false; } let mut valid_signatures = 0; - for sig_data in &signed_tx_data.sigs { let pk = key::get(&ctx, &addr, sig_data.index); if let Ok(Some(public_key)) = pk { diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index 246fc1f72c..45e3adf160 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -356,7 +356,7 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -655,7 +655,7 @@ mod tests { let storage_key_addresses = storage_key.find_addresses(); tx_env.spawn_accounts(storage_key_addresses); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { @@ -731,7 +731,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -772,7 +772,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -815,7 +815,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -862,7 +862,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -904,7 +904,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { From 41c55ccd85e9c355f283dcc1687d735016338538 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Fri, 3 Feb 2023 15:47:06 +0100 Subject: [PATCH 10/27] wip --- apps/src/lib/cli.rs | 8 +-- apps/src/lib/client/rpc.rs | 15 +++--- apps/src/lib/client/signing.rs | 82 +++++++------------------------ apps/src/lib/client/tx.rs | 27 +++++----- apps/src/lib/client/types.rs | 6 +-- apps/src/lib/client/utils.rs | 4 +- core/src/proto/types.rs | 33 ++++++++++++- core/src/types/time.rs | 3 +- core/src/types/transaction/mod.rs | 4 +- tx_prelude/src/account.rs | 8 ++- wasm/checksums.json | 36 +++++++------- wasm/wasm_source/src/vp_user.rs | 2 +- 12 files changed, 104 insertions(+), 124 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 05c4e8841e..40e38aec24 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -3040,7 +3040,7 @@ pub mod args { public key, public key hash or alias from your \ wallet.", ) - .conflicts_with_all(&[SIGNERS.name, SIGNATURES.name]), + .conflicts_with_all(&[SIGNERS.name]), ) .arg( SIGNERS @@ -3049,11 +3049,11 @@ pub mod args { "Sign the transaction with the keypair of the public \ key of the given address.", ) - .conflicts_with_all(&[SIGNATURES.name, SIGNING_KEYS.name]), + .conflicts_with_all(&[SIGNING_KEYS.name]), ) .arg(SIGNATURES.def().about( "The paths to signatures." - ).conflicts_with_all(&[SIGNERS.name, SIGNING_KEYS.name])) + )) } fn parse(matches: &ArgMatches) -> Self { @@ -3066,7 +3066,7 @@ pub mod args { let fee_amount = GAS_AMOUNT.parse(matches); let fee_token = GAS_TOKEN.parse(matches); let gas_limit = GAS_LIMIT.parse(matches).into(); - let dump_tx = DUMP_TX.parse(matches).into(); + let dump_tx = DUMP_TX.parse(matches); let signing_keys = SIGNING_KEYS.parse(matches); let signers = SIGNERS.parse(matches); let signatures = SIGNATURES.parse(matches); diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 534b57854e..310d3f26a1 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -67,7 +67,7 @@ use crate::facade::tendermint_rpc::{ Client, HttpClient, Order, SubscriptionClient, WebSocketClient, }; -use super::signing::{tx_signer, tx_signers, OfflineSignature}; +use super::signing::{tx_signer, OfflineSignature}; /// Query the status of a given transaction. /// @@ -1387,7 +1387,7 @@ pub async fn query_bond( pub async fn sign_tx( mut ctx: Context, args::SignTx { - mut tx, + tx, data_path, signing_tx, }: args::SignTx, @@ -1397,10 +1397,7 @@ pub async fn sign_tx( .decode(data.as_bytes()) .expect("SHould be hex decodable."), (Some(path), None) => { - let data = fs::read(path.clone()).await.expect( - format!("File {} should exist.", path.to_string_lossy()) - .as_ref(), - ); + let data = fs::read(path.clone()).await.unwrap_or_else(|_| panic!("File {} should exist.", path.to_string_lossy())); HEXLOWER.decode(&data).expect("SHould be hex decodable.") } (_, _) => { @@ -2629,9 +2626,9 @@ pub async fn get_address_pks_map( if let Some(pks) = pk_iter { let mut pks_map = HashMap::new(); - pks.enumerate() - .map(|(index, (_, pk))| pks_map.insert(pk, index as u64)); - + pks.enumerate().for_each(|(index, (_, pk))| { + pks_map.insert(pk, index as u64); + }); pks_map } else { HashMap::new() diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index 64d6c0bb78..ba8ffd7b96 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -2,8 +2,9 @@ //! wallet. use std::collections::HashMap; +use std::fs; -use borsh::{BorshSerialize, BorshDeserialize, BorshSchema}; +use borsh::{BorshSerialize}; use namada::ledger::parameters::storage as parameter_storage; use namada::proto::Tx; @@ -216,68 +217,6 @@ pub async fn tx_signers( /// hashes needed for monitoring the tx on chain. /// /// If it is a dry run, it is not put in a wrapper, but returned as is. -// pub async fn sign_tx( -// mut ctx: Context, -// tx: Tx, -// args: &args::Tx, -// default: TxSigningKey, -// #[cfg(not(feature = "mainnet"))] requires_pow: bool, -// ) -> (Context, TxBroadcastData) { -// let keypair = tx_signer(&mut ctx, args, default).await; -// let tx = tx.sign(&keypair); - -// let epoch = rpc::query_epoch(args::Query { -// ledger_address: args.ledger_address.clone(), -// }) -// .await; -// let broadcast_data = if args.dry_run { -// TxBroadcastData::DryRun(tx) -// } else { -// sign_wrapper( -// &ctx, -// args, -// epoch, -// tx, -// &keypair, -// #[cfg(not(feature = "mainnet"))] -// requires_pow, -// ) -// .await -// }; -// (ctx, broadcast_data) -// } - -pub async fn offline_sign( - -) { - -} - -pub fn dump_tx_helper( - ctx: &Context, - tx: &Tx, - extension: &str, - precomputed_hash: Option<&String>, -) { - let chain_dir = ctx.config.ledger.chain_dir(); - let hash = match precomputed_hash { - Some(hash) => hash.to_owned(), - None => { - let hash: Hash = tx - .hash() - .as_ref() - .try_into() - .expect("expected hash of dumped tx to be a hash"); - format!("{}", hash) - } - }; - let filename = chain_dir.join(hash).with_extension(extension); - let tx_bytes = tx.to_bytes(); - - std::fs::write(filename, tx_bytes) - .expect("expected to be able to write tx dump file"); -} - pub async fn sign_tx_multisignature( mut ctx: Context, tx: Tx, @@ -287,7 +226,22 @@ pub async fn sign_tx_multisignature( #[cfg(not(feature = "mainnet"))] requires_pow: bool, ) -> (Context, TxBroadcastData) { let keypairs = tx_signers(&mut ctx, args, default).await; - let tx = tx.sign_multisignature(&keypairs, pks_index_map); + let tx = match args.signatures.is_empty() { + true => { + tx.sign_multisignature(&keypairs, pks_index_map) + }, + false => { + let signatures = args.signatures.iter().map(|signature_path| { + let content = fs::read(signature_path).expect("Signature file should exist."); + let offline_signature: OfflineSignature = serde_json::from_slice(&content).expect("Signature should be deserializable."); + (offline_signature.public_key, offline_signature.sig) + }).collect::>(); + println!("{:?}", signatures); + println!("{:?}", pks_index_map); + tx.add_signatures(signatures, pks_index_map) + }, + }; + let epoch = rpc::query_epoch(args::Query { ledger_address: args.ledger_address.clone(), diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index bdcbabb230..36a9ebc2e1 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -111,10 +111,12 @@ pub async fn submit_custom(ctx: Context, args: args::TxCustom) { let tx_data = std::fs::read(args.clone().data_path.unwrap()).expect("Expected a file at given data path"); let timestamp = args.timestamp.unwrap_or_default(); + // TODO: check if 1 signer and use the address dervied from that signer + // TODO: make args.address optional let address = ctx.get(&args.address); let pks_index_map = rpc::get_account_pks(&client, &address).await; - let tx = Tx::new(tx_code, Some(tx_data)); + let tx = Tx::new_with_timestamp(tx_code, Some(tx_data), timestamp); let (_ctx, _initialized_accounts) = process_tx( ctx, @@ -199,6 +201,9 @@ pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { .await; } + + + pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { let public_keys: Vec = args .public_keys @@ -220,6 +225,7 @@ pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { } } }; + // TODO: check that threshold should be <= pks len let vp_code = args .vp_code_path @@ -2670,17 +2676,18 @@ async fn process_tx( #[cfg(not(feature = "mainnet"))] requires_pow: bool, ) -> (Context, Vec
) { if args.dump_tx { - fs::write("code.tx", tx.clone().code); - if tx.data.is_some() { - fs::write("data.tx", tx.clone().data.unwrap()); + // TODO: use async version of fs + tokio::fs::write("code.tx", tx.clone().code).await.expect("Should be able to write a file to disk."); + if let Some(ref data) = tx.data { + tokio::fs::write("data.tx", data).await.expect("Should be able to write a file to disk."); } let signing_tx_blob = tx.clone().signing_tx().try_to_vec().expect("Tx should be serializable."); - println!("Transaction code, data and timestamp have been written to files code.tx and data.tx."); + println!("Transaction code, data and timestmamp have been written to files code.tx and data.tx."); println!("You can share the following blob with the other signers:\n{}\n", HEXLOWER.encode(&signing_tx_blob)); - println!("You can later submit the tx with: namada client tx --code-path code.tx --data-path data.tx --timestamp {}", tx.timestamp.to_rfc3339()); + println!("You can later submit the tx with:\nnamada client tx --code-path code.tx --data-path data.tx --timestamp {}", tx.timestamp.to_rfc3339()); - safe_exit(1) + return (ctx, vec![]); } let (ctx, to_broadcast) = sign_tx_multisignature( @@ -2913,10 +2920,4 @@ pub async fn submit_tx( ); parsed -} - -async fn dump_tx( - -) { - } \ No newline at end of file diff --git a/apps/src/lib/client/types.rs b/apps/src/lib/client/types.rs index 86dccf52b4..c1062c2fe6 100644 --- a/apps/src/lib/client/types.rs +++ b/apps/src/lib/client/types.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; + use std::path::PathBuf; use async_trait::async_trait; @@ -7,12 +7,12 @@ use masp_primitives::primitives::{Diversifier, Note, ViewingKey}; use masp_primitives::sapling::Node; use masp_primitives::transaction::components::Amount; use namada::types::address::Address; -use namada::types::key::common; + use namada::types::masp::{TransferSource, TransferTarget}; use namada::types::storage::Epoch; use namada::types::transaction::GasLimit; use namada::types::{key, token}; -use serde::{Serialize, Deserialize}; + use super::rpc; use crate::cli::{args, Context}; diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index bac31f612e..623ae4a813 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use borsh::BorshSerialize; -use data_encoding::HEXLOWER; + use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression; @@ -22,7 +22,7 @@ use serde_json::json; use sha2::{Digest, Sha256}; use crate::cli::context::ENV_VAR_WASM_DIR; -use crate::cli::{self, args, safe_exit}; +use crate::cli::{self, args}; use crate::config::genesis::genesis_config::{ self, HexString, ValidatorPreGenesisConfig, }; diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index de260958c8..acaa4498d4 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -214,7 +214,7 @@ impl SigningTx { pub fn sign_multisignature( self, - keypairs: &Vec, + keypairs: &[common::SecretKey], pks_index_map: HashMap, ) -> Self { let to_sign = self.hash(); @@ -465,9 +465,38 @@ impl Tx { SigningTx::from(self) } + pub fn add_signatures( + self, + signatures: Vec<(common::PublicKey, common::Signature)>, + pks_index_map: HashMap + ) -> Self { + let sigs = signatures.iter().filter_map(|(pk, sig)| { + let pk_index = pks_index_map.get(pk); + pk_index.map(|index| { + SignatureIndex { + sig: sig.clone(), + index: *index, + } + }) + }).collect::>(); + + println!("{:?}", sigs); + + let signed_tx_data = SignedTxData { + data: self.data.clone(), + sigs, + }; + + SigningTx { + code_hash: self.clone().code_hash(), + data: signed_tx_data.try_to_vec().ok(), + timestamp: self.timestamp, + }.expand(self.code).expect("code hashes to unexpected value") + } + pub fn sign_multisignature( self, - keypairs: &Vec, + keypairs: &[common::SecretKey], pks_index_map: HashMap, ) -> Self { let code = self.code.clone(); diff --git a/core/src/types/time.rs b/core/src/types/time.rs index 75b09f9152..395d4c0a4f 100644 --- a/core/src/types/time.rs +++ b/core/src/types/time.rs @@ -121,6 +121,7 @@ impl FromStr for DateTimeUtc { type Err = ParseError; fn from_str(s: &str) -> Result { + // DateTime::parse_from_rfc_3339(s) Ok(Self(s.parse::>()?)) } } @@ -250,7 +251,7 @@ impl From for Rfc3339String { impl Display for DateTimeUtc { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( - f, "{}", self.0.to_string() + f, "{}", self.0 ) } } diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index 41b9d51b1e..5e516d787d 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -306,7 +306,7 @@ pub mod tx_types { { // verify signature and extract signed data TxType::Wrapper(wrapper) => { - let sig = tx_data.get_signature_by_index(0).ok_or( + let sig = tx_data.get_signature_by_index(0).ok_or_else(|| TxError::SigError("Unsigned wrappr".to_string()), )?; wrapper.validate_sig(signed_hash, &sig)?; @@ -314,7 +314,7 @@ pub mod tx_types { } // verify signature and extract signed data TxType::Protocol(protocol) => { - let sig = tx_data.get_signature_by_index(0).ok_or( + let sig = tx_data.get_signature_by_index(0).ok_or_else(|| TxError::SigError("Unsigned protocol".to_string()), )?; protocol.validate_sig(signed_hash, &sig)?; diff --git a/tx_prelude/src/account.rs b/tx_prelude/src/account.rs index 09574bdeda..6d6f188056 100644 --- a/tx_prelude/src/account.rs +++ b/tx_prelude/src/account.rs @@ -1,4 +1,4 @@ -use namada_core::types::key::pk_key as the_keyyyyy; +use namada_core::types::key::pk_key; use namada_core::types::transaction::InitAccount; use super::*; @@ -9,11 +9,9 @@ pub fn init_account(ctx: &mut Ctx, data: InitAccount) -> TxResult { let pk_threshold = key::threshold_key(&address); ctx.write(&pk_threshold, data.threshold)?; - let mut index = 0; - for pk in data.public_keys.iter() { - let pk_key = the_keyyyyy(&address, index); + for (pk, index) in data.public_keys.iter().zip(0u64..) { + let pk_key = pk_key(&address, index); ctx.write(&pk_key, pk)?; - index += 1; } Ok(()) diff --git a/wasm/checksums.json b/wasm/checksums.json index 7c2b824921..4564f4cfaa 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,20 +1,20 @@ { - "tx_bond.wasm": "tx_bond.f0094b887c57565472bede01d98fb77f6faac2f72597e2efb2ebfe9b1bf7c234.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.02dca468021b1ec811d0f35cc4b55a24f7c3f7b5e51f16399709257421f4a1f4.wasm", - "tx_ibc.wasm": "tx_ibc.a1735e3221f1ae055c74bb52327765dd37e8676e15fab496f9ab0ed4d0628f51.wasm", - "tx_init_account.wasm": "tx_init_account.7b6eafeceb81b679c382279a5d9c40dfd81fcf37e5a1940340355c9f55af1543.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.f2ed71fe70fc564e1d67e4e7d2ea25466327b62ba2eee18ece0021abff9e2c82.wasm", - "tx_init_validator.wasm": "tx_init_validator.fedcfaecaf37e3e7d050c76a4512baa399fc528710a27038573df53596613a2c.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.3e5417561e8108d4045775bf6d095cbaad22c73ff17a5ba2ad11a1821665a58a.wasm", - "tx_transfer.wasm": "tx_transfer.833a3849ca2c417f4e907c95c6eb15e6b52827458cf603e1c4f5511ab3e4fe76.wasm", - "tx_unbond.wasm": "tx_unbond.d4fd6c94abb947533a2728940b43fb021a008ad593c7df7a3886c4260cac39b5.wasm", - "tx_update_vp.wasm": "tx_update_vp.6d1eabab15dc6d04eec5b25ad687f026a4d6c3f118a1d7aca9232394496bd845.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.54b594f686a72869b0d7f15492591854f26e287a1cf3b6e543b0246d5ac61003.wasm", - "tx_withdraw.wasm": "tx_withdraw.342c222d0707eb5b5a44b89fc1245f527be3fdf841af64152a9ab35a4766e1b5.wasm", - "vp_implicit.wasm": "vp_implicit.73678ac01aa009ac4e0d4a49eecaa19b49cdd3d95f6862a9558c9b175ae68260.wasm", - "vp_masp.wasm": "vp_masp.85446251f8e1defed81549dab37edfe8e640339c7230e678b65340cf71ce1369.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.573b882a806266d6cdfa635fe803e46d6ce89c99321196c231c61d05193a086d.wasm", - "vp_token.wasm": "vp_token.8c6e5a86f047e7b1f1004f0d8a4e91fad1b1c0226a6e42d7fe350f98dc84359b.wasm", - "vp_user.wasm": "vp_user.75c68f018f163d18d398cb4082b261323d115aae43ec021c868d1128e4b0ee29.wasm", - "vp_validator.wasm": "vp_validator.2dc9f1c8f106deeef5ee988955733955444d16b400ebb16a25e7d71e4b1be874.wasm" + "tx_bond.wasm": "tx_bond.413d1faa4523d0e0f7f439191bd3a79c70ad1e1ae5018ef0a63d1d6782fdee31.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.e9fe65500839ad0b080f8a06fa39b243235f958323846cbcfaaecf8d1dd78f61.wasm", + "tx_ibc.wasm": "tx_ibc.40a6cb9dccc536ddccf9bb8f5ccc647535956a651191dfe2259fafc8c96ee647.wasm", + "tx_init_account.wasm": "tx_init_account.f7c3b99198b6bd7966720c32bea4d74e02612a5660181f50a32c576e3138bc35.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.087baa23f1809181f64072f3083b00b97a976199b774bc640c81eecc354a15d8.wasm", + "tx_init_validator.wasm": "tx_init_validator.bfe3679a2064b00f9430cbe2a4461deedf5aa35cab12b3a349e308e39291539f.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.93346dcfc468fe34ac0bf16012c54414f0074c8a7f73aa576d3ee43a3f245d9e.wasm", + "tx_transfer.wasm": "tx_transfer.e498a5a6dba6fc9ffedc0c2fc129ecb3a82ddce1b3b6c27ed290a6e03bc886ac.wasm", + "tx_unbond.wasm": "tx_unbond.4bb03fdf0546a89838bac4b3288974ca1b55ebd113ea51ccf6e046042ca3b99c.wasm", + "tx_update_vp.wasm": "tx_update_vp.381a8c38387af8a53166e85b273ab1bc308cec99978112dbd0e7371672ce8fa5.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.cd7aa2c8fd8d90df9d9923135324184f2e42555028aad68fc45102bfe7498be1.wasm", + "tx_withdraw.wasm": "tx_withdraw.0e7b2aa9c519cc38ac04ea6fd1de0414d4841b961cef40813d8c1de7a63ec065.wasm", + "vp_implicit.wasm": "vp_implicit.01a1df661eca17c8f91d3627e90f91643dbb1d3afc64332614aa99bc73d333a6.wasm", + "vp_masp.wasm": "vp_masp.3e0c111616a4ca978e1a199b01345cbf5797e1bc40c5eafb4a2fc3a7ae8793d8.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.016ca8eaf234d986997e1b6fdccf33f51fe7a041a73e1559681258e1c6bb5101.wasm", + "vp_token.wasm": "vp_token.b90e20815d4464c51aa2d9f535db3d74bd46f50ef3f38fccda6ea95cdb7e78d6.wasm", + "vp_user.wasm": "vp_user.8485662a38411568cacb19276282b7c912912b8cfb07c106c44983be5faa5693.wasm", + "vp_validator.wasm": "vp_validator.30e6429a17abbc658ee94a5c36f5ee9f95b9433003b4adbfafcebd6c505b9e02.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 0c2f1f8e5e..18cef5a25a 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -985,7 +985,7 @@ mod tests { // Initialize a tx environment let mut tx_env = TestTxEnv::default(); - let keypairs: Vec = (0..signers_total).map(|_| { + let keypairs: Vec = (1..signers_total).map(|_| { common::SecretKey::try_from_sk(&key::testing::gen_keypair::()).unwrap() }).collect(); From 2cec2476a8119590e0050d8b2bcf415413380ceb Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Fri, 3 Feb 2023 16:14:34 +0100 Subject: [PATCH 11/27] wip --- apps/src/bin/namada-client/cli.rs | 4 +- apps/src/lib/cli.rs | 43 +++++++------- apps/src/lib/client/rpc.rs | 7 ++- apps/src/lib/client/signing.rs | 31 +++++----- apps/src/lib/client/tx.rs | 89 ++++++++++++++++++++--------- apps/src/lib/client/types.rs | 5 +- apps/src/lib/client/utils.rs | 1 - core/src/proto/mod.rs | 2 +- core/src/proto/types.rs | 23 ++++---- core/src/types/time.rs | 4 +- core/src/types/transaction/mod.rs | 14 +++-- wasm/wasm_source/src/vp_implicit.rs | 7 ++- 12 files changed, 134 insertions(+), 96 deletions(-) diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index fe5ea9acc1..534a316579 100644 --- a/apps/src/bin/namada-client/cli.rs +++ b/apps/src/bin/namada-client/cli.rs @@ -95,9 +95,7 @@ pub async fn main() -> Result<()> { Sub::QueryProtocolParameters(QueryProtocolParameters(args)) => { rpc::query_protocol_parameters(ctx, args).await; } - Sub::SignTx(SignTx(args)) => { - rpc::sign_tx(ctx, args).await - } + Sub::SignTx(SignTx(args)) => rpc::sign_tx(ctx, args).await, } } cli::NamadaClient::WithoutContext(cmd, global_args) => match cmd { diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 40e38aec24..292cfb953c 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -314,7 +314,7 @@ pub mod cmds { QueryProposal(QueryProposal), QueryProposalResult(QueryProposalResult), QueryProtocolParameters(QueryProtocolParameters), - SignTx(SignTx) + SignTx(SignTx), } #[allow(clippy::large_enum_variant)] @@ -1612,6 +1612,7 @@ pub mod args { use crate::facade::tendermint_config::net::Address as TendermintAddress; const ADDRESS: Arg = arg("address"); + const ADDRESS_OPT: ArgOpt = ADDRESS.opt(); const ALIAS_OPT: ArgOpt = ALIAS.opt(); const ALIAS: Arg = arg("alias"); const ALLOW_DUPLICATE_IP: ArgFlag = flag("allow-duplicate-ip"); @@ -1862,7 +1863,7 @@ pub mod args { /// Optional timestamp field pub timestamp: Option, /// The address - pub address: WalletAddress, + pub address: Option, } impl Args for TxCustom { @@ -1871,7 +1872,7 @@ pub mod args { let code_path = CODE_PATH.parse(matches); let data_path = DATA_PATH_OPT.parse(matches); let timestamp = TX_TIMESTAMP.parse(matches); - let address = ADDRESS.parse(matches); + let address = ADDRESS_OPT.parse(matches); Self { tx, code_path, @@ -1898,7 +1899,7 @@ pub mod args { "The timestamp to set be set on the transaction.", ), ) - .arg(ADDRESS.def().about("The address to lookup.")) + .arg(ADDRESS_OPT.def().about("The address to lookup.")) } } @@ -1923,18 +1924,18 @@ pub mod args { fn def(app: App) -> App { app.add_args::() - .arg( - DATA_PATH_OPT - .def() - .about("Path to hex encoeded signing tx file.") - .conflicts_with(SIGNING_TX.name), - ) - .arg( - SIGNING_TX - .def() - .about("The hex encoded transaction to be signed.") - .conflicts_with(DATA_PATH_OPT.name), - ) + .arg( + DATA_PATH_OPT + .def() + .about("Path to hex encoeded signing tx file.") + .conflicts_with(SIGNING_TX.name), + ) + .arg( + SIGNING_TX + .def() + .about("The hex encoded transaction to be signed.") + .conflicts_with(DATA_PATH_OPT.name), + ) } } @@ -2967,7 +2968,7 @@ pub mod args { /// Sign the tx with the keypair of the public key of the given address pub signers: Vec, /// The paths to signatures - pub signatures: Vec + pub signatures: Vec, } impl Tx { @@ -2995,7 +2996,7 @@ pub mod args { .iter() .map(|signer| ctx.get(signer)) .collect(), - signatures: self.signatures.clone() + signatures: self.signatures.clone(), } } } @@ -3051,9 +3052,7 @@ pub mod args { ) .conflicts_with_all(&[SIGNING_KEYS.name]), ) - .arg(SIGNATURES.def().about( - "The paths to signatures." - )) + .arg(SIGNATURES.def().about("The paths to signatures.")) } fn parse(matches: &ArgMatches) -> Self { @@ -3083,7 +3082,7 @@ pub mod args { signing_keys, signers, dump_tx, - signatures + signatures, } } } diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 310d3f26a1..168e3d927a 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -54,6 +54,7 @@ use namada::types::transaction::{ use namada::types::{address, storage, token}; use tokio::time::{Duration, Instant}; +use super::signing::{tx_signer, OfflineSignature}; use crate::cli::{self, args, safe_exit, Context}; use crate::client::tendermint_rpc_types::TxResponse; use crate::client::tx::{ @@ -67,8 +68,6 @@ use crate::facade::tendermint_rpc::{ Client, HttpClient, Order, SubscriptionClient, WebSocketClient, }; -use super::signing::{tx_signer, OfflineSignature}; - /// Query the status of a given transaction. /// /// If a response is not delivered until `deadline`, we exit the cli with an @@ -1397,7 +1396,9 @@ pub async fn sign_tx( .decode(data.as_bytes()) .expect("SHould be hex decodable."), (Some(path), None) => { - let data = fs::read(path.clone()).await.unwrap_or_else(|_| panic!("File {} should exist.", path.to_string_lossy())); + let data = fs::read(path.clone()).await.unwrap_or_else(|_| { + panic!("File {} should exist.", path.to_string_lossy()) + }); HEXLOWER.decode(&data).expect("SHould be hex decodable.") } (_, _) => { diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index ba8ffd7b96..a98fc29127 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -4,8 +4,7 @@ use std::collections::HashMap; use std::fs; -use borsh::{BorshSerialize}; - +use borsh::BorshSerialize; use namada::ledger::parameters::storage as parameter_storage; use namada::proto::Tx; use namada::types::address::{Address, ImplicitAddress}; @@ -15,7 +14,7 @@ use namada::types::storage::Epoch; use namada::types::token; use namada::types::token::Amount; use namada::types::transaction::{hash_tx, Fee, WrapperTx, MIN_FEE}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use super::rpc; use crate::cli::context::{WalletAddress, WalletKeypair}; @@ -28,7 +27,7 @@ use crate::wallet::Wallet; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct OfflineSignature { pub sig: common::Signature, - pub public_key: common::PublicKey + pub public_key: common::PublicKey, } /// Find the public key for the given address and try to load the keypair @@ -227,21 +226,25 @@ pub async fn sign_tx_multisignature( ) -> (Context, TxBroadcastData) { let keypairs = tx_signers(&mut ctx, args, default).await; let tx = match args.signatures.is_empty() { - true => { - tx.sign_multisignature(&keypairs, pks_index_map) - }, + true => tx.sign_multisignature(&keypairs, pks_index_map), false => { - let signatures = args.signatures.iter().map(|signature_path| { - let content = fs::read(signature_path).expect("Signature file should exist."); - let offline_signature: OfflineSignature = serde_json::from_slice(&content).expect("Signature should be deserializable."); - (offline_signature.public_key, offline_signature.sig) - }).collect::>(); + let signatures = args + .signatures + .iter() + .map(|signature_path| { + let content = fs::read(signature_path) + .expect("Signature file should exist."); + let offline_signature: OfflineSignature = + serde_json::from_slice(&content) + .expect("Signature should be deserializable."); + (offline_signature.public_key, offline_signature.sig) + }) + .collect::>(); println!("{:?}", signatures); println!("{:?}", pks_index_map); tx.add_signatures(signatures, pks_index_map) - }, + } }; - let epoch = rpc::query_epoch(args::Query { ledger_address: args.ledger_address.clone(), diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 36a9ebc2e1..fa3d50de58 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1,12 +1,12 @@ use std::borrow::Cow; use std::collections::hash_map::Entry; use std::collections::{BTreeMap, HashMap, HashSet}; -use std::{env, fs}; use std::fmt::Debug; use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; use std::ops::Deref; use std::path::PathBuf; +use std::{env, fs}; use async_std::io::prelude::WriteExt; use async_std::io::{self}; @@ -74,7 +74,7 @@ use crate::cli::{args, safe_exit, Context}; use crate::client::rpc::{query_conversion, query_storage_value}; use crate::client::signing::{find_keypair, tx_signer, TxSigningKey}; use crate::client::tendermint_rpc_types::{TxBroadcastData, TxResponse}; -use crate::client::types::{ParsedTxTransferArgs}; +use crate::client::types::ParsedTxTransferArgs; use crate::facade::tendermint_config::net::Address as TendermintAddress; use crate::facade::tendermint_rpc::endpoint::broadcast::tx_sync::Response; use crate::facade::tendermint_rpc::error::Error as RpcError; @@ -107,13 +107,28 @@ const DEFAULT_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS: u64 = 60; pub async fn submit_custom(ctx: Context, args: args::TxCustom) { let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); - let tx_code = fs::read(args.clone().code_path).expect("Code path file not found."); - let tx_data = std::fs::read(args.clone().data_path.unwrap()).expect("Expected a file at given data path"); + let tx_code = + fs::read(args.clone().code_path).expect("Code path file not found."); + let tx_data = std::fs::read(args.clone().data_path.unwrap()) + .expect("Expected a file at given data path"); let timestamp = args.timestamp.unwrap_or_default(); - // TODO: check if 1 signer and use the address dervied from that signer - // TODO: make args.address optional - let address = ctx.get(&args.address); + let address = match args.address { + Some(address) => ctx.get(&address), + None => { + if args.tx.signers.len() == 1 { + ctx.get(args.tx.signers.first().unwrap()) + } else { + eprintln!( + "Must specify an address if using more than one \ + signer/signing key." + ); + safe_exit(1); + } + } + }; + + // let address = ctx.get(&args.address); let pks_index_map = rpc::get_account_pks(&client, &address).await; let tx = Tx::new_with_timestamp(tx_code, Some(tx_data), timestamp); @@ -201,31 +216,34 @@ pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { .await; } - - - pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { let public_keys: Vec = args .public_keys .iter() - .map(|pk| { - ctx.get_cached(pk) - }) + .map(|pk| ctx.get_cached(pk)) .sorted() .collect(); let threshold = match args.threshold { - Some(threshold) => threshold, + Some(threshold) => { + if threshold > public_keys.len() as u64 { + eprintln!( + "Threshold must be less or equal to the number of pks." + ); + safe_exit(1); + } else { + threshold + } + } None => { - if public_keys.len() == 1 { - 1 + if public_keys.len() as u64 == 1 { + 1u64 } else { eprintln!("Missing threshold for multisignature account."); - safe_exit(1) + safe_exit(1); } } }; - // TODO: check that threshold should be <= pks len let vp_code = args .vp_code_path @@ -2150,7 +2168,8 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { let tx_code = ctx.read_wasm(TX_VOTE_PROPOSAL); let tx = Tx::new(tx_code, Some(data)); - let pks_map = rpc::get_address_pks_map(&client, &voter_address).await; + let pks_map = + rpc::get_address_pks_map(&client, &voter_address).await; process_tx( ctx, @@ -2677,15 +2696,33 @@ async fn process_tx( ) -> (Context, Vec
) { if args.dump_tx { // TODO: use async version of fs - tokio::fs::write("code.tx", tx.clone().code).await.expect("Should be able to write a file to disk."); + tokio::fs::write("code.tx", tx.clone().code) + .await + .expect("Should be able to write a file to disk."); if let Some(ref data) = tx.data { - tokio::fs::write("data.tx", data).await.expect("Should be able to write a file to disk."); + tokio::fs::write("data.tx", data) + .await + .expect("Should be able to write a file to disk."); } - let signing_tx_blob = tx.clone().signing_tx().try_to_vec().expect("Tx should be serializable."); - println!("Transaction code, data and timestmamp have been written to files code.tx and data.tx."); - println!("You can share the following blob with the other signers:\n{}\n", HEXLOWER.encode(&signing_tx_blob)); - println!("You can later submit the tx with:\nnamada client tx --code-path code.tx --data-path data.tx --timestamp {}", tx.timestamp.to_rfc3339()); + let signing_tx_blob = tx + .clone() + .signing_tx() + .try_to_vec() + .expect("Tx should be serializable."); + println!( + "Transaction code, data and timestmamp have been written to files \ + code.tx and data.tx." + ); + println!( + "You can share the following blob with the other signers:\n{}\n", + HEXLOWER.encode(&signing_tx_blob) + ); + println!( + "You can later submit the tx with:\nnamada client tx --code-path \ + code.tx --data-path data.tx --timestamp {}", + tx.timestamp.to_rfc3339() + ); return (ctx, vec![]); } @@ -2920,4 +2957,4 @@ pub async fn submit_tx( ); parsed -} \ No newline at end of file +} diff --git a/apps/src/lib/client/types.rs b/apps/src/lib/client/types.rs index c1062c2fe6..a9a9882abf 100644 --- a/apps/src/lib/client/types.rs +++ b/apps/src/lib/client/types.rs @@ -1,4 +1,3 @@ - use std::path::PathBuf; use async_trait::async_trait; @@ -7,13 +6,11 @@ use masp_primitives::primitives::{Diversifier, Note, ViewingKey}; use masp_primitives::sapling::Node; use masp_primitives::transaction::components::Amount; use namada::types::address::Address; - use namada::types::masp::{TransferSource, TransferTarget}; use namada::types::storage::Epoch; use namada::types::transaction::GasLimit; use namada::types::{key, token}; - use super::rpc; use crate::cli::{args, Context}; use crate::client::tx::Conversions; @@ -47,7 +44,7 @@ pub struct ParsedTxArgs { /// Sign the tx with the keypair of the public key of the given address pub signers: Vec
, /// The path to signatures - pub signatures: Vec + pub signatures: Vec, } #[derive(Clone, Debug)] diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index 623ae4a813..21fed46c11 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -7,7 +7,6 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use borsh::BorshSerialize; - use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression; diff --git a/core/src/proto/mod.rs b/core/src/proto/mod.rs index b99e4d9ed2..b5893655ee 100644 --- a/core/src/proto/mod.rs +++ b/core/src/proto/mod.rs @@ -3,7 +3,7 @@ pub mod generated; mod types; -pub use types::{Dkg, Error, Signed, SignedTxData, Tx, SigningTx}; +pub use types::{Dkg, Error, Signed, SignedTxData, SigningTx, Tx}; #[cfg(test)] mod tests { diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index acaa4498d4..7685cf0b73 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -206,7 +206,7 @@ impl SigningTx { pub fn compute_signature( self, - keypair: &common::SecretKey + keypair: &common::SecretKey, ) -> common::Signature { let to_sign = self.hash(); common::SigScheme::sign(keypair, to_sign) @@ -233,7 +233,7 @@ impl SigningTx { let signed = SignedTxData { data: self.data, - sigs: signatures + sigs: signatures, }; SigningTx { @@ -468,17 +468,18 @@ impl Tx { pub fn add_signatures( self, signatures: Vec<(common::PublicKey, common::Signature)>, - pks_index_map: HashMap + pks_index_map: HashMap, ) -> Self { - let sigs = signatures.iter().filter_map(|(pk, sig)| { - let pk_index = pks_index_map.get(pk); - pk_index.map(|index| { - SignatureIndex { + let sigs = signatures + .iter() + .filter_map(|(pk, sig)| { + let pk_index = pks_index_map.get(pk); + pk_index.map(|index| SignatureIndex { sig: sig.clone(), index: *index, - } + }) }) - }).collect::>(); + .collect::>(); println!("{:?}", sigs); @@ -491,7 +492,9 @@ impl Tx { code_hash: self.clone().code_hash(), data: signed_tx_data.try_to_vec().ok(), timestamp: self.timestamp, - }.expand(self.code).expect("code hashes to unexpected value") + } + .expand(self.code) + .expect("code hashes to unexpected value") } pub fn sign_multisignature( diff --git a/core/src/types/time.rs b/core/src/types/time.rs index 395d4c0a4f..15ccfc73a4 100644 --- a/core/src/types/time.rs +++ b/core/src/types/time.rs @@ -250,9 +250,7 @@ impl From for Rfc3339String { impl Display for DateTimeUtc { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, "{}", self.0 - ) + write!(f, "{}", self.0) } } diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index 5e516d787d..4e85a42fd5 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -306,17 +306,19 @@ pub mod tx_types { { // verify signature and extract signed data TxType::Wrapper(wrapper) => { - let sig = tx_data.get_signature_by_index(0).ok_or_else(|| - TxError::SigError("Unsigned wrappr".to_string()), - )?; + let sig = + tx_data.get_signature_by_index(0).ok_or_else(|| { + TxError::SigError("Unsigned wrappr".to_string()) + })?; wrapper.validate_sig(signed_hash, &sig)?; Ok(TxType::Wrapper(wrapper)) } // verify signature and extract signed data TxType::Protocol(protocol) => { - let sig = tx_data.get_signature_by_index(0).ok_or_else(|| - TxError::SigError("Unsigned protocol".to_string()), - )?; + let sig = + tx_data.get_signature_by_index(0).ok_or_else(|| { + TxError::SigError("Unsigned protocol".to_string()) + })?; protocol.validate_sig(signed_hash, &sig)?; Ok(TxType::Protocol(protocol)) } diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 620a357701..3e9ecc8dae 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -297,9 +297,10 @@ mod tests { vp_host_env::set(vp_env); assert!( - !validate_tx(&CTX, tx_data, addr, keys_changed, - verifiers).unwrap(), "Revealing PK that's already - revealed should be rejected" ); + !validate_tx(&CTX, tx_data, addr, keys_changed, verifiers).unwrap(), + "Revealing PK that's already + revealed should be rejected" + ); } /// Test that a revealed PK that doesn't correspond to the account's address From ee3ad24dfe9d119bfbfaf92bea71ea4022521d14 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Mon, 6 Feb 2023 11:17:29 +0100 Subject: [PATCH 12/27] wip --- apps/src/lib/cli/utils.rs | 1 - apps/src/lib/client/signing.rs | 2 -- apps/src/lib/client/tx.rs | 10 ++++++++-- core/src/proto/types.rs | 2 -- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/src/lib/cli/utils.rs b/apps/src/lib/cli/utils.rs index 01a15879d1..b956126b50 100644 --- a/apps/src/lib/cli/utils.rs +++ b/apps/src/lib/cli/utils.rs @@ -265,7 +265,6 @@ where .values_of(self.name) .unwrap_or_default() .map(|raw| { - println!("a: {}", raw); raw.parse().unwrap_or_else(|e| { eprintln!( "Failed to parse the {} argument. Raw value: {}, \ diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index a98fc29127..11f0d3f393 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -240,8 +240,6 @@ pub async fn sign_tx_multisignature( (offline_signature.public_key, offline_signature.sig) }) .collect::>(); - println!("{:?}", signatures); - println!("{:?}", pks_index_map); tx.add_signatures(signatures, pks_index_map) } }; diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index fa3d50de58..6ad8e0e99a 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1657,8 +1657,14 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { // signer. Also, if the transaction is shielded, redact the amount and token // types by setting the transparent value to 0 and token type to a constant. // This has no side-effect because transaction is to self. - let (default_signer, amount, token) = - if source == masp_addr && target == masp_addr { + let (default_signer, amount, token) = + if args.tx.dump_tx { + ( + TxSigningKey::None, + args.amount, + parsed_args.token.clone(), + ) + } else if source == masp_addr && target == masp_addr { // TODO Refactor me, we shouldn't rely on any specific token here. ( TxSigningKey::SecretKey(masp_tx_key()), diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index 7685cf0b73..fb32050081 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -481,8 +481,6 @@ impl Tx { }) .collect::>(); - println!("{:?}", sigs); - let signed_tx_data = SignedTxData { data: self.data.clone(), sigs, From 3250bd02523bd5f69a815a9a340732704fbdc4ad Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Mon, 6 Feb 2023 11:36:17 +0100 Subject: [PATCH 13/27] wip --- apps/src/lib/client/tx.rs | 49 +++++++++++++---------------- wasm/wasm_source/src/vp_implicit.rs | 4 +-- wasm/wasm_source/src/vp_user.rs | 21 ++++++++----- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 6ad8e0e99a..dd67a51418 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1657,33 +1657,28 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { // signer. Also, if the transaction is shielded, redact the amount and token // types by setting the transparent value to 0 and token type to a constant. // This has no side-effect because transaction is to self. - let (default_signer, amount, token) = - if args.tx.dump_tx { - ( - TxSigningKey::None, - args.amount, - parsed_args.token.clone(), - ) - } else if source == masp_addr && target == masp_addr { - // TODO Refactor me, we shouldn't rely on any specific token here. - ( - TxSigningKey::SecretKey(masp_tx_key()), - 0.into(), - ctx.native_token.clone(), - ) - } else if source == masp_addr { - ( - TxSigningKey::SecretKey(masp_tx_key()), - args.amount, - parsed_args.token.clone(), - ) - } else { - ( - TxSigningKey::WalletAddress(args.source.to_address()), - args.amount, - parsed_args.token.clone(), - ) - }; + let (default_signer, amount, token) = if args.tx.dump_tx { + (TxSigningKey::None, args.amount, parsed_args.token.clone()) + } else if source == masp_addr && target == masp_addr { + // TODO Refactor me, we shouldn't rely on any specific token here. + ( + TxSigningKey::SecretKey(masp_tx_key()), + 0.into(), + ctx.native_token.clone(), + ) + } else if source == masp_addr { + ( + TxSigningKey::SecretKey(masp_tx_key()), + args.amount, + parsed_args.token.clone(), + ) + } else { + ( + TxSigningKey::WalletAddress(args.source.to_address()), + args.amount, + parsed_args.token.clone(), + ) + }; // If our chosen signer is the MASP sentinel key, then our shielded inputs // will need to cover the gas fees. let chosen_signer = tx_signer(&mut ctx, &args.tx, default_signer.clone()) diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 3e9ecc8dae..0d409b880f 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -76,7 +76,7 @@ fn validate_tx( } let mut valid_signatures = 0; for sig_data in &signed_tx_data.sigs { - let pk = key::get(&ctx, &addr, sig_data.index); + let pk = key::get(ctx, &addr, sig_data.index); if let Ok(Some(public_key)) = pk { let signature_result = ctx .verify_tx_signature(&public_key, &sig_data.sig) @@ -89,7 +89,7 @@ fn validate_tx( } } } - return valid_signatures >= threshold; + valid_signatures >= threshold } _ => false, }); diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 18cef5a25a..478e521ed7 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -69,7 +69,10 @@ fn validate_tx( let valid_sig = Lazy::new(|| match &*signed_tx_data { Ok(signed_tx_data) => { - let threshold = key::threshold(ctx, &addr).unwrap().unwrap_or(1); + let threshold = match key::threshold(ctx, &addr) { + Ok(Some(threshold)) => threshold, + _ => 1, + }; if signed_tx_data.total_signatures() < threshold { return false; } @@ -926,7 +929,7 @@ mod tests { // Initialize a tx environment let mut tx_env = TestTxEnv::default(); - let keypairs: Vec = (0..signers_total).map(|_| { + let keypairs: Vec = (0..=signers_total).map(|_| { common::SecretKey::try_from_sk(&key::testing::gen_keypair::()).unwrap() }).collect(); @@ -961,7 +964,7 @@ mod tests { let mut vp_env = vp_host_env::take(); let tx = vp_env.tx.clone(); - let signed_tx = tx.sign_multisignature(&keypairs.choose_multiple(&mut random, threshold as usize).cloned().collect(), pks_index_map); + let signed_tx = tx.sign_multisignature(&keypairs.choose_multiple(&mut random, threshold as usize).cloned().collect::>(), pks_index_map); let tx_data: Vec = signed_tx.data.as_ref().cloned().unwrap(); vp_env.tx = signed_tx; let keys_changed: BTreeSet = vp_env.all_touched_storage_keys(); @@ -979,13 +982,13 @@ mod tests { (vp_owner, storage_key) in arb_account_storage_subspace_key(), // Generate bytes to write. If `None`, delete from the key instead storage_value in any::>>(), - signers_total in (1u64..15) + signers_total in (0u64..15) ) { let mut random = rand::thread_rng(); // Initialize a tx environment let mut tx_env = TestTxEnv::default(); - let keypairs: Vec = (1..signers_total).map(|_| { + let keypairs: Vec = (0..=signers_total).map(|_| { common::SecretKey::try_from_sk(&key::testing::gen_keypair::()).unwrap() }).collect(); @@ -1004,7 +1007,11 @@ mod tests { pks_index_map.insert(pk.to_owned(), index); } - let threshold: u64 = random.gen_range(1..=signers_total); + let threshold: u64 = if signers_total == 0 { + 1 + } else { + random.gen_range(1..=signers_total) + }; tx_env.write_account_threshold(&vp_owner, threshold); // Initialize VP environment from a transaction @@ -1020,7 +1027,7 @@ mod tests { let mut vp_env = vp_host_env::take(); let tx = vp_env.tx.clone(); - let signed_tx = tx.sign_multisignature(&keypairs.choose_multiple(&mut random, (threshold - 1) as usize).cloned().collect(), pks_index_map); + let signed_tx = tx.sign_multisignature(&keypairs.choose_multiple(&mut random, (threshold - 1) as usize).cloned().collect::>(), pks_index_map); let tx_data: Vec = signed_tx.data.as_ref().cloned().unwrap(); vp_env.tx = signed_tx; let keys_changed: BTreeSet = vp_env.all_touched_storage_keys(); From 4b1a671d9c8c5b65f5201c2bb50778ac1d80b0fb Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Mon, 6 Feb 2023 13:49:42 +0100 Subject: [PATCH 14/27] wip --- apps/src/lib/client/rpc.rs | 31 ++---- apps/src/lib/client/tx.rs | 16 ++-- core/src/proto/mod.rs | 4 +- core/src/proto/types.rs | 112 ++++++++++++++-------- core/src/types/time.rs | 1 - core/src/types/transaction/mod.rs | 19 ++-- vp_prelude/src/lib.rs | 30 ++++++ wasm/wasm_source/src/vp_implicit.rs | 23 +---- wasm/wasm_source/src/vp_testnet_faucet.rs | 28 +----- wasm/wasm_source/src/vp_user.rs | 30 +----- wasm/wasm_source/src/vp_validator.rs | 23 +---- 11 files changed, 135 insertions(+), 182 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 168e3d927a..b6584ffda8 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -54,7 +54,7 @@ use namada::types::transaction::{ use namada::types::{address, storage, token}; use tokio::time::{Duration, Instant}; -use super::signing::{tx_signer, OfflineSignature}; +use super::signing::{tx_signers, OfflineSignature}; use crate::cli::{self, args, safe_exit, Context}; use crate::client::tendermint_rpc_types::TxResponse; use crate::client::tx::{ @@ -1409,10 +1409,12 @@ pub async fn sign_tx( let signing_tx = SigningTx::try_from_slice(&data).expect("Tx should be deserialiable."); - let keypair = - tx_signer(&mut ctx, &tx, super::signing::TxSigningKey::None).await; + let keypairs = + tx_signers(&mut ctx, &tx, vec![super::signing::TxSigningKey::None]) + .await; + let keypair = keypairs.get(0).expect("One signer should be provided."); - let signature = signing_tx.compute_signature(&keypair); + let signature = signing_tx.compute_signature(keypair); let public_key = keypair.ref_to(); let offline_signature = OfflineSignature { @@ -1823,7 +1825,7 @@ pub async fn is_delegator_at( ) } -pub async fn get_account_pks( +pub async fn get_address_pks_map( client: &HttpClient, address: &Address, ) -> HashMap { @@ -2617,25 +2619,6 @@ pub async fn get_delegators_delegation( ) } -pub async fn get_address_pks_map( - client: &HttpClient, - address: &Address, -) -> HashMap { - let pk_prefix = pk_prefix_key(address); - let pk_iter = - query_storage_prefix::(client, &pk_prefix).await; - - if let Some(pks) = pk_iter { - let mut pks_map = HashMap::new(); - pks.enumerate().for_each(|(index, (_, pk))| { - pks_map.insert(pk, index as u64); - }); - pks_map - } else { - HashMap::new() - } -} - pub async fn get_governance_parameters(client: &HttpClient) -> GovParams { use namada::types::token::Amount; let key = gov_storage::get_max_proposal_code_size_key(); diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index dd67a51418..745c0af743 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -129,7 +129,7 @@ pub async fn submit_custom(ctx: Context, args: args::TxCustom) { }; // let address = ctx.get(&args.address); - let pks_index_map = rpc::get_account_pks(&client, &address).await; + let pks_index_map = rpc::get_address_pks_map(&client, &address).await; let tx = Tx::new_with_timestamp(tx_code, Some(tx_data), timestamp); @@ -202,7 +202,7 @@ pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { let tx = Tx::new(tx_code, Some(data)); - let pks_map = rpc::get_account_pks(&client, &addr).await; + let pks_map = rpc::get_address_pks_map(&client, &addr).await; let (_ctx, _initialized_accounts) = process_tx( ctx, @@ -1755,7 +1755,7 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { let tx = Tx::new(tx_code, Some(data)); let signing_address = TxSigningKey::WalletAddress(args.source.to_address()); - let pks_map = rpc::get_account_pks(&client, &source).await; + let pks_map = rpc::get_address_pks_map(&client, &source).await; let (_ctx, _initialized_accounts) = process_tx( ctx, @@ -1877,7 +1877,7 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { let tx = Tx::new(tx_code, Some(data)); - let pks_map = rpc::get_account_pks(&client, &source).await; + let pks_map = rpc::get_address_pks_map(&client, &source).await; process_tx( ctx, @@ -2450,7 +2450,7 @@ pub async fn submit_bond(ctx: Context, args: args::Bond) { let tx = Tx::new(tx_code, Some(data)); let default_signer = args.source.unwrap_or(args.validator); - let pks_map = rpc::get_account_pks(&client, bond_source).await; + let pks_map = rpc::get_address_pks_map(&client, bond_source).await; let (_ctx, _initialized_accounts) = process_tx( ctx, @@ -2509,7 +2509,7 @@ pub async fn submit_unbond(ctx: Context, args: args::Unbond) { let tx = Tx::new(tx_code, Some(data)); let default_signer = args.source.unwrap_or(args.validator); - let pks_map = rpc::get_account_pks(&client, &bond_source).await; + let pks_map = rpc::get_address_pks_map(&client, &bond_source).await; let (_ctx, _initialized_accounts) = process_tx( ctx, @@ -2581,7 +2581,7 @@ pub async fn submit_withdraw(ctx: Context, args: args::Withdraw) { let tx = Tx::new(tx_code, Some(data)); let default_signer = args.source.unwrap_or(args.validator); - let pks_map = rpc::get_account_pks(&client, &bond_source).await; + let pks_map = rpc::get_address_pks_map(&client, &bond_source).await; let (_ctx, _initialized_accounts) = process_tx( ctx, @@ -2671,7 +2671,7 @@ pub async fn submit_validator_commission_change( let tx = Tx::new(tx_code, Some(data)); let default_signer = args.validator; - let pks_map = rpc::get_account_pks(&client, &validator).await; + let pks_map = rpc::get_address_pks_map(&client, &validator).await; let (_ctx, _initialized_accounts) = process_tx( ctx, diff --git a/core/src/proto/mod.rs b/core/src/proto/mod.rs index b5893655ee..81258eb66a 100644 --- a/core/src/proto/mod.rs +++ b/core/src/proto/mod.rs @@ -3,7 +3,9 @@ pub mod generated; mod types; -pub use types::{Dkg, Error, Signed, SignedTxData, SigningTx, Tx}; +pub use types::{ + Dkg, Error, SignatureIndex, Signed, SignedTxData, SigningTx, Tx, +}; #[cfg(test)] mod tests { diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index fb32050081..e1997d5517 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -86,6 +86,16 @@ impl SignedTxData { pub fn total_signatures(&self) -> u64 { self.sigs.len() as u64 } + + pub fn from_single_signature( + data: Option>, + signature: common::Signature, + ) -> Self { + Self { + data, + sigs: vec![SignatureIndex::from_single_signature(signature)], + } + } } /// A generic signed data wrapper for Borsh encode-able data. @@ -189,8 +199,7 @@ impl SigningTx { /// Sign a transaction using [`SignedTxData`]. pub fn sign(self, keypair: &common::SecretKey) -> Self { - let to_sign = self.hash(); - let sig = common::SigScheme::sign(keypair, to_sign); + let sig = self.compute_signature(keypair); let signed = SignedTxData { data: self.data, sigs: SignatureIndex::from_single_signature(sig).to_vec(), @@ -205,7 +214,7 @@ impl SigningTx { } pub fn compute_signature( - self, + &self, keypair: &common::SecretKey, ) -> common::Signature { let to_sign = self.hash(); @@ -217,23 +226,13 @@ impl SigningTx { keypairs: &[common::SecretKey], pks_index_map: HashMap, ) -> Self { - let to_sign = self.hash(); - let signatures: Vec = keypairs - .iter() - .filter_map(|key| { - let signature = common::SigScheme::sign(key, to_sign); - let pk = key.ref_to(); - let pk_index = pks_index_map.get(&pk); - pk_index.map(|index| SignatureIndex { - sig: signature, - index: *index, - }) - }) - .collect(); + let signatures = self.compute_signatures(keypairs); + let signature_indexes = + self.compute_signature_indexes(signatures, pks_index_map); let signed = SignedTxData { data: self.data, - sigs: signatures, + sigs: signature_indexes, }; SigningTx { @@ -243,6 +242,57 @@ impl SigningTx { } } + pub fn add_signatures( + self, + signatures: Vec<(common::PublicKey, common::Signature)>, + pks_index_map: HashMap, + ) -> Self { + let signature_indexes = + self.compute_signature_indexes(signatures, pks_index_map); + let signed = SignedTxData { + data: self.data, + sigs: signature_indexes, + }; + + SigningTx { + code_hash: self.code_hash, + data: signed.try_to_vec().ok(), + timestamp: self.timestamp, + } + } + + fn compute_signature_indexes( + &self, + signatures: Vec<(common::PublicKey, common::Signature)>, + pks_index_map: HashMap, + ) -> Vec { + signatures + .iter() + .filter_map(|(public_key, signature)| { + let pk_index = pks_index_map.get(public_key); + pk_index.map(|index| SignatureIndex { + sig: signature.clone(), + index: *index, + }) + }) + .collect::>() + } + + fn compute_signatures( + &self, + keypairs: &[common::SecretKey], + ) -> Vec<(common::PublicKey, common::Signature)> { + let to_sign = self.hash(); + keypairs + .iter() + .map(|key| { + let signature = common::SigScheme::sign(key, to_sign); + let public_key = key.ref_to(); + (public_key, signature) + }) + .collect() + } + /// Verify that the transaction has been signed by the secret key /// counterpart of the given public key. pub fn verify_sig( @@ -470,29 +520,11 @@ impl Tx { signatures: Vec<(common::PublicKey, common::Signature)>, pks_index_map: HashMap, ) -> Self { - let sigs = signatures - .iter() - .filter_map(|(pk, sig)| { - let pk_index = pks_index_map.get(pk); - pk_index.map(|index| SignatureIndex { - sig: sig.clone(), - index: *index, - }) - }) - .collect::>(); - - let signed_tx_data = SignedTxData { - data: self.data.clone(), - sigs, - }; - - SigningTx { - code_hash: self.clone().code_hash(), - data: signed_tx_data.try_to_vec().ok(), - timestamp: self.timestamp, - } - .expand(self.code) - .expect("code hashes to unexpected value") + let code = self.code.clone(); + SigningTx::from(self) + .add_signatures(signatures, pks_index_map) + .expand(code) + .expect("code hashes to unexpected value") } pub fn sign_multisignature( diff --git a/core/src/types/time.rs b/core/src/types/time.rs index 15ccfc73a4..e5769f1868 100644 --- a/core/src/types/time.rs +++ b/core/src/types/time.rs @@ -121,7 +121,6 @@ impl FromStr for DateTimeUtc { type Err = ParseError; fn from_str(s: &str) -> Result { - // DateTime::parse_from_rfc_3339(s) Ok(Self(s.parse::>()?)) } } diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index 4e85a42fd5..fd1cb41744 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -531,17 +531,14 @@ pub mod tx_types { has_valid_pow: false, }; // Invalid signed data - let _ed_sig = - ed25519::Signature::try_from_slice([0u8; 64].as_ref()).unwrap(); - let signed = SignedTxData { - data: Some( - TxType::Decrypted(decrypted) - .try_to_vec() - .expect("Test failed"), - ), - sigs: vec![], /* sig: common::Signature::try_from_sig(&ed_sig). - * unwrap(), */ - }; + let data = TxType::Decrypted(decrypted) + .try_to_vec() + .expect("Test failed"); + let ed_sig = ed25519::Signature::try_from_slice(&data).unwrap(); + let signed = SignedTxData::from_single_signature( + Some(data), + common::Signature::try_from_sig(&ed_sig).unwrap(), + ); // create the tx with signed decrypted data let tx = Tx::new(vec![], Some(signed.try_to_vec().expect("Test failed"))); diff --git a/vp_prelude/src/lib.rs b/vp_prelude/src/lib.rs index 0d0680a2e6..1d5dddc139 100644 --- a/vp_prelude/src/lib.rs +++ b/vp_prelude/src/lib.rs @@ -40,6 +40,36 @@ use namada_vm_env::vp::*; use namada_vm_env::{read_from_buffer, read_key_val_bytes_from_buffer}; pub use sha2::{Digest, Sha256, Sha384, Sha512}; +pub fn verify_signatures( + ctx: &Ctx, + signed_tx_data: &SignedTxData, + addr: &Address, +) -> bool { + let threshold = match key::threshold(ctx, addr) { + Ok(Some(threshold)) => threshold, + _ => 1, + }; + if signed_tx_data.total_signatures() < threshold { + return false; + } + let mut valid_signatures = 0; + for sig_data in &signed_tx_data.sigs { + let pk = key::get(ctx, addr, sig_data.index); + if let Ok(Some(public_key)) = pk { + let signature_result = ctx + .verify_tx_signature(&public_key, &sig_data.sig) + .unwrap_or(false); + if signature_result { + valid_signatures += 1; + } + if valid_signatures >= threshold { + return true; + } + } + } + valid_signatures >= threshold +} + pub fn sha256(bytes: &[u8]) -> Hash { let digest = Sha256::digest(bytes); Hash(*digest.as_ref()) diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 0d409b880f..e3fc60a68a 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -69,28 +69,7 @@ fn validate_tx( Lazy::new(|| SignedTxData::try_from_slice(&tx_data[..])); let valid_sig = Lazy::new(|| match &*signed_tx_data { - Ok(signed_tx_data) => { - let threshold = key::threshold(ctx, &addr).unwrap().unwrap_or(1); - if signed_tx_data.total_signatures() < threshold { - return false; - } - let mut valid_signatures = 0; - for sig_data in &signed_tx_data.sigs { - let pk = key::get(ctx, &addr, sig_data.index); - if let Ok(Some(public_key)) = pk { - let signature_result = ctx - .verify_tx_signature(&public_key, &sig_data.sig) - .unwrap_or(false); - if signature_result { - valid_signatures += 1; - } - if valid_signatures >= threshold { - return true; - } - } - } - valid_signatures >= threshold - } + Ok(signed_tx_data) => verify_signatures(ctx, signed_tx_data, &addr), _ => false, }); diff --git a/wasm/wasm_source/src/vp_testnet_faucet.rs b/wasm/wasm_source/src/vp_testnet_faucet.rs index 6975874fd4..211b1d538f 100644 --- a/wasm/wasm_source/src/vp_testnet_faucet.rs +++ b/wasm/wasm_source/src/vp_testnet_faucet.rs @@ -29,28 +29,7 @@ fn validate_tx( Lazy::new(|| SignedTxData::try_from_slice(&tx_data[..])); let valid_sig = Lazy::new(|| match &*signed_tx_data { - Ok(signed_tx_data) => { - let threshold = key::threshold(ctx, &addr).unwrap().unwrap_or(1); - if signed_tx_data.total_signatures() < threshold { - return false; - } - let mut valid_signatures = 0; - for sig_data in &signed_tx_data.sigs { - let pk = key::get(&ctx, &addr, sig_data.index); - if let Ok(Some(public_key)) = pk { - let signature_result = ctx - .verify_tx_signature(&public_key, &sig_data.sig) - .unwrap_or(false); - if signature_result { - valid_signatures += 1; - } - if valid_signatures >= threshold { - return true; - } - } - } - return valid_signatures >= threshold; - } + Ok(signed_tx_data) => verify_signatures(ctx, signed_tx_data, &addr), _ => false, }); @@ -363,10 +342,7 @@ mod tests { // The signature itself doesn't matter and is not being checked in this // test, it's just used to construct `SignedTxData` let sig = key::common::SigScheme::sign(&target_key, &solution_bytes); - let signed_solution = SignedTxData { - data: Some(solution_bytes), - sigs: Vec::new(), - }; + let signed_solution = SignedTxData::from_single_signature(Some(solution_bytes), sig); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 478e521ed7..e6a6cecf6d 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -68,31 +68,7 @@ fn validate_tx( Lazy::new(|| SignedTxData::try_from_slice(&tx_data[..])); let valid_sig = Lazy::new(|| match &*signed_tx_data { - Ok(signed_tx_data) => { - let threshold = match key::threshold(ctx, &addr) { - Ok(Some(threshold)) => threshold, - _ => 1, - }; - if signed_tx_data.total_signatures() < threshold { - return false; - } - let mut valid_signatures = 0; - for sig_data in &signed_tx_data.sigs { - let pk = key::get(&ctx, &addr, sig_data.index); - if let Ok(Some(public_key)) = pk { - let signature_result = ctx - .verify_tx_signature(&public_key, &sig_data.sig) - .unwrap_or(false); - if signature_result { - valid_signatures += 1; - } - if valid_signatures >= threshold { - return true; - } - } - } - return valid_signatures >= threshold; - } + Ok(signed_tx_data) => verify_signatures(ctx, signed_tx_data, &addr), _ => false, }); @@ -944,7 +920,7 @@ mod tests { let mut pks_index_map = HashMap::new(); for (pk, index) in public_keys.iter().zip(0u64..) { - tx_env.write_public_key(&vp_owner, &pk, index); + tx_env.write_public_key(&vp_owner, pk, index); pks_index_map.insert(pk.to_owned(), index); } @@ -1003,7 +979,7 @@ mod tests { let mut pks_index_map = HashMap::new(); for (pk, index) in public_keys.iter().zip(0u64..) { - tx_env.write_public_key(&vp_owner, &pk, index); + tx_env.write_public_key(&vp_owner, pk, index); pks_index_map.insert(pk.to_owned(), index); } diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index 45e3adf160..4bd6a744bf 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -68,28 +68,7 @@ fn validate_tx( Lazy::new(|| SignedTxData::try_from_slice(&tx_data[..])); let valid_sig = Lazy::new(|| match &*signed_tx_data { - Ok(signed_tx_data) => { - let threshold = key::threshold(ctx, &addr).unwrap().unwrap_or(1); - if signed_tx_data.total_signatures() < threshold { - return false; - } - let mut valid_signatures = 0; - for sig_data in &signed_tx_data.sigs { - let pk = key::get(&ctx, &addr, sig_data.index); - if let Ok(Some(public_key)) = pk { - let signature_result = ctx - .verify_tx_signature(&public_key, &sig_data.sig) - .unwrap_or(false); - if signature_result { - valid_signatures += 1; - } - if valid_signatures >= threshold { - return true; - } - } - } - return valid_signatures >= threshold; - } + Ok(signed_tx_data) => verify_signatures(ctx, signed_tx_data, &addr), _ => false, }); From d257a4617fa1680409c8d6e5d45e5fd67f93c75e Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Mon, 6 Feb 2023 14:48:39 +0100 Subject: [PATCH 15/27] wip --- core/src/types/transaction/mod.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index fd1cb41744..cfebe0755d 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -534,11 +534,10 @@ pub mod tx_types { let data = TxType::Decrypted(decrypted) .try_to_vec() .expect("Test failed"); - let ed_sig = ed25519::Signature::try_from_slice(&data).unwrap(); - let signed = SignedTxData::from_single_signature( - Some(data), - common::Signature::try_from_sig(&ed_sig).unwrap(), - ); + let ed_sig = + ed25519::Signature::try_from_slice([0u8; 64].as_ref()).unwrap(); + let signature = common::Signature::try_from_sig(&ed_sig).unwrap(); + let signed = SignedTxData::from_single_signature(Some(data), signature); // create the tx with signed decrypted data let tx = Tx::new(vec![], Some(signed.try_to_vec().expect("Test failed"))); From 3489a6201527d3438b67fd5259fd1b3b6529103a Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Mon, 6 Feb 2023 16:15:21 +0100 Subject: [PATCH 16/27] wip --- apps/src/lib/cli.rs | 5 +++++ apps/src/lib/client/tx.rs | 19 +++++++------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 292cfb953c..b3fe0c2b6b 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2395,6 +2395,8 @@ pub mod args { pub offline: bool, /// The proposal file path pub proposal_data: Option, + /// The address that will send the vote + pub address: WalletAddress, } impl Args for VoteProposal { @@ -2404,6 +2406,7 @@ pub mod args { let vote = PROPOSAL_VOTE.parse(matches); let offline = PROPOSAL_OFFLINE.parse(matches); let proposal_data = DATA_PATH_OPT.parse(matches); + let address = ADDRESS.parse(matches); Self { tx, @@ -2411,6 +2414,7 @@ pub mod args { vote, offline, proposal_data, + address, } } @@ -2445,6 +2449,7 @@ pub mod args { ) .conflicts_with(PROPOSAL_ID.name), ) + .arg(ADDRESS.def().about("The address to vote with.")) } } diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 745c0af743..31f89ee274 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -408,6 +408,7 @@ pub async fn submit_init_validator( }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); let tx = Tx::new(tx_code, Some(data)); + let (mut ctx, initialized_accounts) = process_tx( ctx, &tx_args, @@ -2026,11 +2027,13 @@ pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { let tx_code = ctx.read_wasm(TX_INIT_PROPOSAL); let tx = Tx::new(tx_code, Some(data)); + let pks_map = rpc::get_address_pks_map(&client, &proposal.author).await; + process_tx( ctx, &args.tx, tx, - HashMap::new(), + pks_map, vec![TxSigningKey::WalletAddress(signer)], #[cfg(not(feature = "mainnet"))] false, @@ -2040,16 +2043,8 @@ pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { } pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { - // TODO: fix me - let signer = if let Some(addr) = args.tx.signers.get(0) { - addr - } else { - eprintln!("Missing mandatory argument --signer."); - safe_exit(1) - }; - if args.offline { - let signer = ctx.get(signer); + let signer = ctx.get(&args.address); let proposal_file_path = args.proposal_data.expect("Proposal file should exist."); let file = File::open(&proposal_file_path).expect("File must exist."); @@ -2105,7 +2100,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { }) .await; - let voter_address = ctx.get(signer); + let voter_address = ctx.get(&args.address); let proposal_id = args.proposal_id.unwrap(); let proposal_start_epoch_key = gov_storage::get_voting_start_epoch_key(proposal_id); @@ -2177,7 +2172,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { &args.tx, tx, pks_map, - vec![TxSigningKey::WalletAddress(signer.clone())], + vec![TxSigningKey::WalletAddress(args.address)], #[cfg(not(feature = "mainnet"))] false, ) From 6433d66992395f2fc46a20d87d67683525518b63 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Mon, 6 Feb 2023 16:23:27 +0100 Subject: [PATCH 17/27] wip --- tests/src/e2e/ledger_tests.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 9437103546..4a4d683979 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2500,7 +2500,7 @@ fn proposal_submission() -> Result<()> { "0", "--vote", "yay", - "--signer", + "--address", "validator-0", "--ledger-address", &validator_one_rpc, @@ -2524,7 +2524,7 @@ fn proposal_submission() -> Result<()> { "0", "--vote", "nay", - "--signer", + "--address", BERTHA, "--ledger-address", &validator_one_rpc, @@ -2544,7 +2544,7 @@ fn proposal_submission() -> Result<()> { "0", "--vote", "yay", - "--signer", + "--address", ALBERT, "--ledger-address", &validator_one_rpc, @@ -2732,7 +2732,7 @@ fn proposal_offline() -> Result<()> { proposal_path.to_str().unwrap(), "--vote", "yay", - "--signer", + "--address", ALBERT, "--offline", "--ledger-address", From 08148544d4acbc4283aecae6cb0bf26631f66af3 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Tue, 7 Feb 2023 14:30:57 +0100 Subject: [PATCH 18/27] wp --- apps/src/lib/cli.rs | 15 +++++---- apps/src/lib/client/tx.rs | 44 +++++++++++++++++++++---- core/src/types/transaction/mod.rs | 4 ++- tx_prelude/src/account.rs | 4 +-- tx_prelude/src/proof_of_stake.rs | 16 +++++---- wasm/wasm_source/src/tx_init_account.rs | 11 ++++++- 6 files changed, 72 insertions(+), 22 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index b3fe0c2b6b..3fde835ed1 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1718,8 +1718,8 @@ pub mod args { const UNSAFE_SHOW_SECRET: ArgFlag = flag("unsafe-show-secret"); const VALIDATOR: Arg = arg("validator"); const VALIDATOR_OPT: ArgOpt = VALIDATOR.opt(); - const VALIDATOR_ACCOUNT_KEY: ArgOpt = - arg_opt("account-key"); + const VALIDATOR_ACCOUNT_KEYS: ArgMulti = + arg_multi("account-keys"); const VALIDATOR_CONSENSUS_KEY: ArgOpt = arg_opt("consensus-key"); const VALIDATOR_CODE_PATH: ArgOpt = arg_opt("validator-code-path"); @@ -2147,12 +2147,13 @@ pub mod args { pub tx: Tx, pub source: WalletAddress, pub scheme: SchemeType, - pub account_key: Option, + pub account_keys: Vec, pub consensus_key: Option, pub protocol_key: Option, pub commission_rate: Decimal, pub max_commission_rate_change: Decimal, pub validator_vp_code_path: Option, + pub threshold: Option, pub unsafe_dont_encrypt: bool, } @@ -2161,24 +2162,26 @@ pub mod args { let tx = Tx::parse(matches); let source = SOURCE.parse(matches); let scheme = SCHEME.parse(matches); - let account_key = VALIDATOR_ACCOUNT_KEY.parse(matches); + let account_keys = VALIDATOR_ACCOUNT_KEYS.parse(matches); let consensus_key = VALIDATOR_CONSENSUS_KEY.parse(matches); let protocol_key = PROTOCOL_KEY.parse(matches); let commission_rate = COMMISSION_RATE.parse(matches); let max_commission_rate_change = MAX_COMMISSION_RATE_CHANGE.parse(matches); let validator_vp_code_path = VALIDATOR_CODE_PATH.parse(matches); + let threshold = THRESHOLD.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); Self { tx, source, scheme, - account_key, + account_keys, consensus_key, protocol_key, commission_rate, max_commission_rate_change, validator_vp_code_path, + threshold, unsafe_dont_encrypt, } } @@ -2192,7 +2195,7 @@ pub mod args { "The key scheme/type used for the validator keys. \ Currently supports ed25519 and secp256k1.", )) - .arg(VALIDATOR_ACCOUNT_KEY.def().about( + .arg(VALIDATOR_ACCOUNT_KEYS.def().about( "A public key for the validator account. A new one will \ be generated if none given.", )) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 31f89ee274..03cd4320e3 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -293,12 +293,13 @@ pub async fn submit_init_validator( tx: tx_args, source, scheme, - account_key, + account_keys, consensus_key, protocol_key, commission_rate, max_commission_rate_change, validator_vp_code_path, + threshold, unsafe_dont_encrypt, }: args::TxInitValidator, ) { @@ -310,17 +311,47 @@ pub async fn submit_init_validator( let validator_key_alias = format!("{}-key", alias); let consensus_key_alias = format!("{}-consensus-key", alias); - let account_key = ctx.get_opt_cached(&account_key).unwrap_or_else(|| { + + let account_keys = if !account_keys.is_empty() { + account_keys + .iter() + .map(|account_key| ctx.get_cached(account_key)) + .collect() + } else { println!("Generating validator account key..."); - ctx.wallet + let public_key = ctx + .wallet .gen_key( scheme, Some(validator_key_alias.clone()), unsafe_dont_encrypt, ) .1 - .ref_to() - }); + .ref_to(); + vec![public_key] + }; + + let threshold = match threshold { + Some(threshold) => { + if threshold > account_keys.len() as u64 { + eprintln!( + "Threshold must be less or equal to the number of public \ + keys." + ); + safe_exit(1); + } else { + threshold + } + } + None => { + if account_keys.len() as u64 == 1 { + 1u64 + } else { + eprintln!("Missing threshold for multisignature account."); + safe_exit(1); + } + } + }; let consensus_key = ctx .get_opt_cached(&consensus_key) @@ -398,12 +429,13 @@ pub async fn submit_init_validator( let tx_code = ctx.read_wasm(TX_INIT_VALIDATOR_WASM); let data = InitValidator { - account_key, + account_keys, consensus_key: consensus_key.ref_to(), protocol_key, dkg_key, commission_rate, max_commission_rate_change, + threshold, validator_vp_code, }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index cfebe0755d..bbdc8c9eb0 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -184,7 +184,7 @@ pub struct InitValidator { /// Public key to be written into the account's storage. This can be used /// for signature verification of transactions for the newly created /// account. - pub account_key: common::PublicKey, + pub account_keys: Vec, /// A key to be used for signing blocks and votes on blocks. pub consensus_key: common::PublicKey, /// Public key used to sign protocol transactions @@ -196,6 +196,8 @@ pub struct InitValidator { /// The maximum change allowed per epoch to the commission rate. This is /// immutable once set here. pub max_commission_rate_change: Decimal, + /// The multisignature threshold + pub threshold: u64, /// The VP code for validator account pub validator_vp_code: Vec, } diff --git a/tx_prelude/src/account.rs b/tx_prelude/src/account.rs index 6d6f188056..4bf08339f8 100644 --- a/tx_prelude/src/account.rs +++ b/tx_prelude/src/account.rs @@ -3,7 +3,7 @@ use namada_core::types::transaction::InitAccount; use super::*; -pub fn init_account(ctx: &mut Ctx, data: InitAccount) -> TxResult { +pub fn init_account(ctx: &mut Ctx, data: InitAccount) -> EnvResult
{ let address = ctx.init_account(&data.vp_code)?; let pk_threshold = key::threshold_key(&address); @@ -14,5 +14,5 @@ pub fn init_account(ctx: &mut Ctx, data: InitAccount) -> TxResult { ctx.write(&pk_key, pk)?; } - Ok(()) + Ok(address) } diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index 26198eafca..c4f1dbd319 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -1,6 +1,7 @@ //! Proof of Stake system integration with functions for transactions -use namada_core::types::transaction::InitValidator; +use namada_core::types::key::common; +use namada_core::types::transaction::{InitAccount, InitValidator}; use namada_core::types::{key, token}; pub use namada_proof_of_stake::parameters::PosParams; use namada_proof_of_stake::{ @@ -66,20 +67,23 @@ impl Ctx { pub fn init_validator( &mut self, InitValidator { - account_key, + account_keys, consensus_key, protocol_key, dkg_key, commission_rate, max_commission_rate_change, + threshold, validator_vp_code, }: InitValidator, ) -> EnvResult
{ let current_epoch = self.get_block_epoch()?; - // Init validator account - let validator_address = self.init_account(&validator_vp_code)?; - let pk_key = key::pk_key(&validator_address, 0); - self.write(&pk_key, &account_key)?; + let account_data = InitAccount { + public_keys: account_keys, + threshold, + vp_code: validator_vp_code, + }; + let validator_address = account::init_account(self, account_data)?; let protocol_pk_key = key::protocol_pk_key(&validator_address); self.write(&protocol_pk_key, &protocol_key)?; let dkg_pk_key = key::dkg_session_keys::dkg_pk_key(&validator_address); diff --git a/wasm/wasm_source/src/tx_init_account.rs b/wasm/wasm_source/src/tx_init_account.rs index 6e797f0156..9019905eb9 100644 --- a/wasm/wasm_source/src/tx_init_account.rs +++ b/wasm/wasm_source/src/tx_init_account.rs @@ -12,5 +12,14 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { .wrap_err("failed to decode InitAccount")?; debug_log!("apply_tx called to init a new established account"); - account::init_account(ctx, tx_data) + match account::init_account(ctx, tx_data) { + Ok(account_address) => { + debug_log!("Created account {}", account_address.encode(),) + } + Err(err) => { + debug_log!("Account creation failed with: {}", err); + panic!() + } + } + Ok(()) } From 4c5b0fe99998c6fae871352754c2105e2099ebd7 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Tue, 7 Feb 2023 15:51:23 +0100 Subject: [PATCH 19/27] wip --- apps/src/lib/cli.rs | 19 ++++++++++--------- apps/src/lib/client/tx.rs | 9 +++++---- wasm/wasm_source/src/tx_init_account.rs | 2 +- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 3fde835ed1..92f965fd32 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1685,8 +1685,7 @@ pub mod args { const PROTOCOL_KEY: ArgOpt = arg_opt("protocol-key"); const PRE_GENESIS_PATH: ArgOpt = arg_opt("pre-genesis-path"); const PUBLIC_KEY: Arg = arg("public-key"); - const PUBLIC_KEY_MULTISIGNATURE: ArgMulti = - arg_multi("public-keys"); + const PUBLIC_KEYS: ArgMulti = arg_multi("public-keys"); const PROPOSAL_ID: Arg = arg("proposal-id"); const PROPOSAL_ID_OPT: ArgOpt = arg_opt("proposal-id"); const PROPOSAL_VOTE: Arg = arg("vote"); @@ -1701,7 +1700,6 @@ pub mod args { const SIGNING_KEYS: ArgMulti = arg_multi("signing-keys"); const SIGNATURES: ArgMulti = arg_multi("signatures"); const SOURCE: Arg = arg("source"); - const SOURCE_MULTISIGNATURE: ArgMulti = arg_multi("sources"); const SOURCE_OPT: ArgOpt = SOURCE.opt(); const STORAGE_KEY: Arg = arg("storage-key"); const SUB_PREFIX: ArgOpt = arg_opt("sub-prefix"); @@ -2085,7 +2083,7 @@ pub mod args { /// Common tx arguments pub tx: Tx, /// Address of the source account - pub sources: Vec, + pub source: Option, /// Path to the VP WASM code file for the new account pub vp_code_path: Option, /// Public key for the new account @@ -2097,13 +2095,13 @@ pub mod args { impl Args for TxInitAccount { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); - let sources = SOURCE_MULTISIGNATURE.parse(matches); + let source = SOURCE_OPT.parse(matches); let vp_code_path = CODE_PATH_OPT.parse(matches); - let public_keys = PUBLIC_KEY_MULTISIGNATURE.parse(matches); + let public_keys = PUBLIC_KEYS.parse(matches); let threshold = THRESHOLD.parse(matches); Self { tx, - sources, + source, vp_code_path, public_keys, threshold, @@ -2113,7 +2111,7 @@ pub mod args { fn def(app: App) -> App { app.add_args::() .arg( - SOURCE_MULTISIGNATURE + SOURCE_OPT .def() .about( "The source account's address that signs the \ @@ -2128,7 +2126,7 @@ pub mod args { specified.", )) .arg( - PUBLIC_KEY_MULTISIGNATURE + PUBLIC_KEYS .def() .about( "A public key to be used for the new account in \ @@ -2223,6 +2221,9 @@ pub mod args { for the validator account. Uses the default validator VP \ if none specified.", )) + .arg(THRESHOLD.def().about( + "Specifiy the treshold if a multisignature account.", + )) .arg(UNSAFE_DONT_ENCRYPT.def().about( "UNSAFE: Do not encrypt the generated keypairs. Do not \ use this for keys used in a live network.", diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 03cd4320e3..f29a90ca80 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -270,16 +270,17 @@ pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { pks_map.insert(pk.clone(), index as u64); } + // TODO: refactor args.source and require either --signer or --signing-keys + // or --signatures + let default_signer = args.source.unwrap(); + let tx = Tx::new(tx_code, Some(data)); let (ctx, initialized_accounts) = process_tx( ctx, &args.tx, tx, pks_map, - args.sources - .iter() - .map(|source| TxSigningKey::WalletAddress(source.clone())) - .collect(), + vec![TxSigningKey::WalletAddress(default_signer)], #[cfg(not(feature = "mainnet"))] false, ) diff --git a/wasm/wasm_source/src/tx_init_account.rs b/wasm/wasm_source/src/tx_init_account.rs index 9019905eb9..4104087ad3 100644 --- a/wasm/wasm_source/src/tx_init_account.rs +++ b/wasm/wasm_source/src/tx_init_account.rs @@ -14,7 +14,7 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { match account::init_account(ctx, tx_data) { Ok(account_address) => { - debug_log!("Created account {}", account_address.encode(),) + debug_log!("Created account {}", account_address.encode()) } Err(err) => { debug_log!("Account creation failed with: {}", err); From 2d9dcc72869439e5fc0f6c27fa0fe86eadc59046 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Tue, 7 Feb 2023 16:00:58 +0100 Subject: [PATCH 20/27] wip --- tests/src/e2e/eth_bridge_tests.rs | 2 +- tests/src/e2e/ibc_tests.rs | 4 ++-- tests/src/e2e/ledger_tests.rs | 28 ++++++++++++++-------------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/src/e2e/eth_bridge_tests.rs b/tests/src/e2e/eth_bridge_tests.rs index fe97731b22..450d8a911c 100644 --- a/tests/src/e2e/eth_bridge_tests.rs +++ b/tests/src/e2e/eth_bridge_tests.rs @@ -56,7 +56,7 @@ fn everything() { let ledger_addr = get_actor_rpc(&test, &SOLE_VALIDATOR); let tx_args = vec![ "tx", - "--signer", + "--signers", ALBERT, "--code-path", &tx_code_path, diff --git a/tests/src/e2e/ibc_tests.rs b/tests/src/e2e/ibc_tests.rs index 8151cdaaca..9fb68ac14e 100644 --- a/tests/src/e2e/ibc_tests.rs +++ b/tests/src/e2e/ibc_tests.rs @@ -1057,7 +1057,7 @@ fn submit_ibc_tx( &code_path, "--data-path", &data_path, - "--signer", + "--signerss", signer, "--gas-amount", "0", @@ -1097,7 +1097,7 @@ fn transfer( sender.as_ref(), "--receiver", &receiver, - "--signer", + "--signers", sender.as_ref(), "--token", token.as_ref(), diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 4a4d683979..5b52e60e1d 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -358,7 +358,7 @@ fn ledger_txs_and_queries() -> Result<()> { // 4. Submit a custom tx vec![ "tx", - "--signer", + "--signers", BERTHA, "--code-path", &tx_no_op, @@ -407,7 +407,7 @@ fn ledger_txs_and_queries() -> Result<()> { "--amount", "10.1", // Faucet withdrawal requires an explicit signer - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc, @@ -596,7 +596,7 @@ fn masp_txs_and_queries() -> Result<()> { ETH, "--amount", "10", - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc, @@ -615,7 +615,7 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "7", - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc, @@ -634,7 +634,7 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "7", - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc, @@ -653,7 +653,7 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "7", - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc, @@ -672,7 +672,7 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "6", - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc, @@ -728,7 +728,7 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "20", - "--signer", + "--signers", BERTHA, "--ledger-address", &validator_one_rpc, @@ -1296,7 +1296,7 @@ fn masp_incentives() -> Result<()> { ETH, "--amount", "30", - "--signer", + "--signers", BERTHA, "--ledger-address", &validator_one_rpc @@ -1388,7 +1388,7 @@ fn masp_incentives() -> Result<()> { BTC, "--amount", "20", - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc @@ -1546,7 +1546,7 @@ fn masp_incentives() -> Result<()> { NAM, "--amount", &((amt30 * masp_rewards[ð()]).0 * (ep5.0 - ep3.0)).to_string(), - "--signer", + "--signers", BERTHA, "--ledger-address", &validator_one_rpc @@ -1573,7 +1573,7 @@ fn masp_incentives() -> Result<()> { NAM, "--amount", &((amt20 * masp_rewards[&btc()]).0 * (ep6.0 - ep0.0)).to_string(), - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc @@ -1688,7 +1688,7 @@ fn invalid_transactions() -> Result<()> { &tx_wasm_path, "--data-path", &tx_data_path, - "--signing-key", + "--signing-keys", &daewon_lower, "--gas-amount", "0", @@ -1737,7 +1737,7 @@ fn invalid_transactions() -> Result<()> { "transfer", "--source", DAEWON, - "--signing-key", + "--signing-keys", &daewon_lower, "--target", ALBERT, From 1c1e68801b5fc14bc564c7df477a338340854c0f Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Tue, 7 Feb 2023 16:57:21 +0100 Subject: [PATCH 21/27] wip --- tests/src/e2e/ibc_tests.rs | 2 +- tests/src/e2e/ledger_tests.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/src/e2e/ibc_tests.rs b/tests/src/e2e/ibc_tests.rs index 9fb68ac14e..eb045715d5 100644 --- a/tests/src/e2e/ibc_tests.rs +++ b/tests/src/e2e/ibc_tests.rs @@ -1057,7 +1057,7 @@ fn submit_ibc_tx( &code_path, "--data-path", &data_path, - "--signerss", + "--signers", signer, "--gas-amount", "0", diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 5b52e60e1d..4618ce895e 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -378,7 +378,7 @@ fn ledger_txs_and_queries() -> Result<()> { "init-account", "--source", BERTHA, - "--public-key", + "--public-keys", // Value obtained from `namada::types::key::ed25519::tests::gen_keypair` "001be519a321e29020fa3cbfbfd01bd5e92db134305609270b71dace25b5a21168", "--code-path", From e2892f7da4a1d2e6616cc6dcc163506e10afd943 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Tue, 7 Feb 2023 17:07:21 +0100 Subject: [PATCH 22/27] wip --- apps/src/lib/cli.rs | 12 ++++++------ apps/src/lib/client/tx.rs | 4 ++-- apps/src/lib/client/types.rs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 92f965fd32..c43107ebbd 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1644,7 +1644,7 @@ pub mod args { const DECRYPT: ArgFlag = flag("decrypt"); const DONT_ARCHIVE: ArgFlag = flag("dont-archive"); const DRY_RUN_TX: ArgFlag = flag("dry-run"); - const DUMP_TX: ArgFlag = flag("dump-tx"); + const OFFLINE_TX: ArgFlag = flag("offline-tx"); const EPOCH: ArgOpt = arg_opt("epoch"); const FORCE: ArgFlag = flag("force"); const DONT_PREFETCH_WASM: ArgFlag = flag("dont-prefetch-wasm"); @@ -2971,7 +2971,7 @@ pub mod args { /// The max amount of gas used to process tx pub gas_limit: GasLimit, /// Dump the signing tx to file - pub dump_tx: bool, + pub offline_tx: bool, /// Sign the tx with the key for the given alias from your wallet pub signing_keys: Vec, /// Sign the tx with the keypair of the public key of the given address @@ -2994,7 +2994,7 @@ pub mod args { fee_amount: self.fee_amount, fee_token: ctx.get(&self.fee_token), gas_limit: self.gas_limit.clone(), - dump_tx: self.dump_tx, + offline_tx: self.offline_tx, signing_keys: self .signing_keys .iter() @@ -3041,7 +3041,7 @@ pub mod args { "The maximum amount of gas needed to run transaction", ), ) - .arg(DUMP_TX.def().about("Dump tx to file.")) + .arg(OFFLINE_TX.def().about("Dump tx to file.")) .arg( SIGNING_KEYS .def() @@ -3074,7 +3074,7 @@ pub mod args { let fee_amount = GAS_AMOUNT.parse(matches); let fee_token = GAS_TOKEN.parse(matches); let gas_limit = GAS_LIMIT.parse(matches).into(); - let dump_tx = DUMP_TX.parse(matches); + let offline_tx = OFFLINE_TX.parse(matches); let signing_keys = SIGNING_KEYS.parse(matches); let signers = SIGNERS.parse(matches); let signatures = SIGNATURES.parse(matches); @@ -3090,7 +3090,7 @@ pub mod args { gas_limit, signing_keys, signers, - dump_tx, + offline_tx, signatures, } } diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index f29a90ca80..acde1c80d2 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1691,7 +1691,7 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { // signer. Also, if the transaction is shielded, redact the amount and token // types by setting the transparent value to 0 and token type to a constant. // This has no side-effect because transaction is to self. - let (default_signer, amount, token) = if args.tx.dump_tx { + let (default_signer, amount, token) = if args.tx.offline_tx { (TxSigningKey::None, args.amount, parsed_args.token.clone()) } else if source == masp_addr && target == masp_addr { // TODO Refactor me, we shouldn't rely on any specific token here. @@ -2723,7 +2723,7 @@ async fn process_tx( default_signers: Vec, #[cfg(not(feature = "mainnet"))] requires_pow: bool, ) -> (Context, Vec
) { - if args.dump_tx { + if args.offline_tx { // TODO: use async version of fs tokio::fs::write("code.tx", tx.clone().code) .await diff --git a/apps/src/lib/client/types.rs b/apps/src/lib/client/types.rs index a9a9882abf..6cd1a143c1 100644 --- a/apps/src/lib/client/types.rs +++ b/apps/src/lib/client/types.rs @@ -38,7 +38,7 @@ pub struct ParsedTxArgs { /// The max amount of gas used to process tx pub gas_limit: GasLimit, /// Dump the signing tx to file - pub dump_tx: bool, + pub offline_tx: bool, /// Sign the tx with the key for the given alias from your wallet pub signing_keys: Vec, /// Sign the tx with the keypair of the public key of the given address From 2793ed05705780d2c8c8a2326b9ce7f2ddf267e9 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Tue, 7 Feb 2023 19:31:07 +0100 Subject: [PATCH 23/27] rebase --- apps/src/lib/cli.rs | 1 + apps/src/lib/client/signing.rs | 4 ++-- tests/src/vm_host_env/tx.rs | 6 ++++-- tx_prelude/src/lib.rs | 1 - tx_prelude/src/proof_of_stake.rs | 2 +- wasm/wasm_source/src/vp_user.rs | 5 ++--- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index c43107ebbd..a1b431d970 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1643,6 +1643,7 @@ pub mod args { const DATA_PATH: Arg = arg("data-path"); const DECRYPT: ArgFlag = flag("decrypt"); const DONT_ARCHIVE: ArgFlag = flag("dont-archive"); + const DUMP_TX: ArgFlag = flag("dump-tx"); const DRY_RUN_TX: ArgFlag = flag("dry-run"); const OFFLINE_TX: ArgFlag = flag("offline-tx"); const EPOCH: ArgOpt = arg_opt("epoch"); diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index 11f0d3f393..595268482b 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -8,7 +8,7 @@ use borsh::BorshSerialize; use namada::ledger::parameters::storage as parameter_storage; use namada::proto::Tx; use namada::types::address::{Address, ImplicitAddress}; -use namada::types::hash::Hash; + use namada::types::key::*; use namada::types::storage::Epoch; use namada::types::token; @@ -244,7 +244,7 @@ pub async fn sign_tx_multisignature( } }; - let epoch = rpc::query_epoch(args::Query { + let epoch = rpc::query_and_print_epoch(args::Query { ledger_address: args.ledger_address.clone(), }) .await; diff --git a/tests/src/vm_host_env/tx.rs b/tests/src/vm_host_env/tx.rs index 940955a814..5f7c57a303 100644 --- a/tests/src/vm_host_env/tx.rs +++ b/tests/src/vm_host_env/tx.rs @@ -189,7 +189,8 @@ impl TestTxEnv { index: u64, ) { let storage_key = key::pk_key(address, index); - self.storage + self.wl_storage + .storage .write(&storage_key, public_key.try_to_vec().unwrap()) .unwrap(); } @@ -201,7 +202,8 @@ impl TestTxEnv { threshold: u64, ) { let storage_key = key::threshold_key(address); - self.storage + self.wl_storage + .storage .write(&storage_key, threshold.try_to_vec().unwrap()) .unwrap(); } diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index 3b93a26c20..df6ca73cc9 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -7,7 +7,6 @@ #![deny(rustdoc::private_intra_doc_links)] pub mod account; -pub mod governance; pub mod ibc; pub mod key; pub mod proof_of_stake; diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index c4f1dbd319..c0ee4047d4 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -1,6 +1,6 @@ //! Proof of Stake system integration with functions for transactions -use namada_core::types::key::common; + use namada_core::types::transaction::{InitAccount, InitValidator}; use namada_core::types::{key, token}; pub use namada_proof_of_stake::parameters::PosParams; diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index e6a6cecf6d..c093aa8761 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -179,12 +179,11 @@ fn validate_tx( #[cfg(test)] mod tests { - use address::testing::arb_non_internal_address; - use namada::ledger::pos::{GenesisValidator, PosParams}; - use namada::types::storage::Epoch; use std::collections::HashMap; use address::testing::arb_non_internal_address; + use namada::ledger::pos::{GenesisValidator, PosParams}; + use namada::types::storage::Epoch; // Use this as `#[test]` annotation to enable logging use namada_tests::log::test; use namada_tests::native_vp::pos::init_pos; From 4b8a33a210d1bcfeabab0e341bef5627d1c25697 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Tue, 7 Feb 2023 19:59:26 +0100 Subject: [PATCH 24/27] fixes --- apps/src/lib/client/signing.rs | 1 - tx_prelude/src/proof_of_stake.rs | 1 - wasm/wasm_source/src/vp_implicit.rs | 2 +- wasm/wasm_source/src/vp_user.rs | 2 +- wasm/wasm_source/src/vp_validator.rs | 2 +- 5 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index 595268482b..54548c029c 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -8,7 +8,6 @@ use borsh::BorshSerialize; use namada::ledger::parameters::storage as parameter_storage; use namada::proto::Tx; use namada::types::address::{Address, ImplicitAddress}; - use namada::types::key::*; use namada::types::storage::Epoch; use namada::types::token; diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index c0ee4047d4..aab4d5d0bf 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -1,6 +1,5 @@ //! Proof of Stake system integration with functions for transactions - use namada_core::types::transaction::{InitAccount, InitValidator}; use namada_core::types::{key, token}; pub use namada_proof_of_stake::parameters::PosParams; diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index e3fc60a68a..b43f8edcd4 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -471,7 +471,7 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index c093aa8761..056f35e731 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -467,7 +467,7 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index 4bd6a744bf..ac286f9b29 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -479,7 +479,7 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { From ac9a448837f8dde9b606ca818c1bd84474799276 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Tue, 7 Feb 2023 21:03:06 +0100 Subject: [PATCH 25/27] update wasm_for_test --- wasm_for_tests/tx_memory_limit.wasm | Bin 133402 -> 133415 bytes wasm_for_tests/tx_mint_tokens.wasm | Bin 352703 -> 360003 bytes wasm_for_tests/tx_no_op.wasm | Bin 25554 -> 25554 bytes wasm_for_tests/tx_proposal_code.wasm | Bin 203731 -> 203147 bytes wasm_for_tests/tx_read_storage_key.wasm | Bin 152406 -> 152461 bytes wasm_for_tests/tx_write.wasm | Bin 163365 -> 167321 bytes wasm_for_tests/vp_always_false.wasm | Bin 156489 -> 156526 bytes wasm_for_tests/vp_always_true.wasm | Bin 156489 -> 156526 bytes wasm_for_tests/vp_eval.wasm | Bin 157426 -> 157463 bytes wasm_for_tests/vp_memory_limit.wasm | Bin 158937 -> 158974 bytes wasm_for_tests/vp_read_storage_key.wasm | Bin 170659 -> 170724 bytes 11 files changed, 0 insertions(+), 0 deletions(-) diff --git a/wasm_for_tests/tx_memory_limit.wasm b/wasm_for_tests/tx_memory_limit.wasm index f8db36d004350c71674a0eeb959783c5f08ed270..4c31ef06dd45b0715396ae5ab5813c6132f794c5 100755 GIT binary patch delta 2093 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 diff --git a/wasm_for_tests/tx_write.wasm b/wasm_for_tests/tx_write.wasm index 761eded647efe78bedc6b45ef695e728cfb7caaf..ed80d2321e5d54b4f78997fb1a6a4b30bb3914ec 100755 GIT binary patch delta 21646 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 delta 17804 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 diff --git a/wasm_for_tests/vp_always_false.wasm b/wasm_for_tests/vp_always_false.wasm index f0693e332dd5d5b6bf5ab8b107b74260f8efed8e..f7dc23ea65edd6551af951fbe2d9003d2b12eeb3 100755 GIT binary patch delta 1753 zcmaJBZERFUaA)_ucklY{+5@52mSW#sffi`FLg^Ly0k^$gd(clyiqTv=pA| zadFN8IB@0yAi80gUZBM|2tkjCqzOF*+(K|oOVbpONIDp<{nyqNl!g}8>Gchb<5JUn zMdO2$CQr%Bub5dpN9fV{yn1d~d3-_hGcB!ci&J)egx!!0gSZ#k===$;ieayXvLzkA z;i+E9Y?jY`K_Q`b0^a4dJh>1<;xqwgSQHuxrl&-8Yw*N5Cb!bzTZAjpx*o%a(xU#p z5mr!mnayo3&4f)CUn_=vMaC%4S?$XaJX!DTed+_D*8?Oumidd*EgR_Nk}M@)i$s{4 zl353w?fx%;^CT|JECkEhl=%sVL1$+6FM>d}U8;V4%?g{xfHgEKjt-oiADL7v)q=gODJQ00<#nH3_S~TX6v>a z*XB2xVO~TGGvS=iZ_;(g)F6rJGpbhx&46LMh@1meWXHW#V?NZ!G zTqzUS6N;)3d=;wWU|{;J0=SLQSr0OKl0MT584H5U*dQ}r3Tl!ydRZ1W7bAu6PH{Qc zt4ETsttxBmEEP91Ajkv?9i_p-fvR4ZhcX(2Z}Ii$a+r-jNBdPq_Ecv+>HX>Icj<-tGzj!-n>$&ym78pE(TSDQ|CW|A)Feip|d-(bX0j3-i?%udraB=QDsg@ zHUVnWs_|CLuP+!$z#p-ky#FTPNo{cx=hdvDOdP8@`ahZ2Soi3QEy>9YZs(QBs11G!L=I{WYZkEn)Lzs(ut0vK~sKK6= zDNup~Ex)O1dvDS3NdN7A_95SS1YcWHufX*s&nfSV+iUaw_S}+ZAR(`-nExAQOYVY% zZBaY-S2ocajj_+iIe9O6!Vd6%KP1E%eEPOog& zp~mrbO$M?jye0&d&Wmf786D8$Njgm%^Ju)roz0uZcp!#v>c}Ho|P^&{kH9K3~#cYS{drrulG_B;mHG= zovz*rj!}$_4noz#G#C+Iehl~bE_Q2FP?km!#{7dP$RfOPuuXwb-%&Mdzwc{PS__ oJD0Y)Pjae`Khm)M)L1`bX+%&9XiuX(op!;F#Ho)Hr{LD$Un7LaD*ylh delta 1673 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 S+|zL9yb&AvI5q^g2mb;(wAN+- delta 1701 zcmZ`(Yiv|S6rMA4_wH`Dw_QR@TP*2xd31w?mX^Mxr90iW+tPLmOIse{(b`DK77HZ; z!Vk6zJ}?@K2TMVNf*~YmK%&*4Uw2yR$6U^;bdUK2O<}^4Oo5Cqc z$=>`_-=xV?a{QGgWwV51Mj_8cn7!V^L**DFZ!DuV%Q}xdwTVA*`{==J|4|w zOr44~TFfI&^%9TzoD%9SE`gM8X@)FAI;69hrzC=hBp^w1R?NLhcPiUdOuLvgqdNa@ zJkV))Vfyh@K}~}0Y%YcN1?u!s9Czde^+m7dHE?j^ zpP^XTh)L$ajzXk1-7B2MU2Z0sq5|-E(^&Z2ZYc|LD8j*6gVxZ#(_|0C|BJ+-z<69x zp3hJ2!)@h7<9L8PrV>p?K#f=6b^D;ka^Y9yA?nlRvI#_gE=$L7MJD!C9Gt*C5~L|b zcQH3e64Dth=1dC3>MV}=v+sGoqtobafo^YNjRlDY7FCv5|1eqtH&%xA@nr1ZtPEG#ZmITqR$v5?f2Sk?8Pv8S|(=2v?O5@;5B1KH5)8{{foDc;DzW3Em={ z8CcGT_TjsMR>HS`47Aa9)nRqjBV1MK!Td@CBf(OV6LBEetV_5RY)*Ls_`=>eusx@` z8B7Hbqn|P_lscAsOZCLLvPFDh=*CeR-5sFtfmH(+vAm=Mp20mO`>{EcG3MW~J*6K| zY*JYbT2rg>Mkr0x(gHlF$@-62y%JwzW$j8rJXCw|e-X3nT_F!VhP_jF-ab`-RE(-{ zA1mf6+YGlF;-{69XxVG=Kx7Kc#PgB6dcsc4zZKuCLkk}AZ};NCg^im1x^SuPN@P(e z=W$m&>Lf(fZ=+p#+-{G09iq}hJMNu{rx(qD0N!2n5R^T#n6^5IrAs=AzP}^}K|6D) zUz=){MTibAI|S9XTK+OPS)+6*)`;iYOUS`HXulh=tD}HYc6VIUDeF7ulX9*z!2p+i zwJT;)@r!!$=&)@)zd#jkTAfGq@ai_GvSH0F1FBIi&$J!uR>aYh3Ef`1K8Gft(SC2k z2&b?&HnqDwMo9XUr(5ynUi^K_6+JH(wl3D!Ufz;wSG>~Y)NkI0Z`!8C>S*DiVeMLr z1`yK+LZh#qUT}H^qXQeC&_@Q^?CV(0T?Wms$6yLq;ENdEIjpbt=gwBmS_Tj52lnA0 zX<<9ED_fU`>vn6WePQ>r^yb@nd$N)+VK~!ajPbJZ7#q&v{xLXobR33{9I?J2V*_LC alAAHN-8KBk!0?HwTl!CY(tiT3ANvc{<<3C> diff --git a/wasm_for_tests/vp_eval.wasm b/wasm_for_tests/vp_eval.wasm index 192b3a93670ada69fe6b66634ec95204a258fb7e..ca12412f9bb546acff670309c6a096cf3a138a7f 100755 GIT binary patch delta 1815 zcmaJ?Yiv|i5I%Fxy?5{KcH5;E`qbWTsa>T|p1aV8-P7)NOIuoAEz%;il!tC(pu}oP zv7D7VB~Ku@+4+h_^r-x?31)F_b*6aZId~QFM=Mv~K#KyRR#EWs+Fc z?B*O8IE4;^Gm`;u0L>*56C6Sa00DwIfIPr3Ob^h<1aR7;R2tVgI5`(CP4jBXNID%1 zN8jC!g84IR7a4W+izg>}C(W9YoA+qG|FP29a~EhvxKc!lic6|%mM&Y~xZ+9Iw)e3Q zvfu&^LIY8aUlX@HZv56c1@G`gk7O3}bFWWGh-RWgM0va#7mKsRY*U+VC?0fG=Ib@3RL|FA^4UY)_vKZhL?FdmJvnUMtGyo%rPv7+{R-CjLGxy zS`TwurrgeBKEr~A9&P~%?Uot!Xmpqn;3)|}p)8=4;uOUz6|y;oWXeR@#nk?P&I7d# zznO75)j^;_yHDhB%b~2HDiuuN@xlrP!Ow*?91IN3E`Xc3Zual#JWhw{(;4%DOnXkI zb^0{P>O3q1JLVuksLw6tW>{T9+~PPCo}9wHa};=Hj-WgUKe`JyUMb%TWtf&2fzRVnnvgX&M*EAdqZwL;(-K# zr6oa<5`g3Ok~($xwUVrQ>OLyMqK5Xm6S&(8%q+&!jS?-4be!6^2d4JGw zYy4fkv+^F_c@*Dns#R>P=_ys;lICds-?h2qIf%(G6srFM>5{u4X1VB@cmsI8c@{i@ zzc$~4GW+%_D!3ejtJ{bkTpfpUJ9CX+slqKwiT1agfC@X*x&s_gMb8JS@Y32K8KA9= zldz#}Hf?#mZA5M9T)&KjOY4(0-I-%rPJ8s3l{!T2z@|K)I#z8iq`0qc{skny)KN(E zR7WF7TibF=g9;3_X4&Qo8^!}AQ6RgsE1za^m3?^Y9Zqh0Ui3LB;&6|I5RUasgOL5m zu0qJFqPl)yF>ln$jaWYEZkN2CCZ|toG&DE9a$Pk@X!m#*^z}@(Pw(F7P=hn0*ET%y zTABh-vt^5iF*jBp=(3IdB^*chMJ8sbdj2$13;MU-ClB{GIyLG_x5gyH4-X6~C^`-{ zD0cDSF*T+)9coZfJ~@=5YQP`gP)0j+c&(tk2aaSX;(?(wEE?YHSvSecY8Yczos2p0 w{7^Ri6MQ{1ZSr@d&mp~!e!28h>#6q0aFUMvY%*RxllF4T*>_XU!dJt80m^#qG5`Po delta 1831 zcmZ`3ZA_I__&n#l_q`u4FT9ij5{|<~<>nWP3Zf|Y0GEq`PzV(Gg)+9ftSOcgr}LWS z$B!D=4aeyGzm-PG0E`k3XK)ij$foz`xGSu@08ZL_hK4q?Wc#o7V2IiS{t-a(4UEgjj7mY6>;fpcKs z6CMzp`56EQFap9(^a>#W1PJB@vH;WcCjlKU0@_88AxRAn2OsB#;TT$}1zs?>JZK+V zFrjLWSzYte&?F~$VoLVNQ8}R(!&9df8D`#OF=@uk;@OoAue@4YS0A``8n-|Obm3vB zCus05#Lu>c>*`YQE>E;13vnkX6hzz6FQS}PW3A{Tqx@%>4qsb>dNL18n=C0D+fB*Ll1MrVRKsB=4=Olm@f+xz@MA!Y&KqSQ%4C6HND zy0(@gmd|~PS?C@}y$HB`*f_Um*eMQOZd1l>K`0^%bOc=?f; zg9S|ue9CB#7-30Fn)-GF-TN8=>3>0SC8Qtyp&IaGV`wD!Fdj-HxFb~U4f^u=1nG0{ zhH5 zexhttX}Ha?BQ_)$*ic+x|4V2K>@Kd>>2DWj%pubd_mMw17Bx@VIvSI3s6=io5$_go zBf(=iz%2C_#Z?6q`5P8fpX?*1>tmYLMBPQoCc(+a`tTy&vlsiq^%UQ=RRic*YAxx< z_lqrDT3SRn0Z)|H45m-VVA+%*PX)fbXCScKR#pS!6+oQ2MssIq;0ubjN}4(R8daf zCA~az)HAmK^?$y?=JMAl!KL#3|C=BaPe)g~)~xkh4_IgAeeopYz3OsYT`_x*bDL^2 zxqG9ggNs{jBWP!oVOO1j7wSg96#S*`zV`jT{^r1^{H*a2|85VSZ>rKLdENru#%1%O zIZs*`P$wa#9t^hd$=d|fW{AlEtqW%gUY|D+%F&ts2$Y+!fEGjAX_u|7bBVdt`kVni zxBvA<4^+5~OGg6L5MP!{Igc#+4N9@6J(u9;?F~@s<}ANyP=chDg9(x-fxDw4hbE@V zJ-6yECzIYaK|e(C%MA)jaOlP{P~xhMxsV>EdTz0pQz5e>GN_Cw0jo{%ns$6|)7N?e zTQ@i92|TxXjaTp2;x5;;Sp_WX0!m0ei$9xuzGpW#ueSGp5ixkH@GXe7jd6K zzriw?!v1X?c(ePEu6^6idX4Vv+^?7U)?LI^yT9(r(wE1?-CF6k?p`9maWC%4OvHV? z8DQYq-Z4CsjOu6#o;i|+{YSRi7m^)T$r!upXUu=>Xp#s2JU;rJ!oCxQeQ>?^Z(y13 A&j0`b diff --git a/wasm_for_tests/vp_memory_limit.wasm b/wasm_for_tests/vp_memory_limit.wasm index 717ceeba4d54d3d231b182d15fc32bbcce4bcc6f..ea17ea9f9e4afb811869958dcee24c03327e9417 100755 GIT binary patch delta 2002 zcmb7FYitx%6u#%)+1c5hZadUMm$t~=DcV)3<+-J$g}t=fZrh~~v5NwQ@)F&Rh8B$q zq3eVAA|{Yy1d*qJQDZc5>I*fX!B;Q@jmBc|g+EBt_=pjS81dW%6G`~vBy-L^a~}7e z^PPL<`0>Es#{-)hD||Wquzo``$n`@GhWZ)n=R-qmbN?7FhBgmWFDMX;`)nbAL4f;! z3uZ9@0UV4VFADiN=Rg8yejpPtO{);-VH^ZG=}*EZzz7fz8ir$NC4k`r)4%!5V0+!< z#N1A^tGj1Rkuz$_9K(#aijvX^S5`%@x_avL`k4)l9jSTO_s+lJ#^5LK@V?mqxk{#F z)@2izWU3;gSd8n{2o6Ipj9`#|EKY@NbkbwWd7Q^3i*m~iDri+?7UhmBXkCsW(aLk} zm=>=P^9qe>Q}f1Plg}P`@eotb5OLDDvFO%|WZ^WG9?Gj+5n_@{uB?otg)<-foJr<+ zorlhN!E-5Z__F7OFGYvuxU_UC*x21t0*`yomVN-7=Wt+bHGSJT_N)*Ly!~Yd0YqtLU9u$79LLZV&sM5&*u3|Uu1xb$nUcv2ri;OjL_x-@^+bZ{`S zeab{QiFZypJ(lNa6JIrBE~xPBDtw1)DApNbWw>D~Qfb4}X7b6+8kHO&NX0ZXv0Rbu zpi(j^2+nEwPZ2@qr8-NI$zzA7=G+6b@$$k19K#oymO>1j=DpPv2QC>0E*0m?=3NwH zeIgNhx5%PPN7qhMoII<S_i0B&!MWyIJk}k=^~v`0uwkg}h#AmBhk2j26#vLy#eAWt>{f z^i76YxV`Tz%=We}rouJh$tBB)w)E$q$$PIqsz3d_^g5z*mOTS4-Ve*}13x6`vO^MY zUr|en=T|J}B#c@)g;xNqTkbe&S>n9dZNE=5;){ zk>+IzM;_i;(MBmzzd1=+nUJosvx;}oY(Snh zo*BxesI8%6a4^J}jca$@<;9+C;8?INaem6{IYm={(YCuUlE2xO4j6PJY=bbzUw16# z(1|@ePwQf|?dsKR_pbf=f6}_US8uj+cSL7_Z|~7cFZ|SddZd57zp@a&@WQxu__5GO zqa4=37&{tZEP&^{@jP0La`@`esbYtPNimk*GJ5s8+$(?iH~n)cdbLB~;DKUnI1s*n N!+}>f9Dv@J{sFmg4PXEO delta 1871 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 delta 6359 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 Q^!?fRmSf{v_^(d Date: Wed, 8 Feb 2023 17:41:49 +0100 Subject: [PATCH 26/27] wip --- apps/src/lib/client/tx.rs | 3 +-- tests/src/e2e/ledger_tests.rs | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index acde1c80d2..c09ac0be87 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -120,8 +120,7 @@ pub async fn submit_custom(ctx: Context, args: args::TxCustom) { ctx.get(args.tx.signers.first().unwrap()) } else { eprintln!( - "Must specify an address if using more than one \ - signer/signing key." + "Must specify and address." ); safe_exit(1); } diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 4618ce895e..bc6f6d340f 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -1690,6 +1690,8 @@ fn invalid_transactions() -> Result<()> { &tx_data_path, "--signing-keys", &daewon_lower, + "--address", + &daewon_lower, "--gas-amount", "0", "--gas-limit", From 9b12d15dd51942fc36893f8a4af941e3582d94d9 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Fri, 10 Feb 2023 11:58:40 +0100 Subject: [PATCH 27/27] fix test --- apps/src/lib/cli/context.rs | 19 ++++++++++++++++- apps/src/lib/client/tx.rs | 12 ++++++----- apps/src/lib/wasm_loader/mod.rs | 13 ------------ wasm/checksums.json | 36 ++++++++++++++++----------------- 4 files changed, 43 insertions(+), 37 deletions(-) diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index e61fda9dfc..d8caf8328e 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -12,6 +12,7 @@ use namada::types::key::*; use namada::types::masp::*; use super::args; +use crate::cli::safe_exit; use crate::client::tx::ShieldedContext; use crate::config::genesis::genesis_config; use crate::config::global::GlobalConfig; @@ -185,7 +186,23 @@ impl Context { /// Read the given WASM file from the WASM directory or an absolute path. pub fn read_wasm(&self, file_name: impl AsRef) -> Vec { - wasm_loader::read_wasm_or_exit(self.wasm_dir(), file_name) + match self.try_read_wasm(&file_name) { + Some(bytes) => bytes, + None => { + println!( + "Unable to read {}.", + file_name.as_ref().to_string_lossy() + ); + safe_exit(1) + } + } + } + + pub fn try_read_wasm( + &self, + file_name: impl AsRef, + ) -> Option> { + wasm_loader::read_wasm(self.wasm_dir(), file_name).ok() } } diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index c09ac0be87..8de7a8d275 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -107,8 +107,12 @@ const DEFAULT_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS: u64 = 60; pub async fn submit_custom(ctx: Context, args: args::TxCustom) { let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); - let tx_code = - fs::read(args.clone().code_path).expect("Code path file not found."); + let tx_code = match ctx.try_read_wasm(&args.code_path) { + Some(bytes) => bytes, + None => { + fs::read(args.clone().code_path).expect("Code path file not found.") + } + }; let tx_data = std::fs::read(args.clone().data_path.unwrap()) .expect("Expected a file at given data path"); let timestamp = args.timestamp.unwrap_or_default(); @@ -119,9 +123,7 @@ pub async fn submit_custom(ctx: Context, args: args::TxCustom) { if args.tx.signers.len() == 1 { ctx.get(args.tx.signers.first().unwrap()) } else { - eprintln!( - "Must specify and address." - ); + eprintln!("Must specify and address."); safe_exit(1); } } diff --git a/apps/src/lib/wasm_loader/mod.rs b/apps/src/lib/wasm_loader/mod.rs index 9a075fbcf8..d8d092a263 100644 --- a/apps/src/lib/wasm_loader/mod.rs +++ b/apps/src/lib/wasm_loader/mod.rs @@ -292,19 +292,6 @@ pub fn read_wasm( )) } -pub fn read_wasm_or_exit( - wasm_directory: impl AsRef, - file_path: impl AsRef, -) -> Vec { - match read_wasm(wasm_directory, file_path) { - Ok(wasm) => wasm, - Err(err) => { - eprintln!("Error reading wasm: {}", err); - safe_exit(1); - } - } -} - async fn download_wasm(url: String) -> Result, Error> { tracing::info!("Downloading WASM {}...", url); let response = reqwest::get(&url).await; diff --git a/wasm/checksums.json b/wasm/checksums.json index 4564f4cfaa..a57216d31a 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,20 +1,20 @@ { - "tx_bond.wasm": "tx_bond.413d1faa4523d0e0f7f439191bd3a79c70ad1e1ae5018ef0a63d1d6782fdee31.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.e9fe65500839ad0b080f8a06fa39b243235f958323846cbcfaaecf8d1dd78f61.wasm", - "tx_ibc.wasm": "tx_ibc.40a6cb9dccc536ddccf9bb8f5ccc647535956a651191dfe2259fafc8c96ee647.wasm", - "tx_init_account.wasm": "tx_init_account.f7c3b99198b6bd7966720c32bea4d74e02612a5660181f50a32c576e3138bc35.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.087baa23f1809181f64072f3083b00b97a976199b774bc640c81eecc354a15d8.wasm", - "tx_init_validator.wasm": "tx_init_validator.bfe3679a2064b00f9430cbe2a4461deedf5aa35cab12b3a349e308e39291539f.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.93346dcfc468fe34ac0bf16012c54414f0074c8a7f73aa576d3ee43a3f245d9e.wasm", - "tx_transfer.wasm": "tx_transfer.e498a5a6dba6fc9ffedc0c2fc129ecb3a82ddce1b3b6c27ed290a6e03bc886ac.wasm", - "tx_unbond.wasm": "tx_unbond.4bb03fdf0546a89838bac4b3288974ca1b55ebd113ea51ccf6e046042ca3b99c.wasm", - "tx_update_vp.wasm": "tx_update_vp.381a8c38387af8a53166e85b273ab1bc308cec99978112dbd0e7371672ce8fa5.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.cd7aa2c8fd8d90df9d9923135324184f2e42555028aad68fc45102bfe7498be1.wasm", - "tx_withdraw.wasm": "tx_withdraw.0e7b2aa9c519cc38ac04ea6fd1de0414d4841b961cef40813d8c1de7a63ec065.wasm", - "vp_implicit.wasm": "vp_implicit.01a1df661eca17c8f91d3627e90f91643dbb1d3afc64332614aa99bc73d333a6.wasm", - "vp_masp.wasm": "vp_masp.3e0c111616a4ca978e1a199b01345cbf5797e1bc40c5eafb4a2fc3a7ae8793d8.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.016ca8eaf234d986997e1b6fdccf33f51fe7a041a73e1559681258e1c6bb5101.wasm", - "vp_token.wasm": "vp_token.b90e20815d4464c51aa2d9f535db3d74bd46f50ef3f38fccda6ea95cdb7e78d6.wasm", - "vp_user.wasm": "vp_user.8485662a38411568cacb19276282b7c912912b8cfb07c106c44983be5faa5693.wasm", - "vp_validator.wasm": "vp_validator.30e6429a17abbc658ee94a5c36f5ee9f95b9433003b4adbfafcebd6c505b9e02.wasm" + "tx_bond.wasm": "tx_bond.e7ce9a96968175c9834f946abd58619b03d50466ccb01c6777144479f314a359.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.627360703b218fe83912d9e4f32b294df09e47e89d04a341ef5a40787ab84195.wasm", + "tx_ibc.wasm": "tx_ibc.81665084a4d1f3f6d0d0bf996e0f5eee88b513da445e606d281b68b35ab88cdd.wasm", + "tx_init_account.wasm": "tx_init_account.69eac03e2aab6bc263c0b9cee71fc40fd17e708cb9211bd32b66f463ae97532a.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.69c91a7edcece48053a345f0ded721dc48857a91d9bfa32c8ba5498a2bcf3f49.wasm", + "tx_init_validator.wasm": "tx_init_validator.422ab5d40d1a268f28d879f8300cd7d3f92bed5da198c3db5bd2449902fdaf78.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.98e7e0084363c1aca40d2a2f2b87f66bd36145b64f249508c64292cadf75ff63.wasm", + "tx_transfer.wasm": "tx_transfer.4f656bc6f4459d065e0dd4ce9ae4aa5278f53cd29adf69d6a2060180e432e164.wasm", + "tx_unbond.wasm": "tx_unbond.c065510900650e995d850c2887a230134d0547494ea4670afcb87c8e701688b5.wasm", + "tx_update_vp.wasm": "tx_update_vp.67ba11b57e8c144886b4355a4d62910b3ec848358637723e9072e47223929c65.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.c5258a6047bc599f78b95a9a4e0147f7ec07b1efdeb403d4b814afa16953013a.wasm", + "tx_withdraw.wasm": "tx_withdraw.ca7549691c43f2ea68b7ad1c4fe208e37995860457a604fbd20f1579d8b54eae.wasm", + "vp_implicit.wasm": "vp_implicit.7c5a53331188da2ff33e1fb5809148115d381ea81c28aaaaf3f5405c530ae099.wasm", + "vp_masp.wasm": "vp_masp.74de81daf2d1ebd219e2969b58ae52d6233794194d7da3d8d07c506dd1fa5767.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.6e2cfc3281956cd39100cdd353b77afd517c61ffc6fe64efa91bdcc13df6a65d.wasm", + "vp_token.wasm": "vp_token.74c4efcc7e9a9258965cc50e4e42ec9c9920ea58ff3fa358573051144b2de6f1.wasm", + "vp_user.wasm": "vp_user.c2a7e556b61c46f5fd6763d970088469d9bf60eff11cadabb57f40911c05aac8.wasm", + "vp_validator.wasm": "vp_validator.5251edeb5e18d2da681eca0caf3766312f6239c238b1da055bb9a3baec51552a.wasm" } \ No newline at end of file

l+&$Sg6MrANb@eU`sHj*nI<2 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 Oll{3RHc#iajsO4|rb*TS delta 248 zcmca~obl3e#tF(wb#)90O!f5)35@lObqTEXAi!A9%*(^V%E-pZ#LmDx(KVfolZ%^` zonzuAZ6*efiT7=}H#8n#a6ADd1lT64Gp3{PXEHjAKLAT9G4nDgFexxOa%3s6DliGK zPX5ejzy^_*{m##ybK-+jE;=n zMMb;}4BV6BINc#uf$Rrby@9h5g>S+YwpoSUhKYlPL4lE*g~4(18n%kbQ@CzU4&~O` JJe}J*0sxSCMNt3% diff --git a/wasm_for_tests/tx_proposal_code.wasm b/wasm_for_tests/tx_proposal_code.wasm index f05e0e95bd17f566696e69f1cd619a2b851a351e..9a241a8b5eb7a8980b44722fafef518d453e3847 100755 GIT binary patch delta 55555 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%0AI&#uAY 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 delta 56085 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