diff --git a/.changelog/unreleased/improvements/1498-separate-signing.md b/.changelog/unreleased/improvements/1498-separate-signing.md new file mode 100644 index 0000000000..506b902672 --- /dev/null +++ b/.changelog/unreleased/improvements/1498-separate-signing.md @@ -0,0 +1,3 @@ +- Separate the transaction building, signing, and submission + actions in the SDKs API to enable hardware wallet usage + ([\#1498](https://github.com/anoma/namada/pull/1498)) \ No newline at end of file diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index 69ca15a89d..9e19841bfb 100644 --- a/apps/src/bin/namada-client/cli.rs +++ b/apps/src/bin/namada-client/cli.rs @@ -92,7 +92,7 @@ pub async fn main() -> Result<()> { .unwrap(); let args = args.to_sdk(&mut ctx); tx::submit_init_validator::(&client, ctx, args) - .await; + .await?; } Sub::TxInitProposal(TxInitProposal(args)) => { wait_until_node_is_synched(&args.tx.ledger_address).await; diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 0f5644a159..a85c784b91 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1966,6 +1966,8 @@ pub mod args { pub const VALIDATOR_CODE_PATH: ArgOpt = arg_opt("validator-code-path"); pub const VALUE: ArgOpt = arg_opt("value"); + pub const VERIFICATION_KEY: ArgOpt = + arg_opt("verification-key"); pub const VIEWING_KEY: Arg = arg("key"); pub const WALLET_ALIAS_FORCE: ArgFlag = flag("wallet-alias-force"); pub const WASM_CHECKSUMS_PATH: Arg = arg("wasm-checksums-path"); @@ -3371,11 +3373,16 @@ pub mod args { fee_token: ctx.get(&self.fee_token), gas_limit: self.gas_limit, signing_key: self.signing_key.map(|x| ctx.get_cached(&x)), + verification_key: self + .verification_key + .map(|x| ctx.get_cached(&x)), signer: self.signer.map(|x| ctx.get(&x)), tx_reveal_code_path: self.tx_reveal_code_path, password: self.password, expiration: self.expiration, - chain_id: self.chain_id, + chain_id: self + .chain_id + .or_else(|| Some(ctx.config.ledger.chain_id.clone())), } } } @@ -3434,7 +3441,8 @@ pub mod args { public key, public key hash or alias from your \ wallet.", ) - .conflicts_with(SIGNER.name), + .conflicts_with(SIGNER.name) + .conflicts_with(VERIFICATION_KEY.name), ) .arg( SIGNER @@ -3443,6 +3451,18 @@ pub mod args { "Sign the transaction with the keypair of the public \ key of the given address.", ) + .conflicts_with(SIGNING_KEY_OPT.name) + .conflicts_with(VERIFICATION_KEY.name), + ) + .arg( + VERIFICATION_KEY + .def() + .help( + "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(SIGNING_KEY_OPT.name), ) .arg(CHAIN_ID_OPT.def().help("The chain ID.")) @@ -3462,6 +3482,7 @@ pub mod args { let gas_limit = GAS_LIMIT.parse(matches).amount.into(); let expiration = EXPIRATION_OPT.parse(matches); let signing_key = SIGNING_KEY_OPT.parse(matches); + let verification_key = VERIFICATION_KEY.parse(matches); let signer = SIGNER.parse(matches); let tx_reveal_code_path = PathBuf::from(TX_REVEAL_PK); let chain_id = CHAIN_ID_OPT.parse(matches); @@ -3479,6 +3500,7 @@ pub mod args { gas_limit, expiration, signing_key, + verification_key, signer, tx_reveal_code_path, password, diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index c44da88f9b..4ae12c6b17 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -14,16 +14,15 @@ use crate::cli::args; /// 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< +pub async fn find_pk< C: namada::ledger::queries::Client + Sync, U: WalletUtils, >( client: &C, wallet: &mut Wallet, addr: &Address, -) -> Result { - namada::ledger::signing::find_keypair::(client, wallet, addr, None) - .await +) -> Result { + namada::ledger::signing::find_pk(client, wallet, addr, None).await } /// Given CLI arguments and some defaults, determine the rightful transaction @@ -38,7 +37,7 @@ pub async fn tx_signer< wallet: &mut Wallet, args: &args::Tx, default: TxSigningKey, -) -> Result { +) -> Result<(Option
, common::PublicKey), tx::Error> { namada::ledger::signing::tx_signer::(client, wallet, args, default) .await } @@ -55,23 +54,12 @@ pub async fn sign_tx< C: namada::ledger::queries::Client + Sync, U: WalletUtils, >( - client: &C, wallet: &mut Wallet, - tx: Tx, + tx: &mut Tx, args: &args::Tx, - default: TxSigningKey, - #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) -> Result { - namada::ledger::signing::sign_tx::( - client, - wallet, - tx, - args, - default, - #[cfg(not(feature = "mainnet"))] - requires_pow, - ) - .await + default: &common::PublicKey, +) -> Result<(), tx::Error> { + namada::ledger::signing::sign_tx(wallet, tx, args, default).await } /// Create a wrapper tx from a normal tx. Get the hash of the diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index f3207d32f8..3fb7d06cf6 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -5,8 +5,6 @@ use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; use std::path::PathBuf; -use async_std::io; -use async_std::io::prelude::WriteExt; use async_trait::async_trait; use borsh::{BorshDeserialize, BorshSerialize}; use data_encoding::HEXLOWER_PERMISSIVE; @@ -15,7 +13,7 @@ use namada::ledger::governance::storage as gov_storage; use namada::ledger::rpc::{TxBroadcastData, TxResponse}; use namada::ledger::signing::TxSigningKey; use namada::ledger::wallet::{Wallet, WalletUtils}; -use namada::ledger::{masp, pos, tx}; +use namada::ledger::{masp, pos, signing, tx}; use namada::proof_of_stake::parameters::PosParams; use namada::proto::{Code, Data, Section, Tx}; use namada::types::address::Address; @@ -33,50 +31,83 @@ use super::rpc; use crate::cli::context::WalletAddress; use crate::cli::{args, safe_exit, Context}; use crate::client::rpc::query_wasm_code_hash; -use crate::client::signing::find_keypair; +use crate::client::signing::find_pk; use crate::client::tx::tx::ProcessTxResponse; use crate::config::TendermintMode; use crate::facade::tendermint_rpc::endpoint::broadcast::tx_sync::Response; use crate::facade::tendermint_rpc::HttpClient; use crate::node::ledger::tendermint_node; -use crate::wallet::{ - gen_validator_keys, read_and_confirm_encryption_password, CliWalletUtils, -}; +use crate::wallet::{gen_validator_keys, read_and_confirm_encryption_password}; + +// Build a transaction to reveal the signer of the given transaction. +pub async fn submit_reveal_aux( + client: &C, + ctx: &mut Context, + args: &args::Tx, + addr: Option
, + pk: common::PublicKey, + tx: &mut Tx, +) -> Result<(), tx::Error> { + if let Some(Address::Implicit(_)) = addr { + let reveal_pk = tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { + tx: args.clone(), + public_key: pk.clone(), + }, + ) + .await?; + if let Some((mut rtx, _, pk)) = reveal_pk { + // Sign the reveal public key transaction with the fee payer + signing::sign_tx(&mut ctx.wallet, &mut rtx, args, &pk).await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, args, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, args, tx, &pk, false).await; + } + } + Ok(()) +} pub async fn submit_custom( client: &C, ctx: &mut Context, - mut args: args::TxCustom, + args: args::TxCustom, ) -> Result<(), tx::Error> { - args.tx.chain_id = args - .tx - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_custom::(client, &mut ctx.wallet, args).await + let (mut tx, addr, pk) = + tx::build_custom(client, &mut ctx.wallet, args.clone()).await?; + submit_reveal_aux(client, ctx, &args.tx, addr, pk.clone(), &mut tx).await?; + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + Ok(()) } pub async fn submit_update_vp( client: &C, ctx: &mut Context, - mut args: args::TxUpdateVp, + args: args::TxUpdateVp, ) -> Result<(), tx::Error> { - args.tx.chain_id = args - .tx - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_update_vp::(client, &mut ctx.wallet, args).await + let (mut tx, addr, pk) = + tx::build_update_vp(client, &mut ctx.wallet, args.clone()).await?; + submit_reveal_aux(client, ctx, &args.tx, addr, pk.clone(), &mut tx).await?; + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + Ok(()) } pub async fn submit_init_account( client: &C, ctx: &mut Context, - mut args: args::TxInitAccount, + args: args::TxInitAccount, ) -> Result<(), tx::Error> { - args.tx.chain_id = args - .tx - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_init_account::(client, &mut ctx.wallet, args).await + let (mut tx, addr, pk) = + tx::build_init_account(client, &mut ctx.wallet, args.clone()).await?; + submit_reveal_aux(client, ctx, &args.tx, addr, pk.clone(), &mut tx).await?; + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + Ok(()) } pub async fn submit_init_validator< @@ -97,14 +128,7 @@ pub async fn submit_init_validator< unsafe_dont_encrypt, tx_code_path: _, }: args::TxInitValidator, -) { - let tx_args = args::Tx { - chain_id: tx_args - .clone() - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())), - ..tx_args.clone() - }; +) -> Result<(), tx::Error> { let alias = tx_args .initialized_account_alias .as_ref() @@ -224,56 +248,33 @@ pub async fn submit_init_validator< tx.set_data(Data::new(data)); tx.set_code(Code::from_hash(tx_code_hash)); - let (mut ctx, result) = process_tx( + let (mut tx, addr, pk) = tx::prepare_tx( client, - ctx, + &mut ctx.wallet, &tx_args, tx, TxSigningKey::WalletAddress(source), #[cfg(not(feature = "mainnet"))] false, ) - .await - .expect("expected process_tx to work"); + .await?; + submit_reveal_aux(client, &mut ctx, &tx_args, addr, pk.clone(), &mut tx) + .await?; + signing::sign_tx(&mut ctx.wallet, &mut tx, &tx_args, &pk).await?; + let result = tx::process_tx(client, &mut ctx.wallet, &tx_args, tx) + .await? + .initialized_accounts(); if !tx_args.dry_run { let (validator_address_alias, validator_address) = match &result[..] { // There should be 1 account for the validator itself [validator_address] => { - let validator_address_alias = match tx_args - .initialized_account_alias - { - Some(alias) => alias, - None => { - print!("Choose an alias for the validator address: "); - io::stdout().flush().await.unwrap(); - let mut alias = String::new(); - io::stdin().read_line(&mut alias).await.unwrap(); - alias.trim().to_owned() - } - }; - let validator_address_alias = - if validator_address_alias.is_empty() { - println!( - "Empty alias given, using {} as the alias.", - validator_address.encode() - ); - validator_address.encode() - } else { - validator_address_alias - }; - if let Some(new_alias) = ctx.wallet.add_address( - validator_address_alias.clone(), - validator_address.clone(), - tx_args.wallet_alias_force, - ) { - println!( - "Added alias {} for address {}.", - new_alias, - validator_address.encode() - ); + if let Some(alias) = ctx.wallet.find_alias(validator_address) { + (alias.clone(), validator_address.clone()) + } else { + eprintln!("Expected one account to be created"); + safe_exit(1) } - (validator_address_alias, validator_address.clone()) } _ => { eprintln!("Expected one account to be created"); @@ -323,8 +324,9 @@ pub async fn submit_init_validator< pos_params.pipeline_len ); } else { - println!("Transaction dry run. No addresses have been saved.") + println!("Transaction dry run. No addresses have been saved."); } + Ok(()) } /// Shielded context file name @@ -440,36 +442,71 @@ impl masp::ShieldedUtils for CLIShieldedUtils { pub async fn submit_transfer( client: &HttpClient, mut ctx: Context, - mut args: args::TxTransfer, + args: args::TxTransfer, ) -> Result<(), tx::Error> { - args.tx.chain_id = args - .tx - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_transfer(client, &mut ctx.wallet, &mut ctx.shielded, args).await + for _ in 0..2 { + let arg = args.clone(); + let (mut tx, addr, pk, tx_epoch, _isf) = + tx::build_transfer(client, &mut ctx.wallet, &mut ctx.shielded, arg) + .await?; + submit_reveal_aux( + client, + &mut ctx, + &args.tx, + addr, + pk.clone(), + &mut tx, + ) + .await?; + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + let result = + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + // Query the epoch in which the transaction was probably submitted + let submission_epoch = rpc::query_and_print_epoch(client).await; + + match result { + ProcessTxResponse::Applied(resp) if + // If a transaction is shielded + tx_epoch.is_some() && + // And it is rejected by a VP + resp.code == 1.to_string() && + // And the its submission epoch doesn't match construction epoch + tx_epoch.unwrap() != submission_epoch => + { + // Then we probably straddled an epoch boundary. Let's retry... + eprintln!( + "MASP transaction rejected and this may be due to the \ + epoch changing. Attempting to resubmit transaction.", + ); + continue; + }, + // Otherwise either the transaction was successful or it will not + // benefit from resubmission + _ => break, + } + } + Ok(()) } pub async fn submit_ibc_transfer( client: &C, mut ctx: Context, - mut args: args::TxIbcTransfer, + args: args::TxIbcTransfer, ) -> Result<(), tx::Error> { - args.tx.chain_id = args - .tx - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_ibc_transfer::(client, &mut ctx.wallet, args).await + let (mut tx, addr, pk) = + tx::build_ibc_transfer(client, &mut ctx.wallet, args.clone()).await?; + submit_reveal_aux(client, &mut ctx, &args.tx, addr, pk.clone(), &mut tx) + .await?; + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + Ok(()) } pub async fn submit_init_proposal( client: &C, mut ctx: Context, - mut args: args::InitProposal, + args: args::InitProposal, ) -> Result<(), tx::Error> { - args.tx.chain_id = args - .tx - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())); let file = File::open(&args.proposal_data).expect("File must exist."); let proposal: Proposal = serde_json::from_reader(file).expect("JSON was not well-formatted"); @@ -534,9 +571,9 @@ pub async fn submit_init_proposal( if args.offline { let signer = ctx.get(&signer); + let key = find_pk(client, &mut ctx.wallet, &signer).await?; let signing_key = - find_keypair::(client, &mut ctx.wallet, &signer) - .await?; + signing::find_key_by_pk(&mut ctx.wallet, &args.tx, &key)?; let offline_proposal = OfflineProposal::new(proposal, signer, &signing_key); let proposal_filename = args @@ -605,17 +642,14 @@ pub async fn submit_init_proposal( let content_sec = tx.add_section(Section::ExtraData(Code::new( init_proposal_content, ))); - let content_sec_hash = Hash( - content_sec.hash(&mut Sha256::new()).finalize_reset().into(), - ); + let content_sec_hash = content_sec.get_hash(); init_proposal_data.content = content_sec_hash; } // Put any proposal code into an extra section if let Some(init_proposal_code) = init_proposal_code { let code_sec = tx .add_section(Section::ExtraData(Code::new(init_proposal_code))); - let code_sec_hash = - Hash(code_sec.hash(&mut Sha256::new()).finalize_reset().into()); + let code_sec_hash = code_sec.get_hash(); init_proposal_data.r#type = ProposalType::Default(Some(code_sec_hash)); } @@ -625,9 +659,9 @@ pub async fn submit_init_proposal( tx.set_data(Data::new(data)); tx.set_code(Code::from_hash(tx_code_hash)); - process_tx::( + let (mut tx, addr, pk) = tx::prepare_tx( client, - ctx, + &mut ctx.wallet, &args.tx, tx, TxSigningKey::WalletAddress(signer), @@ -635,6 +669,17 @@ pub async fn submit_init_proposal( false, ) .await?; + submit_reveal_aux( + client, + &mut ctx, + &args.tx, + addr, + pk.clone(), + &mut tx, + ) + .await?; + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; Ok(()) } } @@ -642,12 +687,8 @@ pub async fn submit_init_proposal( pub async fn submit_vote_proposal( client: &C, mut ctx: Context, - mut args: args::VoteProposal, + args: args::VoteProposal, ) -> Result<(), tx::Error> { - args.tx.chain_id = args - .tx - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())); let signer = if let Some(addr) = &args.tx.signer { addr } else { @@ -733,9 +774,9 @@ pub async fn submit_vote_proposal( safe_exit(1) } + let key = find_pk(client, &mut ctx.wallet, signer).await?; let signing_key = - find_keypair::(client, &mut ctx.wallet, signer) - .await?; + signing::find_key_by_pk(&mut ctx.wallet, &args.tx, &key)?; let offline_vote = OfflineVote::new( &proposal, proposal_vote, @@ -883,9 +924,9 @@ pub async fn submit_vote_proposal( tx.set_data(Data::new(data)); tx.set_code(Code::from_hash(tx_code_hash)); - process_tx::( + let (mut tx, addr, pk) = tx::prepare_tx( client, - ctx, + &mut ctx.wallet, &args.tx, tx, TxSigningKey::WalletAddress(signer.clone()), @@ -893,6 +934,18 @@ pub async fn submit_vote_proposal( false, ) .await?; + submit_reveal_aux( + client, + &mut ctx, + &args.tx, + addr, + pk.clone(), + &mut tx, + ) + .await?; + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk) + .await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; Ok(()) } None => { @@ -909,54 +962,15 @@ pub async fn submit_vote_proposal( pub async fn submit_reveal_pk( client: &C, ctx: &mut Context, - mut args: args::RevealPk, + args: args::RevealPk, ) -> Result<(), tx::Error> { - args.tx.chain_id = args - .tx - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_reveal_pk::(client, &mut ctx.wallet, args).await -} - -pub async fn reveal_pk_if_needed( - client: &C, - ctx: &mut Context, - public_key: &common::PublicKey, - args: &args::Tx, -) -> Result { - let args = args::Tx { - chain_id: args - .clone() - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())), - ..args.clone() - }; - tx::reveal_pk_if_needed::(client, &mut ctx.wallet, public_key, &args) - .await -} - -pub async fn has_revealed_pk( - client: &C, - addr: &Address, -) -> bool { - tx::has_revealed_pk(client, addr).await -} - -pub async fn submit_reveal_pk_aux( - client: &C, - ctx: &mut Context, - public_key: &common::PublicKey, - args: &args::Tx, -) -> Result { - let args = args::Tx { - chain_id: args - .clone() - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())), - ..args.clone() - }; - tx::submit_reveal_pk_aux::(client, &mut ctx.wallet, public_key, &args) - .await + let reveal_tx = + tx::build_reveal_pk(client, &mut ctx.wallet, args.clone()).await?; + if let Some((mut tx, _, pk)) = reveal_tx { + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + } + Ok(()) } /// Check if current epoch is in the last third of the voting period of the @@ -1012,37 +1026,42 @@ async fn filter_delegations( pub async fn submit_bond( client: &C, ctx: &mut Context, - mut args: args::Bond, + args: args::Bond, ) -> Result<(), tx::Error> { - args.tx.chain_id = args - .tx - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_bond::(client, &mut ctx.wallet, args).await + let (mut tx, addr, pk) = + tx::build_bond::(client, &mut ctx.wallet, args.clone()).await?; + submit_reveal_aux(client, ctx, &args.tx, addr, pk.clone(), &mut tx).await?; + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + Ok(()) } pub async fn submit_unbond( client: &C, ctx: &mut Context, - mut args: args::Unbond, + args: args::Unbond, ) -> Result<(), tx::Error> { - args.tx.chain_id = args - .tx - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_unbond::(client, &mut ctx.wallet, args).await + let (mut tx, addr, pk, latest_withdrawal_pre) = + tx::build_unbond(client, &mut ctx.wallet, args.clone()).await?; + submit_reveal_aux(client, ctx, &args.tx, addr, pk.clone(), &mut tx).await?; + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + tx::query_unbonds(client, args.clone(), latest_withdrawal_pre).await?; + Ok(()) } pub async fn submit_withdraw( client: &C, mut ctx: Context, - mut args: args::Withdraw, + args: args::Withdraw, ) -> Result<(), tx::Error> { - args.tx.chain_id = args - .tx - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_withdraw::(client, &mut ctx.wallet, args).await + let (mut tx, addr, pk) = + tx::build_withdraw(client, &mut ctx.wallet, args.clone()).await?; + submit_reveal_aux(client, &mut ctx, &args.tx, addr, pk.clone(), &mut tx) + .await?; + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + Ok(()) } pub async fn submit_validator_commission_change< @@ -1050,18 +1069,17 @@ pub async fn submit_validator_commission_change< >( client: &C, mut ctx: Context, - mut args: args::CommissionRateChange, + args: args::CommissionRateChange, ) -> Result<(), tx::Error> { - args.tx.chain_id = args - .tx - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_validator_commission_change::( - client, - &mut ctx.wallet, - args, - ) - .await + let arg = args.clone(); + let (mut tx, addr, pk) = + tx::build_validator_commission_change(client, &mut ctx.wallet, arg) + .await?; + submit_reveal_aux(client, &mut ctx, &args.tx, addr, pk.clone(), &mut tx) + .await?; + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + Ok(()) } pub async fn submit_unjail_validator< @@ -1069,44 +1087,16 @@ pub async fn submit_unjail_validator< >( client: &C, mut ctx: Context, - mut args: args::TxUnjailValidator, + args: args::TxUnjailValidator, ) -> Result<(), tx::Error> { - args.tx.chain_id = args - .tx - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_unjail_validator::(client, &mut ctx.wallet, args).await -} - -/// 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( - client: &C, - mut ctx: Context, - args: &args::Tx, - tx: Tx, - default_signer: TxSigningKey, - #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) -> Result<(Context, Vec
), tx::Error> { - let args = args::Tx { - chain_id: args - .clone() - .chain_id - .or_else(|| Some(tx.header.chain_id.clone())), - ..args.clone() - }; - let res: Vec
= tx::process_tx::( - client, - &mut ctx.wallet, - &args, - tx, - default_signer, - #[cfg(not(feature = "mainnet"))] - requires_pow, - ) - .await? - .initialized_accounts(); - Ok((ctx, res)) + let (mut tx, addr, pk) = + tx::build_unjail_validator(client, &mut ctx.wallet, args.clone()) + .await?; + submit_reveal_aux(client, &mut ctx, &args.tx, addr, pk.clone(), &mut tx) + .await?; + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + Ok(()) } /// Save accounts initialized from a tx into the wallet, if any. @@ -1190,4 +1180,4 @@ mod test_tx { // MaspDenom::One // assert_eq!(zero.abs(), Uint::zero()); } -} +} \ No newline at end of file diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index e740613d3e..13796ebb88 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -981,7 +981,7 @@ mod test_finalize_block { amount: MIN_FEE.into(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1056,7 +1056,7 @@ mod test_finalize_block { amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1180,7 +1180,7 @@ mod test_finalize_block { amount: MIN_FEE.into(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1217,7 +1217,7 @@ mod test_finalize_block { amount: MIN_FEE.into(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1766,7 +1766,7 @@ mod test_finalize_block { amount: 0.into(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index a5ffcef28f..be1cbc8513 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -1263,7 +1263,7 @@ mod test_utils { amount: Default::default(), token: native_token, }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1331,7 +1331,7 @@ mod test_mempool_validate { amount: 100.into(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1368,7 +1368,7 @@ mod test_mempool_validate { amount: 100.into(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1437,7 +1437,7 @@ mod test_mempool_validate { .expect("This can't fail"), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] diff --git a/apps/src/lib/node/ledger/shell/prepare_proposal.rs b/apps/src/lib/node/ledger/shell/prepare_proposal.rs index 3a91612e44..2cce0e53b9 100644 --- a/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -283,6 +283,7 @@ mod test_prepare_proposal { use namada::ledger::replay_protection; use namada::proof_of_stake::Epoch; use namada::proto::{Code, Data, Header, Section, Signature}; + use namada::types::key::RefTo; use namada::types::transaction::{Fee, WrapperTx}; use super::*; @@ -318,7 +319,7 @@ mod test_prepare_proposal { amount: 0.into(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -359,7 +360,7 @@ mod test_prepare_proposal { amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -428,7 +429,7 @@ mod test_prepare_proposal { amount: 0.into(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -480,7 +481,7 @@ mod test_prepare_proposal { amount: 0.into(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -521,7 +522,7 @@ mod test_prepare_proposal { amount: 0.into(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -574,7 +575,7 @@ mod test_prepare_proposal { amount: 0.into(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -597,7 +598,7 @@ mod test_prepare_proposal { amount: 0.into(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair_2, + keypair_2.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -642,7 +643,7 @@ mod test_prepare_proposal { amount: 0.into(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] diff --git a/apps/src/lib/node/ledger/shell/process_proposal.rs b/apps/src/lib/node/ledger/shell/process_proposal.rs index f81af5bc21..2008abf33c 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -540,7 +540,7 @@ mod test_process_proposal { amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -585,7 +585,7 @@ mod test_process_proposal { amount: Amount::from_uint(100, 0).expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -649,7 +649,7 @@ mod test_process_proposal { amount: Amount::from_uint(1, 0).expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -716,7 +716,7 @@ mod test_process_proposal { amount: Amount::native_whole(1_000_100), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -767,7 +767,7 @@ mod test_process_proposal { amount: i.into(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -826,7 +826,7 @@ mod test_process_proposal { amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -874,7 +874,7 @@ mod test_process_proposal { amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1025,7 +1025,7 @@ mod test_process_proposal { amount: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1100,7 +1100,7 @@ mod test_process_proposal { amount: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1158,7 +1158,7 @@ mod test_process_proposal { amount: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1248,7 +1248,7 @@ mod test_process_proposal { amount: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1271,7 +1271,7 @@ mod test_process_proposal { amount: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair_2, + keypair_2.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1322,7 +1322,7 @@ mod test_process_proposal { amount: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1390,7 +1390,7 @@ mod test_process_proposal { amount: token::Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1455,7 +1455,7 @@ mod test_process_proposal { amount: token::Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1498,7 +1498,7 @@ mod test_process_proposal { amount: token::Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index 099c3a2018..cfeed3ed75 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -323,7 +323,7 @@ mod test_process_tx { amount: 10.into(), token: nam(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -359,7 +359,7 @@ mod test_process_tx { amount: 10.into(), token: nam(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index d1427a0c76..95529e6be6 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -230,7 +230,7 @@ pub mod wrapper_tx { /// transaction pub fn new( fee: Fee, - keypair: &common::SecretKey, + pk: common::PublicKey, epoch: Epoch, gas_limit: GasLimit, #[cfg(not(feature = "mainnet"))] pow_solution: Option< @@ -239,7 +239,7 @@ pub mod wrapper_tx { ) -> WrapperTx { Self { fee, - pk: keypair.ref_to(), + pk, epoch, gas_limit, #[cfg(not(feature = "mainnet"))] @@ -369,7 +369,7 @@ pub mod wrapper_tx { amount: 10.into(), token: nam(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -402,7 +402,7 @@ pub mod wrapper_tx { amount: 10.into(), token: nam(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -437,7 +437,7 @@ pub mod wrapper_tx { amount: Amount::from_uint(10, 0).expect("Test failed"), token: nam(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 4e8cbb560f..d9b28f8ab7 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -53,9 +53,11 @@ use storage::{ get_validator_address_from_bond, into_tm_voting_power, is_bond_key, is_unbond_key, is_validator_slashes_key, last_block_proposer_key, params_key, slashes_prefix, unbonds_for_source_prefix, unbonds_prefix, - validator_address_raw_hash_key, validator_max_commission_rate_change_key, - BondDetails, BondsAndUnbondsDetail, BondsAndUnbondsDetails, - ReverseOrdTokenAmount, RewardsAccumulator, UnbondDetails, + validator_address_raw_hash_key, validator_last_slash_key, + validator_max_commission_rate_change_key, BondDetails, + BondsAndUnbondsDetail, BondsAndUnbondsDetails, EpochedSlashes, + ReverseOrdTokenAmount, RewardsAccumulator, SlashedAmount, UnbondDetails, + ValidatorAddresses, ValidatorUnbondRecords, }; use thiserror::Error; use types::{ @@ -68,11 +70,6 @@ use types::{ WeightedValidator, }; -use crate::storage::{ - validator_last_slash_key, EpochedSlashes, SlashedAmount, - ValidatorAddresses, ValidatorUnbondRecords, -}; - /// Address of the PoS account implemented as a native VP pub const ADDRESS: Address = Address::Internal(InternalAddress::PoS); diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index e05e216db4..7ad213f327 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -436,6 +436,8 @@ pub struct Tx { pub signer: Option, /// Path to the TX WASM code file to reveal PK pub tx_reveal_code_path: PathBuf, + /// Sign the tx with the public key for the given alias from your wallet + pub verification_key: Option, /// Password to decrypt key pub password: Option>, } diff --git a/shared/src/ledger/signing.rs b/shared/src/ledger/signing.rs index 14ca94f7dc..775a2c6b6c 100644 --- a/shared/src/ledger/signing.rs +++ b/shared/src/ledger/signing.rs @@ -15,7 +15,9 @@ use masp_primitives::asset_type::AssetType; use masp_primitives::transaction::components::sapling::fees::{ InputView, OutputView, }; -use namada_core::types::address::{masp, Address, ImplicitAddress}; +use namada_core::types::address::{ + masp, masp_tx_key, Address, ImplicitAddress, +}; use namada_core::types::storage::Key; use namada_core::types::token::{ self, Amount, DenominatedAmount, MaspDenom, TokenAddress, @@ -48,7 +50,6 @@ use crate::types::key::*; use crate::types::masp::{ExtendedViewingKey, PaymentAddress}; use crate::types::storage::Epoch; use crate::types::token::Transfer; -use crate::types::transaction::decrypted::DecryptedTx; use crate::types::transaction::governance::{ InitProposalData, VoteProposalData, }; @@ -67,7 +68,7 @@ const ENV_VAR_TX_LOG_PATH: &str = "NAMADA_TX_LOG_PATH"; /// for it from the wallet. If the keypair is encrypted but a password is not /// supplied, then it is interactively prompted. Errors if the key cannot be /// found or loaded. -pub async fn find_keypair< +pub async fn find_pk< C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( @@ -75,37 +76,31 @@ pub async fn find_keypair< wallet: &mut Wallet, addr: &Address, password: Option>, -) -> Result { +) -> Result { match addr { Address::Established(_) => { println!( "Looking-up public key of {} from the ledger...", addr.encode() ); - let public_key = rpc::get_public_key(client, addr).await.ok_or( - Error::Other(format!( + rpc::get_public_key(client, addr).await.ok_or(Error::Other( + format!( "No public key found for the address {}", addr.encode() - )), - )?; - wallet.find_key_by_pk(&public_key, password).map_err(|err| { - Error::Other(format!( - "Unable to load the keypair from the wallet for public \ - key {}. Failed with: {}", - public_key, err - )) - }) + ), + )) } - Address::Implicit(ImplicitAddress(pkh)) => { - wallet.find_key_by_pkh(pkh, password).map_err(|err| { + Address::Implicit(ImplicitAddress(pkh)) => Ok(wallet + .find_key_by_pkh(pkh, password) + .map_err(|err| { Error::Other(format!( "Unable to load the keypair from the wallet for the \ implicit address {}. Failed with: {}", addr.encode(), err )) - }) - } + })? + .ref_to()), Address::Internal(_) => other_err(format!( "Internal address {} doesn't have any signing keys.", addr @@ -113,18 +108,47 @@ pub async fn find_keypair< } } +/// Load the secret key corresponding to the given public key from the wallet. +/// If the keypair is encrypted but a password is not supplied, then it is +/// interactively prompted. Errors if the key cannot be found or loaded. +pub fn find_key_by_pk( + wallet: &mut Wallet, + args: &args::Tx, + keypair: &common::PublicKey, +) -> Result { + if *keypair == masp_tx_key().ref_to() { + // We already know the secret key corresponding to the MASP sentinal key + Ok(masp_tx_key()) + } else if args + .signing_key + .as_ref() + .map(|x| x.ref_to() == *keypair) + .unwrap_or(false) + { + // We can lookup the secret key from the CLI arguments in this case + Ok(args.signing_key.clone().unwrap()) + } else { + // Otherwise we need to search the wallet for the secret key + wallet + .find_key_by_pk(keypair, args.password.clone()) + .map_err(|err| { + Error::Other(format!( + "Unable to load the keypair from the wallet for public \ + key {}. Failed with: {}", + keypair, err + )) + }) + } +} + /// Carries types that can be directly/indirectly used to sign a transaction. #[allow(clippy::large_enum_variant)] #[derive(Clone)] pub enum TxSigningKey { /// Do not sign any transaction None, - /// Obtain the actual keypair from wallet and use that to sign - WalletKeypair(common::SecretKey), /// Obtain the keypair corresponding to given address from wallet and sign WalletAddress(Address), - /// Directly use the given secret key to sign transactions - SecretKey(common::SecretKey), } /// Given CLI arguments and some defaults, determine the rightful transaction @@ -139,39 +163,32 @@ pub async fn tx_signer< wallet: &mut Wallet, args: &args::Tx, default: TxSigningKey, -) -> Result { - // Override the default signing key source if possible - let default = if let Some(signing_key) = &args.signing_key { - TxSigningKey::WalletKeypair(signing_key.clone()) +) -> Result<(Option
, common::PublicKey), Error> { + let signer = if args.dry_run { + // We cannot override the signer if we're doing a dry run + default + } else if let Some(signing_key) = &args.signing_key { + // Otherwise use the signing key override provided by user + return Ok((None, signing_key.ref_to())); + } else if let Some(verification_key) = &args.verification_key { + return Ok((None, verification_key.clone())); } else if let Some(signer) = &args.signer { + // Otherwise use the signer address provided by user TxSigningKey::WalletAddress(signer.clone()) } else { + // Otherwise use the signer determined by the caller default }; // Now actually fetch the signing key and apply it - match default { - TxSigningKey::WalletKeypair(signing_key) => Ok(signing_key), - TxSigningKey::WalletAddress(signer) => { - let signer = signer; - let signing_key = find_keypair::( - client, - wallet, - &signer, - args.password.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::( - client, wallet, &pk, args, - ) - .await?; - } - Ok(signing_key) + match signer { + TxSigningKey::WalletAddress(signer) if signer == masp() => { + Ok((None, masp_tx_key().ref_to())) } - TxSigningKey::SecretKey(signing_key) => Ok(signing_key), + TxSigningKey::WalletAddress(signer) => Ok(( + Some(signer.clone()), + find_pk::(client, wallet, &signer, args.password.clone()) + .await?, + )), TxSigningKey::None => other_err( "All transactions must be signed; please either specify the key \ or the address from which to look up the signing key." @@ -188,49 +205,173 @@ pub async fn tx_signer< /// 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< - C: crate::ledger::queries::Client + Sync, - U: WalletUtils, ->( - client: &C, +pub async fn sign_tx( wallet: &mut Wallet, - mut tx: Tx, + tx: &mut Tx, args: &args::Tx, - default: TxSigningKey, - #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) -> Result { - let keypair = tx_signer::(client, wallet, args, default).await?; - // Sign over the transaction data and code + keypair: &common::PublicKey, +) -> Result<(), Error> { + let keypair = find_key_by_pk(wallet, args, keypair)?; + // Sign over the transacttion data tx.add_section(Section::Signature(Signature::new( vec![*tx.data_sechash(), *tx.code_sechash()], &keypair, ))); + // Remove all the sensitive sections + tx.protocol_filter(); + // Encrypt all sections not relating to the header + tx.encrypt(&Default::default()); + // Then sign over the bound wrapper + tx.add_section(Section::Signature(Signature::new( + tx.sechashes(), + &keypair, + ))); + Ok(()) +} - let epoch = rpc::query_epoch(client).await; - - let broadcast_data = if args.dry_run { - tx.update_header(TxType::Decrypted(DecryptedTx::Decrypted { - #[cfg(not(feature = "mainnet"))] - // To be able to dry-run testnet faucet withdrawal, pretend - // that we got a valid PoW - has_valid_pow: true, - })); - TxBroadcastData::DryRun(tx) - } else { - sign_wrapper( - client, - wallet, - args, - epoch, - tx, - &keypair, - #[cfg(not(feature = "mainnet"))] - requires_pow, - ) - .await +#[cfg(not(feature = "mainnet"))] +/// Solve the PoW challenge if balance is insufficient to pay transaction fees +/// or if solution is explicitly requested. +pub async fn solve_pow_challenge( + client: &C, + args: &args::Tx, + keypair: &common::PublicKey, + requires_pow: bool, +) -> (Option, Fee) { + let wrapper_tx_fees_key = parameter_storage::get_wrapper_tx_fees_key(); + let fee_amount = rpc::query_storage_value::( + client, + &wrapper_tx_fees_key, + ) + .await + .unwrap_or_default(); + let fee_token = &args.fee_token; + let source = Address::from(keypair); + let balance_key = token::balance_key(fee_token, &source); + let balance = + rpc::query_storage_value::(client, &balance_key) + .await + .unwrap_or_default(); + let is_bal_sufficient = fee_amount <= balance; + if !is_bal_sufficient { + let token_addr = TokenAddress { + address: args.fee_token.clone(), + sub_prefix: None, + }; + let err_msg = format!( + "The wrapper transaction source doesn't have enough balance to \ + pay fee {}, got {}.", + format_denominated_amount(client, &token_addr, fee_amount).await, + format_denominated_amount(client, &token_addr, balance).await, + ); + if !args.force && cfg!(feature = "mainnet") { + panic!("{}", err_msg); + } + } + let fee = Fee { + amount: fee_amount, + token: fee_token.clone(), }; + // A PoW solution can be used to allow zero-fee testnet transactions + // If the address derived from the keypair doesn't have enough balance + // to pay for the fee, allow to find a PoW solution instead. + if requires_pow || !is_bal_sufficient { + println!("The transaction requires the completion of a PoW challenge."); + // Obtain a PoW challenge for faucet withdrawal + let challenge = rpc::get_testnet_pow_challenge(client, source).await; + + // Solve the solution, this blocks until a solution is found + let solution = challenge.solve(); + (Some(solution), fee) + } else { + (None, fee) + } +} + +#[cfg(not(feature = "mainnet"))] +/// Update the PoW challenge inside the given transaction +pub async fn update_pow_challenge( + client: &C, + args: &args::Tx, + tx: &mut Tx, + keypair: &common::PublicKey, + requires_pow: bool, +) { + if let TxType::Wrapper(wrapper) = &mut tx.header.tx_type { + let (pow_solution, fee) = + solve_pow_challenge(client, args, keypair, requires_pow).await; + wrapper.fee = fee; + wrapper.pow_solution = pow_solution; + } +} + +/// 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. +pub async fn wrap_tx< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + #[allow(unused_variables)] wallet: &mut Wallet, + args: &args::Tx, + epoch: Epoch, + mut tx: Tx, + keypair: &common::PublicKey, + #[cfg(not(feature = "mainnet"))] requires_pow: bool, +) -> Tx { + #[cfg(not(feature = "mainnet"))] + let (pow_solution, fee) = + solve_pow_challenge(client, args, keypair, requires_pow).await; + // This object governs how the payload will be processed + tx.update_header(TxType::Wrapper(Box::new(WrapperTx::new( + fee, + keypair.clone(), + epoch, + args.gas_limit.clone(), + #[cfg(not(feature = "mainnet"))] + pow_solution, + )))); + tx.header.chain_id = args.chain_id.clone().unwrap(); + tx.header.expiration = args.expiration; + + #[cfg(feature = "std")] + // Attempt to decode the construction + if let Ok(path) = env::var(ENV_VAR_LEDGER_LOG_PATH) { + let mut tx = tx.clone(); + // Contract the large data blobs in the transaction + tx.wallet_filter(); + // Convert the transaction to Ledger format + let decoding = to_ledger_vector(client, wallet, &tx) + .await + .expect("unable to decode transaction"); + let output = serde_json::to_string(&decoding) + .expect("failed to serialize decoding"); + // Record the transaction at the identified path + let mut f = File::options() + .append(true) + .create(true) + .open(path) + .expect("failed to open test vector file"); + writeln!(f, "{},", output) + .expect("unable to write test vector to file"); + } + #[cfg(feature = "std")] + // Attempt to decode the construction + if let Ok(path) = env::var(ENV_VAR_TX_LOG_PATH) { + let mut tx = tx.clone(); + // Contract the large data blobs in the transaction + tx.wallet_filter(); + // Record the transaction at the identified path + let mut f = File::options() + .append(true) + .create(true) + .open(path) + .expect("failed to open test vector file"); + writeln!(f, "{:x?},", tx).expect("unable to write test vector to file"); + } - Ok(broadcast_data) + tx } /// Create a wrapper tx from a normal tx. Get the hash of the @@ -311,7 +452,7 @@ pub async fn sign_wrapper< amount: fee_amount, token: fee_token.clone(), }, - keypair, + keypair.ref_to(), epoch, args.gas_limit.clone(), #[cfg(not(feature = "mainnet"))] diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index dfae0bb397..894887eeaa 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -4,7 +4,6 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::str::FromStr; use borsh::BorshSerialize; -use itertools::Either::*; use masp_primitives::asset_type::AssetType; use masp_primitives::transaction::builder; use masp_primitives::transaction::builder::Builder; @@ -37,9 +36,9 @@ use crate::ledger::args::{self, InputAmount}; use crate::ledger::governance::storage as gov_storage; use crate::ledger::masp::{ShieldedContext, ShieldedUtils}; use crate::ledger::rpc::{self, validate_amount, TxBroadcastData, TxResponse}; -use crate::ledger::signing::{find_keypair, sign_tx, tx_signer, TxSigningKey}; +use crate::ledger::signing::{tx_signer, wrap_tx, TxSigningKey}; use crate::ledger::wallet::{Wallet, WalletUtils}; -use crate::proto::{Code, Data, MaspBuilder, Section, Signature, Tx}; +use crate::proto::{Code, Data, MaspBuilder, Section, Tx}; use crate::tendermint_rpc::endpoint::broadcast::tx_sync::Response; use crate::tendermint_rpc::error::Error as RpcError; use crate::types::key::*; @@ -158,7 +157,13 @@ pub enum Error { transferred and fees. Amount to transfer is {1} {2} and fees are {3} \ {4}." )] - NegativeBalanceAfterTransfer(Address, String, Address, String, Address), + NegativeBalanceAfterTransfer( + Box
, + String, + Box
, + String, + Box
, + ), /// No Balance found for token #[error("{0}")] MaspError(builder::Error), @@ -219,9 +224,9 @@ impl ProcessTxResponse { } } -/// Submit transaction and wait for result. Returns a list of addresses -/// initialized in the transaction if any. In dry run, this is always empty. -pub async fn process_tx< +/// Prepare a transaction for signing and submission by adding a wrapper header +/// to it. +pub async fn prepare_tx< C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( @@ -231,17 +236,44 @@ pub async fn process_tx< tx: Tx, default_signer: TxSigningKey, #[cfg(not(feature = "mainnet"))] requires_pow: bool, +) -> Result<(Tx, Option
, common::PublicKey), Error> { + let (signer_addr, signer_pk) = + tx_signer::(client, wallet, args, default_signer.clone()).await?; + if args.dry_run { + Ok((tx, signer_addr, signer_pk)) + } else { + let epoch = rpc::query_epoch(client).await; + Ok(( + wrap_tx( + client, + wallet, + args, + epoch, + tx.clone(), + &signer_pk, + #[cfg(not(feature = "mainnet"))] + requires_pow, + ) + .await, + signer_addr, + signer_pk, + )) + } +} + +/// Submit transaction and wait for result. Returns a list of addresses +/// initialized in the transaction if any. In dry run, this is always empty. +pub async fn process_tx< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: &args::Tx, + mut tx: Tx, ) -> Result { - let to_broadcast = sign_tx::( - client, - wallet, - tx, - args, - default_signer, - #[cfg(not(feature = "mainnet"))] - requires_pow, - ) - .await?; + // Remove all the sensitive sections + tx.protocol_filter(); // NOTE: use this to print the request JSON body: // let request = @@ -253,68 +285,86 @@ pub async fn process_tx< // println!("HTTP request body: {}", request_body); if args.dry_run { - expect_dry_broadcast(to_broadcast, client).await + expect_dry_broadcast(TxBroadcastData::DryRun(tx), client).await } else { + // Encrypt all sections not relating to the header + tx.encrypt(&Default::default()); + // We use this to determine when the wrapper tx makes it on-chain + let wrapper_hash = tx.header_hash().to_string(); + // We use this to determine when the decrypted inner tx makes it + // on-chain + let decrypted_hash = tx + .clone() + .update_header(TxType::Raw) + .header_hash() + .to_string(); + let to_broadcast = TxBroadcastData::Wrapper { + tx, + wrapper_hash, + decrypted_hash, + }; // Either broadcast or submit transaction and collect result into // sum type - let result = if args.broadcast_only { - Left(broadcast_tx(client, &to_broadcast).await) + if args.broadcast_only { + broadcast_tx(client, &to_broadcast) + .await + .map(ProcessTxResponse::Broadcast) } else { - Right(submit_tx(client, to_broadcast).await) - }; - // Return result based on executed operation, otherwise deal with - // the encountered errors uniformly - match result { - Right(Ok(result)) => Ok(ProcessTxResponse::Applied(result)), - Left(Ok(result)) => Ok(ProcessTxResponse::Broadcast(result)), - Right(Err(err)) => Err(err), - Left(Err(err)) => Err(err), + match submit_tx(client, to_broadcast).await { + Ok(x) => { + save_initialized_accounts::( + wallet, + args, + x.initialized_accounts.clone(), + ) + .await; + Ok(ProcessTxResponse::Applied(x)) + } + Err(x) => Err(x), + } } } } /// Submit transaction to reveal public key -pub async fn submit_reveal_pk< +pub async fn build_reveal_pk< C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( client: &C, wallet: &mut Wallet, args: args::RevealPk, -) -> Result<(), Error> { +) -> Result, common::PublicKey)>, Error> { let args::RevealPk { tx: args, public_key, } = args; let public_key = public_key; - if !reveal_pk_if_needed::(client, wallet, &public_key, &args).await? { + if !is_reveal_pk_needed::(client, &public_key, &args).await? { let addr: Address = (&public_key).into(); println!("PK for {addr} is already revealed, nothing to do."); - Ok(()) + Ok(None) } else { - Ok(()) + // If not, submit it + Ok(Some( + build_reveal_pk_aux::(client, wallet, &public_key, &args) + .await?, + )) } } /// Submit transaction to rveeal public key if needed -pub async fn reveal_pk_if_needed< +pub async fn is_reveal_pk_needed< C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( client: &C, - wallet: &mut Wallet, public_key: &common::PublicKey, args: &args::Tx, ) -> Result { let addr: Address = public_key.into(); // Check if PK revealed - if args.force || !has_revealed_pk(client, &addr).await { - // If not, submit it - submit_reveal_pk_aux::(client, wallet, public_key, args).await?; - Ok(true) - } else { - Ok(false) - } + Ok(args.force || !has_revealed_pk(client, &addr).await) } /// Check if the public key for the given address has been revealed @@ -326,7 +376,7 @@ pub async fn has_revealed_pk( } /// Submit transaction to reveal the given public key -pub async fn submit_reveal_pk_aux< +pub async fn build_reveal_pk_aux< C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( @@ -334,7 +384,7 @@ pub async fn submit_reveal_pk_aux< wallet: &mut Wallet, public_key: &common::PublicKey, args: &args::Tx, -) -> Result { +) -> Result<(Tx, Option
, common::PublicKey), Error> { let addr: Address = public_key.into(); println!("Submitting a tx to reveal the public key for address {addr}..."); let tx_data = public_key.try_to_vec().map_err(Error::EncodeKeyFailure)?; @@ -352,54 +402,16 @@ pub async fn submit_reveal_pk_aux< tx.set_data(Data::new(tx_data)); tx.set_code(Code::from_hash(tx_code_hash)); - // submit_tx without signing the inner tx - let keypair = if let Some(signing_key) = &args.signing_key { - Ok(signing_key.clone()) - } else if let Some(signer) = args.signer.as_ref() { - find_keypair(client, wallet, signer, args.password.clone()).await - } else { - find_keypair(client, wallet, &addr, args.password.clone()).await - }?; - tx.add_section(Section::Signature(Signature::new( - vec![*tx.data_sechash(), *tx.code_sechash()], - &keypair, - ))); - let epoch = rpc::query_epoch(client).await; - let to_broadcast = if args.dry_run { - TxBroadcastData::DryRun(tx) - } else { - super::signing::sign_wrapper( - client, - wallet, - args, - epoch, - tx, - &keypair, - #[cfg(not(feature = "mainnet"))] - false, - ) - .await - }; - - if args.dry_run { - expect_dry_broadcast(to_broadcast, client).await - } else { - // Either broadcast or submit transaction and collect result into - // sum type - let result = if args.broadcast_only { - Left(broadcast_tx(client, &to_broadcast).await) - } else { - Right(submit_tx(client, to_broadcast).await) - }; - // Return result based on executed operation, otherwise deal with - // the encountered errors uniformly - match result { - Right(Err(err)) => Err(err), - Left(Err(err)) => Err(err), - Right(Ok(response)) => Ok(ProcessTxResponse::Applied(response)), - Left(Ok(response)) => Ok(ProcessTxResponse::Broadcast(response)), - } - } + prepare_tx::( + client, + wallet, + args, + tx, + TxSigningKey::WalletAddress(addr), + #[cfg(not(feature = "mainnet"))] + false, + ) + .await } /// Broadcast a transaction to be included in the blockchain and checks that @@ -587,14 +599,14 @@ pub async fn save_initialized_accounts( } /// Submit validator comission rate change -pub async fn submit_validator_commission_change< +pub async fn build_validator_commission_change< C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( client: &C, wallet: &mut Wallet, args: args::CommissionRateChange, -) -> Result<(), Error> { +) -> Result<(Tx, Option
, common::PublicKey), Error> { let epoch = rpc::query_epoch(client).await; let tx_code_hash = @@ -668,7 +680,7 @@ pub async fn submit_validator_commission_change< tx.set_code(Code::from_hash(tx_code_hash)); let default_signer = args.validator.clone(); - process_tx::( + prepare_tx::( client, wallet, &args.tx, @@ -677,8 +689,52 @@ pub async fn submit_validator_commission_change< #[cfg(not(feature = "mainnet"))] false, ) - .await?; - Ok(()) + .await +} + +/// Submit transaction to unjail a jailed validator +pub async fn build_unjail_validator< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::TxUnjailValidator, +) -> Result<(Tx, Option
, common::PublicKey), Error> { + if !rpc::is_validator(client, &args.validator).await { + eprintln!("The given address {} is not a validator.", &args.validator); + if !args.tx.force { + return Err(Error::InvalidValidatorAddress(args.validator.clone())); + } + } + + let tx_code_path = String::from_utf8(args.tx_code_path).unwrap(); + let tx_code_hash = + query_wasm_code_hash(client, tx_code_path).await.unwrap(); + + let data = args + .validator + .clone() + .try_to_vec() + .map_err(Error::EncodeTxFailure)?; + + let mut tx = Tx::new(TxType::Raw); + tx.header.chain_id = args.tx.chain_id.clone().unwrap(); + tx.header.expiration = args.tx.expiration; + tx.set_data(Data::new(data)); + tx.set_code(Code::from_hash(tx_code_hash)); + + let default_signer = args.validator; + prepare_tx( + client, + wallet, + &args.tx, + tx, + TxSigningKey::WalletAddress(default_signer), + #[cfg(not(feature = "mainnet"))] + false, + ) + .await } /// Submit transaction to unjail a jailed validator @@ -714,7 +770,7 @@ pub async fn submit_unjail_validator< tx.set_code(Code::from_hash(tx_code_hash)); let default_signer = args.validator; - process_tx( + prepare_tx( client, wallet, &args.tx, @@ -728,14 +784,14 @@ pub async fn submit_unjail_validator< } /// Submit transaction to withdraw an unbond -pub async fn submit_withdraw< +pub async fn build_withdraw< C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( client: &C, wallet: &mut Wallet, args: args::Withdraw, -) -> Result<(), Error> { +) -> Result<(Tx, Option
, common::PublicKey), Error> { let epoch = rpc::query_epoch(client).await; let validator = @@ -786,7 +842,7 @@ pub async fn submit_withdraw< tx.set_code(Code::from_hash(tx_code_hash)); let default_signer = args.source.unwrap_or(args.validator); - process_tx::( + prepare_tx::( client, wallet, &args.tx, @@ -795,19 +851,26 @@ pub async fn submit_withdraw< #[cfg(not(feature = "mainnet"))] false, ) - .await?; - Ok(()) + .await } /// Submit a transaction to unbond -pub async fn submit_unbond< +pub async fn build_unbond< C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( client: &C, wallet: &mut Wallet, args: args::Unbond, -) -> Result<(), Error> { +) -> Result< + ( + Tx, + Option
, + common::PublicKey, + Option<(Epoch, token::Amount)>, + ), + Error, +> { let source = args.source.clone(); // Check the source's current bond amount let bond_source = source.clone().unwrap_or_else(|| args.validator.clone()); @@ -872,7 +935,7 @@ pub async fn submit_unbond< tx.set_code(Code::from_hash(tx_code_hash)); let default_signer = args.source.unwrap_or_else(|| args.validator.clone()); - process_tx::( + let (tx, signer_addr, default_signer) = prepare_tx::( client, wallet, &args.tx, @@ -883,6 +946,19 @@ pub async fn submit_unbond< ) .await?; + Ok((tx, signer_addr, default_signer, latest_withdrawal_pre)) +} + +/// Query the unbonds post-tx +pub async fn query_unbonds( + client: &C, + args: args::Unbond, + latest_withdrawal_pre: Option<(Epoch, token::Amount)>, +) -> Result<(), Error> { + let source = args.source.clone(); + // Check the source's current bond amount + let bond_source = source.clone().unwrap_or_else(|| args.validator.clone()); + // Query the unbonds post-tx let unbonds = rpc::query_unbond_with_slashing(client, &bond_source, &args.validator) @@ -932,19 +1008,18 @@ pub async fn submit_unbond< latest_withdraw_epoch_post, ); } - Ok(()) } /// Submit a transaction to bond -pub async fn submit_bond< +pub async fn build_bond< C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( client: &C, wallet: &mut Wallet, args: args::Bond, -) -> Result<(), Error> { +) -> Result<(Tx, Option
, common::PublicKey), Error> { let validator = known_validator_or_err(args.validator.clone(), args.tx.force, client) .await?; @@ -992,7 +1067,7 @@ pub async fn submit_bond< tx.set_code(Code::from_hash(tx_code_hash)); let default_signer = args.source.unwrap_or(args.validator); - process_tx::( + prepare_tx::( client, wallet, &args.tx, @@ -1001,8 +1076,7 @@ pub async fn submit_bond< #[cfg(not(feature = "mainnet"))] false, ) - .await?; - Ok(()) + .await } /// Check if current epoch is in the last third of the voting period of the @@ -1036,14 +1110,14 @@ pub async fn is_safe_voting_window( } /// Submit an IBC transfer -pub async fn submit_ibc_transfer< +pub async fn build_ibc_transfer< C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( client: &C, wallet: &mut Wallet, args: args::TxIbcTransfer, -) -> Result<(), Error> { +) -> Result<(Tx, Option
, common::PublicKey), Error> { // Check that the source address exists on chain let source = source_exists_or_err(args.source.clone(), args.tx.force, client) @@ -1134,7 +1208,7 @@ pub async fn submit_ibc_transfer< tx.set_data(Data::new(data)); tx.set_code(Code::from_hash(tx_code_hash)); - process_tx::( + prepare_tx::( client, wallet, &args.tx, @@ -1143,8 +1217,7 @@ pub async fn submit_ibc_transfer< #[cfg(not(feature = "mainnet"))] false, ) - .await?; - Ok(()) + .await } /// Try to decode the given asset type and add its decoding to the supplied set. @@ -1221,7 +1294,7 @@ async fn used_asset_types< } /// Submit an ordinary transfer -pub async fn submit_transfer< +pub async fn build_transfer< C: crate::ledger::queries::Client + Sync, V: WalletUtils, U: ShieldedUtils, @@ -1230,7 +1303,8 @@ pub async fn submit_transfer< wallet: &mut Wallet, shielded: &mut ShieldedContext, mut args: args::TxTransfer, -) -> Result<(), Error> { +) -> Result<(Tx, Option
, common::PublicKey, Option, bool), Error> +{ let source = args.source.effective_address(); let target = args.target.effective_address(); let token = args.token.clone(); @@ -1293,33 +1367,20 @@ pub async fn submit_transfer< // 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 { - // TODO Refactor me, we shouldn't rely on any specific token here. - ( - TxSigningKey::SecretKey(masp_tx_key()), - token::Amount::default(), - args.native_token.clone(), - ) - } else if source == masp_addr { - ( - TxSigningKey::SecretKey(masp_tx_key()), - validated_amount.amount, - token.clone(), - ) - } else { - ( - TxSigningKey::WalletAddress(args.source.effective_address()), - validated_amount.amount, - token.clone(), - ) - }; + let (_amount, token) = if source == masp_addr && target == masp_addr { + // TODO Refactor me, we shouldn't rely on any specific token here. + (token::Amount::default(), args.native_token.clone()) + } else { + (validated_amount.amount, token) + }; + let default_signer = + TxSigningKey::WalletAddress(args.source.effective_address()); // 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::(client, wallet, &args.tx, default_signer.clone()) .await? - .ref_to(); + .1; let shielded_gas = masp_tx_key().ref_to() == chosen_signer; // Determine whether to pin this transaction to a storage key let key = match &args.target { @@ -1335,126 +1396,105 @@ pub async fn submit_transfer< .await .unwrap(); - // Loop twice in case the first submission attempt fails - for _ in 0..2 { - // Construct the shielded part of the transaction, if any - let stx_result = shielded - .gen_shielded_transfer(client, &args, shielded_gas) - .await; + // Construct the shielded part of the transaction, if any + let stx_result = shielded + .gen_shielded_transfer(client, &args, shielded_gas) + .await; - let shielded_parts = match stx_result { - Ok(stx) => Ok(stx), - Err(builder::Error::InsufficientFunds(_)) => { - Err(Error::NegativeBalanceAfterTransfer( - source.clone(), - validated_amount.amount.to_string_native(), - token.clone(), - validate_fee.amount.to_string_native(), - args.tx.fee_token.clone(), - )) - } - Err(err) => Err(Error::MaspError(err)), - }?; - - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = args.tx.chain_id.clone().unwrap(); - tx.header.expiration = args.tx.expiration; - // Add the MASP Transaction and its Builder to facilitate validation - let (masp_hash, shielded_tx_epoch) = - if let Some(shielded_parts) = shielded_parts { - // Add a MASP Transaction section to the Tx - let masp_tx = tx.add_section(Section::MaspTx(shielded_parts.1)); - // Get the hash of the MASP Transaction section - let masp_hash = masp_tx.get_hash(); - // Get the decoded asset types used in the transaction to give - // offline wallet users more information - let asset_types = - used_asset_types(shielded, client, &shielded_parts.0) - .await - .unwrap_or_default(); - // Add the MASP Transaction's Builder to the Tx - tx.add_section(Section::MaspBuilder(MaspBuilder { - asset_types, - // Store how the Info objects map to Descriptors/Outputs - metadata: shielded_parts.2, - // Store the data that was used to construct the Transaction - builder: shielded_parts.0, - // Link the Builder to the Transaction by hash code - target: masp_hash, - })); - // The MASP Transaction section hash will be used in Transfer - (Some(masp_hash), Some(shielded_parts.3)) - } else { - (None, None) - }; - // Construct the corresponding transparent Transfer object - let transfer = token::Transfer { - source: source.clone(), - target: target.clone(), - token: token.clone(), - sub_prefix: sub_prefix.clone(), - amount: validated_amount, - key: key.clone(), - // Link the Transfer to the MASP Transaction by hash code - shielded: masp_hash, - }; - tracing::debug!("Transfer data {:?}", transfer); - // Encode the Transfer and store it beside the MASP transaction - let data = transfer - .try_to_vec() - .expect("Encoding tx data shouldn't fail"); - tx.set_data(Data::new(data)); - // Finally store the Traansfer WASM code in the Tx - tx.set_code(Code::from_hash(tx_code_hash)); - - // Dry-run/broadcast/submit the transaction - let result = process_tx::( - client, - wallet, - &args.tx, - tx, - default_signer.clone(), - #[cfg(not(feature = "mainnet"))] - is_source_faucet, - ) - .await?; - - // Query the epoch in which the transaction was probably submitted - let submission_epoch = rpc::query_epoch(client).await; - - match result { - ProcessTxResponse::Applied(resp) if - // If a transaction is shielded - shielded_tx_epoch.is_some() && - // And it is rejected by a VP - resp.code == 1.to_string() && - // And the its submission epoch doesn't match construction epoch - shielded_tx_epoch.unwrap() != submission_epoch => - { - // Then we probably straddled an epoch boundary. Let's retry... - eprintln!( - "MASP transaction rejected and this may be due to the \ - epoch changing. Attempting to resubmit transaction.", - ); - continue; - }, - // Otherwise either the transaction was successful or it will not - // benefit from resubmission - _ => break, + let shielded_parts = match stx_result { + Ok(stx) => Ok(stx), + Err(builder::Error::InsufficientFunds(_)) => { + Err(Error::NegativeBalanceAfterTransfer( + Box::new(source.clone()), + validated_amount.amount.to_string_native(), + Box::new(token.clone()), + validate_fee.amount.to_string_native(), + Box::new(args.tx.fee_token.clone()), + )) } - } - Ok(()) + Err(err) => Err(Error::MaspError(err)), + }?; + + let mut tx = Tx::new(TxType::Raw); + tx.header.chain_id = args.tx.chain_id.clone().unwrap(); + tx.header.expiration = args.tx.expiration; + // Add the MASP Transaction and its Builder to facilitate validation + let (masp_hash, shielded_tx_epoch) = if let Some(shielded_parts) = + shielded_parts + { + // Add a MASP Transaction section to the Tx + let masp_tx = tx.add_section(Section::MaspTx(shielded_parts.1)); + // Get the hash of the MASP Transaction section + let masp_hash = masp_tx.get_hash(); + // Get the decoded asset types used in the transaction to give + // offline wallet users more information + let asset_types = used_asset_types(shielded, client, &shielded_parts.0) + .await + .unwrap_or_default(); + // Add the MASP Transaction's Builder to the Tx + tx.add_section(Section::MaspBuilder(MaspBuilder { + asset_types, + // Store how the Info objects map to Descriptors/Outputs + metadata: shielded_parts.2, + // Store the data that was used to construct the Transaction + builder: shielded_parts.0, + // Link the Builder to the Transaction by hash code + target: masp_hash, + })); + // The MASP Transaction section hash will be used in Transfer + (Some(masp_hash), Some(shielded_parts.3)) + } else { + (None, None) + }; + // Construct the corresponding transparent Transfer object + let transfer = token::Transfer { + source: source.clone(), + target: target.clone(), + token: token.clone(), + sub_prefix: sub_prefix.clone(), + amount: validated_amount, + key: key.clone(), + // Link the Transfer to the MASP Transaction by hash code + shielded: masp_hash, + }; + tracing::debug!("Transfer data {:?}", transfer); + // Encode the Transfer and store it beside the MASP transaction + let data = transfer + .try_to_vec() + .expect("Encoding tx data shouldn't fail"); + tx.set_data(Data::new(data)); + // Finally store the Traansfer WASM code in the Tx + tx.set_code(Code::from_hash(tx_code_hash)); + + // Dry-run/broadcast/submit the transaction + let (tx, signer_addr, def_key) = prepare_tx::( + client, + wallet, + &args.tx, + tx, + default_signer.clone(), + #[cfg(not(feature = "mainnet"))] + is_source_faucet, + ) + .await?; + Ok(( + tx, + signer_addr, + def_key, + shielded_tx_epoch, + is_source_faucet, + )) } /// Submit a transaction to initialize an account -pub async fn submit_init_account< +pub async fn build_init_account< C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( client: &C, wallet: &mut Wallet, args: args::TxInitAccount, -) -> Result<(), Error> { +) -> Result<(Tx, Option
, common::PublicKey), Error> { let public_key = args.public_key; let vp_code_hash = @@ -1480,8 +1520,7 @@ pub async fn submit_init_account< tx.set_data(Data::new(data)); tx.set_code(Code::from_hash(tx_code_hash)); - // TODO Move unwrap to an either - let initialized_accounts = process_tx::( + prepare_tx::( client, wallet, &args.tx, @@ -1491,22 +1530,17 @@ pub async fn submit_init_account< false, ) .await - .unwrap() - .initialized_accounts(); - save_initialized_accounts::(wallet, &args.tx, initialized_accounts) - .await; - Ok(()) } /// Submit a transaction to update a VP -pub async fn submit_update_vp< +pub async fn build_update_vp< C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( client: &C, wallet: &mut Wallet, args: args::TxUpdateVp, -) -> Result<(), Error> { +) -> Result<(Tx, Option
, common::PublicKey), Error> { let addr = args.addr.clone(); // Check that the address is established and exists on chain @@ -1572,7 +1606,7 @@ pub async fn submit_update_vp< tx.set_data(Data::new(data)); tx.set_code(Code::from_hash(tx_code_hash)); - process_tx::( + prepare_tx::( client, wallet, &args.tx, @@ -1581,26 +1615,25 @@ pub async fn submit_update_vp< #[cfg(not(feature = "mainnet"))] false, ) - .await?; - Ok(()) + .await } /// Submit a custom transaction -pub async fn submit_custom< +pub async fn build_custom< C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( client: &C, wallet: &mut Wallet, args: args::TxCustom, -) -> Result<(), Error> { +) -> Result<(Tx, Option
, common::PublicKey), Error> { let mut tx = Tx::new(TxType::Raw); tx.header.chain_id = args.tx.chain_id.clone().unwrap(); tx.header.expiration = args.tx.expiration; args.data_path.map(|data| tx.set_data(Data::new(data))); tx.set_code(Code::new(args.code_path)); - let initialized_accounts = process_tx::( + prepare_tx::( client, wallet, &args.tx, @@ -1609,11 +1642,7 @@ pub async fn submit_custom< #[cfg(not(feature = "mainnet"))] false, ) - .await? - .initialized_accounts(); - save_initialized_accounts::(wallet, &args.tx, initialized_accounts) - .await; - Ok(()) + .await } async fn expect_dry_broadcast( diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 70900abf7d..4593bf50e1 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -527,7 +527,9 @@ fn ledger_txs_and_queries() -> Result<()> { for tx_args in &txs_args { for &dry_run in &[true, false] { - let tx_args = if dry_run { + let tx_args = if dry_run && tx_args[0] == "tx" { + continue; + } else if dry_run { vec![tx_args.clone(), vec!["--dry-run"]].concat() } else { tx_args.clone() @@ -707,8 +709,6 @@ fn masp_txs_and_queries() -> Result<()> { ETH, "--amount", "10", - "--signer", - ALBERT, "--node", &validator_one_rpc, ], @@ -726,8 +726,6 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "7", - "--signer", - ALBERT, "--node", &validator_one_rpc, ], @@ -745,8 +743,6 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "7", - "--signer", - ALBERT, "--node", &validator_one_rpc, ], @@ -764,8 +760,6 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "7", - "--signer", - ALBERT, "--node", &validator_one_rpc, ], @@ -783,8 +777,6 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "6", - "--signer", - ALBERT, "--node", &validator_one_rpc, ], @@ -839,8 +831,6 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "20", - "--signer", - BERTHA, "--node", &validator_one_rpc, ], diff --git a/wasm_for_tests/tx_write_storage_key.wasm b/wasm_for_tests/tx_write_storage_key.wasm new file mode 100755 index 0000000000..a0fb758ae9 Binary files /dev/null and b/wasm_for_tests/tx_write_storage_key.wasm differ