From 86cc779c3b88106a4e490e89489b63f75babfe86 Mon Sep 17 00:00:00 2001 From: Murisi Tarusenga Date: Thu, 1 Jun 2023 15:27:33 +0200 Subject: [PATCH 01/12] Removed unnecessary header updates for dry runs. --- shared/src/ledger/signing.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/shared/src/ledger/signing.rs b/shared/src/ledger/signing.rs index 30bdb12d34..d41420fb9d 100644 --- a/shared/src/ledger/signing.rs +++ b/shared/src/ledger/signing.rs @@ -48,7 +48,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, }; @@ -214,12 +213,6 @@ pub async fn sign_tx< 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( From e78396c35162a9db4023723dc997be419044fe39 Mon Sep 17 00:00:00 2001 From: Murisi Tarusenga Date: Fri, 2 Jun 2023 16:56:39 +0200 Subject: [PATCH 02/12] Separate transaction building from signing from submission. --- apps/src/lib/client/signing.rs | 7 +- apps/src/lib/client/tx.rs | 272 ++++++++++-- .../lib/node/ledger/shell/finalize_block.rs | 10 +- apps/src/lib/node/ledger/shell/mod.rs | 8 +- .../lib/node/ledger/shell/prepare_proposal.rs | 17 +- .../lib/node/ledger/shell/process_proposal.rs | 32 +- core/src/types/transaction/mod.rs | 4 +- core/src/types/transaction/wrapper.rs | 10 +- shared/src/ledger/signing.rs | 154 +++++-- shared/src/ledger/tx.rs | 409 +++++++++--------- wasm/checksums.json | 38 +- wasm_for_tests/tx_write_storage_key.wasm | Bin 0 -> 465585 bytes 12 files changed, 643 insertions(+), 318 deletions(-) create mode 100755 wasm_for_tests/tx_write_storage_key.wasm diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index c44da88f9b..3ac138956d 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -57,19 +57,16 @@ pub async fn sign_tx< >( client: &C, wallet: &mut Wallet, - tx: Tx, + tx: &mut Tx, args: &args::Tx, default: TxSigningKey, - #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) -> Result { +) -> Result<(), tx::Error> { namada::ledger::signing::sign_tx::( client, wallet, tx, args, default, - #[cfg(not(feature = "mainnet"))] - requires_pow, ) .await } diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index be8936f6ee..075a7be96b 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -15,7 +15,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, tx, signing}; use namada::proof_of_stake::parameters::PosParams; use namada::proto::{Code, Data, Section, Tx}; use namada::types::address::Address; @@ -56,7 +56,23 @@ pub async fn submit_custom( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_custom::(client, &mut ctx.wallet, args).await + let (mut tx, default_signer) = tx::build_custom::(client, &mut ctx.wallet, args.clone()).await?; + signing::sign_tx::( + client, + &mut ctx.wallet, + &mut tx, + &args.tx, + default_signer, + ) + .await?; + tx::process_tx::( + client, + &mut ctx.wallet, + &args.tx, + tx, + ) + .await?; + Ok(()) } pub async fn submit_update_vp( @@ -68,7 +84,23 @@ pub async fn submit_update_vp( .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, default_signer) = tx::build_update_vp::(client, &mut ctx.wallet, args.clone()).await?; + signing::sign_tx::( + client, + &mut ctx.wallet, + &mut tx, + &args.tx, + default_signer, + ) + .await?; + tx::process_tx::( + client, + &mut ctx.wallet, + &args.tx, + tx, + ) + .await?; + Ok(()) } pub async fn submit_init_account( @@ -80,7 +112,27 @@ pub async fn submit_init_account( .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, default_signer) = tx::build_init_account::( + client, + &mut ctx.wallet, + args.clone(), + ).await?; + signing::sign_tx::( + client, + &mut ctx.wallet, + &mut tx, + &args.tx, + default_signer, + ) + .await?; + tx::process_tx::( + client, + &mut ctx.wallet, + &args.tx, + tx, + ) + .await?; + Ok(()) } pub async fn submit_init_validator< @@ -230,9 +282,9 @@ 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, default_signer) = tx::prepare_tx( client, - ctx, + &mut ctx.wallet, &tx_args, tx, TxSigningKey::WalletAddress(source), @@ -240,7 +292,15 @@ pub async fn submit_init_validator< false, ) .await - .expect("expected process_tx to work"); + .expect("expected process_tx to work"); + + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &tx_args, default_signer) + .await + .expect("expected process_tx to work"); + + let (mut ctx, result) = process_tx(client, ctx, &tx_args, tx) + .await + .expect("expected process_tx to work"); if !tx_args.dry_run { let (validator_address_alias, validator_address) = match &result[..] { @@ -452,7 +512,53 @@ pub async fn submit_transfer( .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 (mut tx, default_signer, shielded_tx_epoch) = tx::build_transfer( + client, + &mut ctx.wallet, + &mut ctx.shielded, + args.clone(), + ).await?; + signing::sign_tx( + client, + &mut ctx.wallet, + &mut tx, + &args.tx, + default_signer, + ) + .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 + 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, + } + } + Ok(()) } pub async fn submit_ibc_transfer( @@ -464,7 +570,23 @@ pub async fn submit_ibc_transfer( .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, default_signer) = tx::build_ibc_transfer::(client, &mut ctx.wallet, args.clone()).await?; + signing::sign_tx::( + client, + &mut ctx.wallet, + &mut tx, + &args.tx, + default_signer, + ) + .await?; + tx::process_tx::( + client, + &mut ctx.wallet, + &args.tx, + tx, + ) + .await?; + Ok(()) } pub async fn submit_init_proposal( @@ -611,16 +733,26 @@ 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, default_signer) = tx::prepare_tx( client, - ctx, + &mut ctx.wallet, &args.tx, tx, TxSigningKey::WalletAddress(signer), #[cfg(not(feature = "mainnet"))] false, ) - .await?; + .await + .expect("expected process_tx to work"); + + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, default_signer) + .await + .expect("expected process_tx to work"); + + process_tx(client, ctx, &args.tx, tx) + .await + .expect("expected process_tx to work"); + Ok(()) } } @@ -869,16 +1001,26 @@ 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, default_signer) = tx::prepare_tx( client, - ctx, + &mut ctx.wallet, &args.tx, tx, TxSigningKey::WalletAddress(signer.clone()), #[cfg(not(feature = "mainnet"))] false, ) - .await?; + .await + .expect("expected process_tx to work"); + + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, default_signer) + .await + .expect("expected process_tx to work"); + + process_tx(client, ctx, &args.tx, tx) + .await + .expect("expected process_tx to work"); + Ok(()) } None => { @@ -1004,7 +1146,23 @@ pub async fn submit_bond( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_bond::(client, &mut ctx.wallet, args).await + let (mut tx, default_signer) = tx::build_bond::(client, &mut ctx.wallet, args.clone()).await?; + signing::sign_tx::( + client, + &mut ctx.wallet, + &mut tx, + &args.tx, + default_signer, + ) + .await?; + tx::process_tx::( + client, + &mut ctx.wallet, + &args.tx, + tx, + ) + .await?; + Ok(()) } pub async fn submit_unbond( @@ -1016,7 +1174,24 @@ pub async fn submit_unbond( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_unbond::(client, &mut ctx.wallet, args).await + let (mut tx, default_signer, latest_withdrawal_pre) = tx::build_unbond::(client, &mut ctx.wallet, args.clone()).await?; + signing::sign_tx::( + client, + &mut ctx.wallet, + &mut tx, + &args.tx, + default_signer, + ) + .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( @@ -1028,7 +1203,23 @@ pub async fn submit_withdraw( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_withdraw::(client, &mut ctx.wallet, args).await + let (mut tx, default_signer) = tx::build_withdraw::(client, &mut ctx.wallet, args.clone()).await?; + signing::sign_tx::( + client, + &mut ctx.wallet, + &mut tx, + &args.tx, + default_signer, + ) + .await?; + tx::process_tx::( + client, + &mut ctx.wallet, + &args.tx, + tx, + ) + .await?; + Ok(()) } pub async fn submit_validator_commission_change< @@ -1042,12 +1233,28 @@ pub async fn submit_validator_commission_change< .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_validator_commission_change::( + let (mut tx, default_signer) = tx::build_validator_commission_change::( client, &mut ctx.wallet, - args, + args.clone(), ) - .await + .await?; + signing::sign_tx::( + client, + &mut ctx.wallet, + &mut tx, + &args.tx, + default_signer, + ) + .await?; + tx::process_tx::( + client, + &mut ctx.wallet, + &args.tx, + tx, + ) + .await?; + Ok(()) } pub async fn submit_unjail_validator< @@ -1061,7 +1268,23 @@ pub async fn submit_unjail_validator< .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - tx::submit_unjail_validator::(client, &mut ctx.wallet, args).await + let (mut tx, default_signer) = tx::build_unjail_validator::(client, &mut ctx.wallet, args.clone()).await?; + signing::sign_tx::( + client, + &mut ctx.wallet, + &mut tx, + &args.tx, + default_signer, + ) + .await?; + tx::process_tx::( + client, + &mut ctx.wallet, + &args.tx, + tx, + ) + .await?; + Ok(()) } /// Submit transaction and wait for result. Returns a list of addresses @@ -1071,8 +1294,6 @@ async fn process_tx( 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 @@ -1086,9 +1307,6 @@ async fn process_tx( &mut ctx.wallet, &args, tx, - default_signer, - #[cfg(not(feature = "mainnet"))] - requires_pow, ) .await? .initialized_accounts(); diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 2c2db986c6..b85e765787 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -980,7 +980,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"))] @@ -1055,7 +1055,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"))] @@ -1179,7 +1179,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"))] @@ -1216,7 +1216,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"))] @@ -1760,7 +1760,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 50c809a850..04e79290bc 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"))] @@ -1434,7 +1434,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 5d655cb059..e2326d9013 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"))] @@ -639,7 +640,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 7a6d8b660d..0ca4c515e8 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"))] @@ -1319,7 +1319,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"))] @@ -1383,7 +1383,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"))] @@ -1444,7 +1444,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"))] @@ -1487,7 +1487,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 d4104920ab..43be0b438e 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -360,7 +360,7 @@ mod test_process_tx { amount: 10.into(), token: nam(), }, - &keypair, + keypair.ref_to(), Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -396,7 +396,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 08e8f7d2aa..d1f531b24c 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/shared/src/ledger/signing.rs b/shared/src/ledger/signing.rs index d41420fb9d..468d7984d4 100644 --- a/shared/src/ledger/signing.rs +++ b/shared/src/ledger/signing.rs @@ -15,7 +15,7 @@ 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, @@ -118,12 +118,8 @@ pub async fn find_keypair< 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 @@ -141,7 +137,7 @@ pub async fn tx_signer< ) -> Result { // Override the default signing key source if possible let default = if let Some(signing_key) = &args.signing_key { - TxSigningKey::WalletKeypair(signing_key.clone()) + return Ok(signing_key.clone()); } else if let Some(signer) = &args.signer { TxSigningKey::WalletAddress(signer.clone()) } else { @@ -149,7 +145,9 @@ pub async fn tx_signer< }; // Now actually fetch the signing key and apply it match default { - TxSigningKey::WalletKeypair(signing_key) => Ok(signing_key), + TxSigningKey::WalletAddress(signer) if signer == masp() => { + Ok(masp_tx_key()) + }, TxSigningKey::WalletAddress(signer) => { let signer = signer; let signing_key = find_keypair::( @@ -170,7 +168,6 @@ pub async fn tx_signer< } Ok(signing_key) } - TxSigningKey::SecretKey(signing_key) => Ok(signing_key), 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." @@ -193,11 +190,10 @@ pub async fn sign_tx< >( client: &C, wallet: &mut Wallet, - mut tx: Tx, + tx: &mut Tx, args: &args::Tx, default: TxSigningKey, - #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) -> Result { +) -> Result<(), Error> { let keypair = tx_signer::(client, wallet, args, default).await?; // Sign over the transacttion data tx.add_section(Section::Signature(Signature::new( @@ -209,26 +205,134 @@ pub async fn sign_tx< tx.code_sechash(), &keypair, ))); + // Then sign over the bound wrapper + tx.add_section(Section::Signature(Signature::new( + &tx.header_hash(), + &keypair, + ))); + Ok(()) +} - let epoch = rpc::query_epoch(client).await; - - let broadcast_data = if args.dry_run { - TxBroadcastData::DryRun(tx) +/// 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 { + let fee_amount = if cfg!(feature = "mainnet") { + Amount::native_whole(MIN_FEE) } else { - sign_wrapper( + let wrapper_tx_fees_key = parameter_storage::get_wrapper_tx_fees_key(); + rpc::query_storage_value::( client, - wallet, - args, - epoch, - tx, - &keypair, - #[cfg(not(feature = "mainnet"))] - requires_pow, + &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 { + eprintln!( + "The wrapper transaction source doesn't have enough balance to \ + pay fee {fee_amount}, got {balance}." + ); + if !args.force && cfg!(feature = "mainnet") { + panic!( + "The wrapper transaction source doesn't have enough balance \ + to pay fee {fee_amount}, got {balance}." + ); + } + } + + #[cfg(not(feature = "mainnet"))] + // A PoW solution can be used to allow zero-fee testnet transactions + let pow_solution: Option = { + // 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) + } else { + None + } }; - Ok(broadcast_data) + // This object governs how the payload will be processed + tx.update_header(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount: fee_amount, + token: fee_token.clone(), + }, + 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"); + } + + tx } /// Create a wrapper tx from a normal tx. Get the hash of the @@ -309,7 +413,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 27245ecaed..01c2551cab 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -38,7 +38,7 @@ 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::{find_keypair, sign_tx, tx_signer, TxSigningKey, wrap_tx}; use crate::ledger::wallet::{Wallet, WalletUtils}; use crate::proto::{Code, Data, MaspBuilder, Section, Signature, Tx}; use crate::tendermint_rpc::endpoint::broadcast::tx_sync::Response; @@ -221,29 +221,73 @@ impl ProcessTxResponse { } } +/// 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, + >( + client: &C, + wallet: &mut Wallet, + args: &args::Tx, + tx: Tx, + default_signer: TxSigningKey, + #[cfg(not(feature = "mainnet"))] requires_pow: bool, +) -> Result<(Tx, TxSigningKey), Error> { + if args.dry_run { + Ok((tx, default_signer)) + } else { + let keypair = tx_signer::( + client, + wallet, + args, + default_signer.clone(), + ).await? + .ref_to(); + let epoch = rpc::query_epoch(client).await; + Ok((wrap_tx( + client, + wallet, + args, + epoch, + tx.clone(), + &keypair, + #[cfg(not(feature = "mainnet"))] + requires_pow, + ) + .await, default_signer)) + } +} + /// 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, + C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( client: &C, wallet: &mut Wallet, args: &args::Tx, - tx: Tx, - default_signer: TxSigningKey, - #[cfg(not(feature = "mainnet"))] requires_pow: bool, + mut tx: Tx, ) -> Result { - let to_broadcast = sign_tx::( - client, - wallet, + // Remove all the sensitive sections + tx.protocol_filter(); + // 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, - args, - default_signer, - #[cfg(not(feature = "mainnet"))] - requires_pow, - ) - .await?; + wrapper_hash, + decrypted_hash, + }; // NOTE: use this to print the request JSON body: // let request = @@ -259,18 +303,17 @@ pub async fn process_tx< } 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) + 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), + } } } } @@ -593,14 +636,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::TxCommissionRateChange, -) -> Result<(), Error> { +) -> Result<(Tx, TxSigningKey), Error> { let epoch = rpc::query_epoch(client).await; let tx_code_hash = @@ -674,7 +717,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, @@ -683,19 +726,18 @@ 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 submit_unjail_validator< +pub async fn build_unjail_validator< C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( client: &C, wallet: &mut Wallet, args: args::TxUnjailValidator, -) -> Result<(), Error> { +) -> Result<(Tx, TxSigningKey), Error> { if !rpc::is_validator(client, &args.validator).await { eprintln!("The given address {} is not a validator.", &args.validator); if !args.tx.force { @@ -720,7 +762,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, @@ -729,19 +771,18 @@ pub async fn submit_unjail_validator< #[cfg(not(feature = "mainnet"))] false, ) - .await?; - Ok(()) + .await } /// 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, TxSigningKey), Error> { let epoch = rpc::query_epoch(client).await; let validator = @@ -792,7 +833,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, @@ -801,19 +842,18 @@ 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, TxSigningKey, 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()); @@ -878,7 +918,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, default_signer) = prepare_tx::( client, wallet, &args.tx, @@ -889,6 +929,19 @@ pub async fn submit_unbond< ) .await?; + Ok((tx, 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) @@ -938,19 +991,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, TxSigningKey), Error> { let validator = known_validator_or_err(args.validator.clone(), args.tx.force, client) .await?; @@ -998,7 +1050,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, @@ -1007,8 +1059,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 @@ -1042,14 +1093,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, TxSigningKey), Error> { // Check that the source address exists on chain let source = source_exists_or_err(args.source.clone(), args.tx.force, client) @@ -1140,7 +1191,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, @@ -1149,8 +1200,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. @@ -1227,7 +1277,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, @@ -1235,8 +1285,8 @@ pub async fn submit_transfer< client: &C, wallet: &mut Wallet, shielded: &mut ShieldedContext, - mut args: args::TxTransfer, -) -> Result<(), Error> { + args: args::TxTransfer, +) -> Result<(Tx, TxSigningKey, Option), Error> { let source = args.source.effective_address(); let target = args.target.effective_address(); let token = args.token.clone(); @@ -1292,40 +1342,33 @@ pub async fn submit_transfer< args.tx.force, client, ) - .await?; + .await?; let masp_addr = masp(); // For MASP sources, use a special sentinel key recognized by VPs as default // 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) = + let (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(), + 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(); + .await? + .ref_to(); 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 { @@ -1338,131 +1381,104 @@ pub async fn submit_transfer< let tx_code_hash = query_wasm_code_hash(client, args.tx_code_path.to_str().unwrap()) - .await - .unwrap(); + .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 = - Hash(masp_tx.hash(&mut Sha256::new()).finalize_reset().into()); - // 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?; + let shielded_parts = match stx_result { + Ok(stx) => Ok(stx), + Err(builder::Error::InsufficientFunds(_)) => { + Err(Error::NegativeBalanceAfterTransfer( + source.clone(), + args.amount, + token.clone(), + args.tx.fee_amount, + args.tx.fee_token.clone(), + )) + } + Err(err) => Err(Error::MaspError(err)), + }?; - // Query the epoch in which the transaction was probably submitted - let submission_epoch = rpc::query_epoch(client).await; + 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 = + Hash(masp_tx.hash(&mut Sha256::new()).finalize_reset().into()); + // 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, + 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)); - 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, - } - } - Ok(()) + // Dry-run/broadcast/submit the transaction + let (tx, def_key) = prepare_tx::( + client, + wallet, + &args.tx, + tx, + default_signer.clone(), + #[cfg(not(feature = "mainnet"))] + is_source_faucet, + ) + .await?; + Ok((tx, def_key, shielded_tx_epoch)) } /// 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, TxSigningKey), Error> { let public_key = args.public_key; let vp_code_hash = @@ -1490,8 +1506,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, @@ -1501,22 +1516,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, TxSigningKey), Error> { let addr = args.addr.clone(); // Check that the address is established and exists on chain @@ -1584,7 +1594,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, @@ -1593,26 +1603,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, TxSigningKey), 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, @@ -1621,11 +1630,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/wasm/checksums.json b/wasm/checksums.json index 4dc880d270..58f9833cf3 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,21 +1,21 @@ { - "tx_bond.wasm": "tx_bond.22ae29557b12fcd402add2041b0616b02ec517b255f57fe2a2b42f8e8b122d6a.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.bc2221a98b0a7434ee10fc60e90c6da75722d18233bb20b878959ca739dde3a3.wasm", - "tx_ibc.wasm": "tx_ibc.6a67b61b4b749498f5f11d667d41bad841781089bf104f53843675569cbead06.wasm", - "tx_init_account.wasm": "tx_init_account.8d0853e9b8c2c453ebf18d32ba7268150318d64efd93d93bcfbb1f1e21789dfd.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.6fb695273206ac9a0666e6d25ed002cd335673b6f444e01df9fcc8b4a0bf5967.wasm", - "tx_init_validator.wasm": "tx_init_validator.d57519588d95903437d987157d293e25f2e919fa4da4b9e998b95f4b6c8e098b.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.24a641072eb7aba949f0d4f927dc156f8096d3e4da6fad046363a3fc1cf0cb01.wasm", - "tx_transfer.wasm": "tx_transfer.60a7dbe38bad2a86f52f6cd109cd30002b46de4cd7ee82045fd3302da6deb424.wasm", - "tx_unbond.wasm": "tx_unbond.64ce8a1181e993c8cea75ec52c8a103d98d946e633ccd1c134041ba5473ffbce.wasm", - "tx_unjail_validator.wasm": "tx_unjail_validator.fdb92288ddf746c8e94fdd3d1cef8eb0498a576da6d080bce9a0c6ed32dde909.wasm", - "tx_update_vp.wasm": "tx_update_vp.5ede9d5a56d8ebe7075f9a9bb8a988fcd629e5ca1f800ca738a10d8b31d56ae7.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.2f7176863cec7379cbf2c27e17dc6475e2a4b5a772e4142bb3cefa5783cb710f.wasm", - "tx_withdraw.wasm": "tx_withdraw.f30cc0a7d015cdc141cb3d68865006486f0cdb40140ddafec11528fd3e9ceb3c.wasm", - "vp_implicit.wasm": "vp_implicit.ee1caec6e233c899dd6c0f7106ae493304394a7a0774db0cd0714afdee8d26bd.wasm", - "vp_masp.wasm": "vp_masp.8f2e84eeacf76c17892d7cd4e327a071ce399ab4cdd48fb709bb66324e82bea1.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.9cd90e26b1d2660ad1add04e65c302325de7210fca5cce2f520a08ad83f9cba7.wasm", - "vp_token.wasm": "vp_token.94be4a19c2b3e4b78f61d213ea5d070e81f6a79348bc6b05ff6cc36291a2dbcb.wasm", - "vp_user.wasm": "vp_user.595d69f43189d651c7fddbb9106683522ae5128194b382ff4193952930faa142.wasm", - "vp_validator.wasm": "vp_validator.285dc9030008de641312269bb4e707a2882ff7daeb18ddfc70a238eddf00af57.wasm" + "tx_bond.wasm": "tx_bond.b23503aa5d68ca2714dd9f3f370270c515b0a72ca731092a3d864b040f2a0a68.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.c332e0748c360848dbfa177be595ef2ecc3beb4ef448658efe7dd0316fa59355.wasm", + "tx_ibc.wasm": "tx_ibc.3f41ce92b36c8ee49545dce04741e4e2819c6f2a1b7b97ca2a163d6ba82a1761.wasm", + "tx_init_account.wasm": "tx_init_account.0d02d5932a767bdcd29cc16637e4172490459fbf6598af1b27465734019e2c48.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.94707372396ffdf38ac20b49d1247b6204572bdac35581ab3524ad3a0a61a236.wasm", + "tx_init_validator.wasm": "tx_init_validator.a4178a8af1e2369fc7218a0e3b408966bfcdaf9cc013fdb97e70df13083076d6.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.cfea4de5cdb0c39923cb367f5d57fd6e55f7e98162d91e14bc650a5a19669f94.wasm", + "tx_transfer.wasm": "tx_transfer.0c5c0535d293be46ec44721be1f06d46ac084604144498ef8b196d8bc34029cc.wasm", + "tx_unbond.wasm": "tx_unbond.1472fa9c81dadff1bc39899875aa0b229ca8143b4f882567aa3a9691233853bc.wasm", + "tx_unjail_validator.wasm": "tx_unjail_validator.57e54c3cfdc34959e8f572368ff5de24fa4681b4d50d0642c7436a2d77ecd9ff.wasm", + "tx_update_vp.wasm": "tx_update_vp.6c295330b7e913bb6d29e92b8fd9ddb665f63039a3b4ff0f13e2c62da845c8f5.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.97e985b23f668c8aa0e5233070c0ce488effa4d8b2396f62c7dca06e353b5bab.wasm", + "tx_withdraw.wasm": "tx_withdraw.9a37def5e043cd662681a8fbacf2424b9923aee6d3987781ee8d0ddec33c0b15.wasm", + "vp_implicit.wasm": "vp_implicit.18d45f024a7ffeb2b1fa849f2aa5ba0ea493617217cad66f0c0d170436935f49.wasm", + "vp_masp.wasm": "vp_masp.6747a0fdc27354ea63168575032d9d5649d23e3acddef2af7f09eedb3be16972.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.b74cdffa08ab3d9836d51441387d7bc40db05326eb764bd5b2046d7d3a533184.wasm", + "vp_token.wasm": "vp_token.72cca61e36732e28df4a8388bdb2b997294363d5f34e8c30f248ad13d31eb9ee.wasm", + "vp_user.wasm": "vp_user.165f2b2a67fe8b535a7036b8c41d88e8480a69e051c271ee2fe65ac0f73d810b.wasm", + "vp_validator.wasm": "vp_validator.703b32947b310251698815f91c55812d43722bd86f786d44de56551fbb10c1f0.wasm" } \ No newline at end of file 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 0000000000000000000000000000000000000000..a0fb758ae9663ab199bf18cb5f5f1d18fa7e34c1 GIT binary patch literal 465585 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 AGXMYp literal 0 HcmV?d00001 From 884cc3cbdec1ee325bbbf59b0038a8c0332965e4 Mon Sep 17 00:00:00 2001 From: Murisi Tarusenga Date: Tue, 6 Jun 2023 10:17:47 +0200 Subject: [PATCH 03/12] Separating out the reveal PK transaction construction. --- apps/src/bin/namada-client/cli.rs | 2 +- apps/src/lib/client/signing.rs | 2 +- apps/src/lib/client/tx.rs | 448 +++++++++++++++--------------- shared/src/ledger/signing.rs | 31 +-- shared/src/ledger/tx.rs | 128 +++------ 5 files changed, 274 insertions(+), 337 deletions(-) diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index bf9921a527..c04ba8d785 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/client/signing.rs b/apps/src/lib/client/signing.rs index 3ac138956d..2fae827702 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -59,7 +59,7 @@ pub async fn sign_tx< wallet: &mut Wallet, tx: &mut Tx, args: &args::Tx, - default: TxSigningKey, + default: &common::PublicKey, ) -> Result<(), tx::Error> { namada::ledger::signing::sign_tx::( client, diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 075a7be96b..f267f55a50 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -56,22 +56,23 @@ pub async fn submit_custom( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, default_signer) = tx::build_custom::(client, &mut ctx.wallet, args.clone()).await?; - signing::sign_tx::( - client, - &mut ctx.wallet, - &mut tx, - &args.tx, - default_signer, - ) - .await?; - tx::process_tx::( + let (mut tx, pk) = tx::build_custom(client, &mut ctx.wallet, args.clone()) + .await?; + // Build a transaction to reveal the signer of this transaction + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - &args.tx, - tx, - ) - .await?; + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await?; + if let Some((mut tx, pk)) = reveal_pk { + // Sign the reveal public key transaction with the fee payer + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + } + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; Ok(()) } @@ -84,22 +85,23 @@ pub async fn submit_update_vp( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, default_signer) = tx::build_update_vp::(client, &mut ctx.wallet, args.clone()).await?; - signing::sign_tx::( + let (mut tx, pk) = + tx::build_update_vp(client, &mut ctx.wallet, args.clone()).await?; + // Build a transaction to reveal the signer of this transaction + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - &mut tx, - &args.tx, - default_signer, - ) - .await?; - tx::process_tx::( - client, - &mut ctx.wallet, - &args.tx, - tx, - ) - .await?; + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await?; + if let Some((mut tx, pk)) = reveal_pk { + // Sign the reveal public key transaction with the fee payer + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + } + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; Ok(()) } @@ -112,26 +114,23 @@ pub async fn submit_init_account( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, default_signer) = tx::build_init_account::( + let (mut tx, pk) = + tx::build_init_account(client, &mut ctx.wallet, args.clone()).await?; + // Build a transaction to reveal the signer of this transaction + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - args.clone(), + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, ).await?; - signing::sign_tx::( - client, - &mut ctx.wallet, - &mut tx, - &args.tx, - default_signer, - ) - .await?; - tx::process_tx::( - client, - &mut ctx.wallet, - &args.tx, - tx, - ) - .await?; + if let Some((mut tx, pk)) = reveal_pk { + // Sign the reveal public key transaction with the fee payer + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + } + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; Ok(()) } @@ -153,7 +152,7 @@ pub async fn submit_init_validator< unsafe_dont_encrypt, tx_code_path: _, }: args::TxInitValidator, -) { +) -> Result<(), tx::Error> { let tx_args = args::Tx { chain_id: tx_args .clone() @@ -282,7 +281,7 @@ pub async fn submit_init_validator< tx.set_data(Data::new(data)); tx.set_code(Code::from_hash(tx_code_hash)); - let (mut tx, default_signer) = tx::prepare_tx( + let (mut tx, pk) = tx::prepare_tx( client, &mut ctx.wallet, &tx_args, @@ -291,16 +290,23 @@ pub async fn submit_init_validator< #[cfg(not(feature = "mainnet"))] false, ) - .await - .expect("expected process_tx to work"); - - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &tx_args, default_signer) - .await - .expect("expected process_tx to work"); - - let (mut ctx, result) = process_tx(client, ctx, &tx_args, tx) - .await - .expect("expected process_tx to work"); + .await?; + // Build a transaction to reveal the signer of this transaction + let reveal_pk = tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: tx_args.clone(), public_key: pk.clone() }, + ).await?; + if let Some((mut tx, pk)) = reveal_pk { + // Sign the reveal public key transaction with the fee payer + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &tx_args, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &tx_args, tx).await?; + } + signing::sign_tx(client, &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[..] { @@ -389,8 +395,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 @@ -513,26 +520,26 @@ pub async fn submit_transfer( .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); for _ in 0..2 { - let (mut tx, default_signer, shielded_tx_epoch) = tx::build_transfer( + let arg = args.clone(); + let (mut tx, pk, tx_epoch) = + tx::build_transfer(client, &mut ctx.wallet, &mut ctx.shielded, arg) + .await?; + // Build a transaction to reveal the signer of this transaction + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - &mut ctx.shielded, - args.clone(), + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, ).await?; - signing::sign_tx( - client, - &mut ctx.wallet, - &mut tx, - &args.tx, - default_signer, - ) + if let Some((mut tx, pk)) = reveal_pk { + // Sign the reveal public key transaction with the fee payer + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + } + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) .await?; - let result = tx::process_tx( - client, - &mut ctx.wallet, - &args.tx, - tx, - ) + 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; @@ -540,11 +547,11 @@ pub async fn submit_transfer( match result { ProcessTxResponse::Applied(resp) if // If a transaction is shielded - shielded_tx_epoch.is_some() && + 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 => + tx_epoch.unwrap() != submission_epoch => { // Then we probably straddled an epoch boundary. Let's retry... eprintln!( @@ -570,22 +577,23 @@ pub async fn submit_ibc_transfer( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, default_signer) = tx::build_ibc_transfer::(client, &mut ctx.wallet, args.clone()).await?; - signing::sign_tx::( - client, - &mut ctx.wallet, - &mut tx, - &args.tx, - default_signer, - ) - .await?; - tx::process_tx::( + let (mut tx, pk) = + tx::build_ibc_transfer(client, &mut ctx.wallet, args.clone()).await?; + // Build a transaction to reveal the signer of this transaction + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - &args.tx, - tx, - ) - .await?; + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await?; + if let Some((mut tx, pk)) = reveal_pk { + // Sign the reveal public key transaction with the fee payer + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + } + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; Ok(()) } @@ -733,7 +741,7 @@ pub async fn submit_init_proposal( tx.set_data(Data::new(data)); tx.set_code(Code::from_hash(tx_code_hash)); - let (mut tx, default_signer) = tx::prepare_tx( + let (mut tx, pk) = tx::prepare_tx( client, &mut ctx.wallet, &args.tx, @@ -742,17 +750,23 @@ pub async fn submit_init_proposal( #[cfg(not(feature = "mainnet"))] false, ) - .await - .expect("expected process_tx to work"); - - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, default_signer) - .await - .expect("expected process_tx to work"); - - process_tx(client, ctx, &args.tx, tx) - .await - .expect("expected process_tx to work"); - + .await?; + // Build a transaction to reveal the signer of this transaction + let reveal_pk = tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await?; + if let Some((mut tx, pk)) = reveal_pk { + // Sign the reveal public key transaction with the fee payer + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + } + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + .await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; Ok(()) } } @@ -1001,7 +1015,7 @@ pub async fn submit_vote_proposal( tx.set_data(Data::new(data)); tx.set_code(Code::from_hash(tx_code_hash)); - let (mut tx, default_signer) = tx::prepare_tx( + let (mut tx, pk) = tx::prepare_tx( client, &mut ctx.wallet, &args.tx, @@ -1010,17 +1024,23 @@ pub async fn submit_vote_proposal( #[cfg(not(feature = "mainnet"))] false, ) - .await - .expect("expected process_tx to work"); - - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, default_signer) - .await - .expect("expected process_tx to work"); - - process_tx(client, ctx, &args.tx, tx) - .await - .expect("expected process_tx to work"); - + .await?; + // Build a transaction to reveal the signer of this transaction + let reveal_pk = tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await?; + if let Some((mut tx, pk)) = reveal_pk { + // Sign the reveal public key transaction with the fee payer + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + } + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + .await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; Ok(()) } None => { @@ -1043,48 +1063,14 @@ pub async fn submit_reveal_pk( .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(client, &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 @@ -1146,22 +1132,22 @@ pub async fn submit_bond( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, default_signer) = tx::build_bond::(client, &mut ctx.wallet, args.clone()).await?; - signing::sign_tx::( - client, - &mut ctx.wallet, - &mut tx, - &args.tx, - default_signer, - ) - .await?; - tx::process_tx::( + let (mut tx, pk) = tx::build_bond::(client, &mut ctx.wallet, args.clone()).await?; + // Build a transaction to reveal the signer of this transaction + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - &args.tx, - tx, - ) - .await?; + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await?; + if let Some((mut tx, pk)) = reveal_pk { + // Sign the reveal public key transaction with the fee payer + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + } + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; Ok(()) } @@ -1174,22 +1160,23 @@ pub async fn submit_unbond( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, default_signer, latest_withdrawal_pre) = tx::build_unbond::(client, &mut ctx.wallet, args.clone()).await?; - signing::sign_tx::( - client, - &mut ctx.wallet, - &mut tx, - &args.tx, - default_signer, - ) - .await?; - tx::process_tx::( + let (mut tx, pk, latest_withdrawal_pre) = + tx::build_unbond(client, &mut ctx.wallet, args.clone()).await?; + // Build a transaction to reveal the signer of this transaction + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - &args.tx, - tx, - ) - .await?; + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await?; + if let Some((mut tx, pk)) = reveal_pk { + // Sign the reveal public key transaction with the fee payer + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + } + signing::sign_tx(client, &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(()) } @@ -1203,22 +1190,23 @@ pub async fn submit_withdraw( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, default_signer) = tx::build_withdraw::(client, &mut ctx.wallet, args.clone()).await?; - signing::sign_tx::( - client, - &mut ctx.wallet, - &mut tx, - &args.tx, - default_signer, - ) - .await?; - tx::process_tx::( + let (mut tx, pk) = tx::build_withdraw(client, &mut ctx.wallet, args.clone()) + .await?; + // Build a transaction to reveal the signer of this transaction + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - &args.tx, - tx, - ) - .await?; + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await?; + if let Some((mut tx, pk)) = reveal_pk { + // Sign the reveal public key transaction with the fee payer + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + } + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; Ok(()) } @@ -1233,27 +1221,25 @@ pub async fn submit_validator_commission_change< .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, default_signer) = tx::build_validator_commission_change::( - client, - &mut ctx.wallet, - args.clone(), - ) + let arg = args.clone(); + let (mut tx, pk) = + tx::build_validator_commission_change(client, &mut ctx.wallet, arg) .await?; - signing::sign_tx::( - client, - &mut ctx.wallet, - &mut tx, - &args.tx, - default_signer, - ) - .await?; - tx::process_tx::( + // Build a transaction to reveal the signer of this transaction + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - &args.tx, - tx, - ) - .await?; + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await?; + if let Some((mut tx, pk)) = reveal_pk { + // Sign the reveal public key transaction with the fee payer + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + } + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; Ok(()) } @@ -1268,22 +1254,24 @@ pub async fn submit_unjail_validator< .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, default_signer) = tx::build_unjail_validator::(client, &mut ctx.wallet, args.clone()).await?; - signing::sign_tx::( - client, - &mut ctx.wallet, - &mut tx, - &args.tx, - default_signer, - ) - .await?; - tx::process_tx::( + let (mut tx, pk) = + tx::build_unjail_validator(client, &mut ctx.wallet, args.clone()) + .await?; + // Build a transaction to reveal the signer of this transaction + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - &args.tx, - tx, - ) - .await?; + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await?; + if let Some((mut tx, pk)) = reveal_pk { + // Sign the reveal public key transaction with the fee payer + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + } + signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; Ok(()) } diff --git a/shared/src/ledger/signing.rs b/shared/src/ledger/signing.rs index 468d7984d4..dcc3efaa04 100644 --- a/shared/src/ledger/signing.rs +++ b/shared/src/ledger/signing.rs @@ -135,12 +135,17 @@ pub async fn tx_signer< args: &args::Tx, default: TxSigningKey, ) -> Result { - // Override the default signing key source if possible - let default = if let Some(signing_key) = &args.signing_key { + let default = 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(signing_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 @@ -148,25 +153,17 @@ pub async fn tx_signer< TxSigningKey::WalletAddress(signer) if signer == masp() => { Ok(masp_tx_key()) }, + TxSigningKey::WalletAddress(signer) if signer == (&masp_tx_key().ref_to()).into() => { + Ok(masp_tx_key()) + }, TxSigningKey::WalletAddress(signer) => { - let signer = signer; - let signing_key = find_keypair::( + 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) + .await } TxSigningKey::None => other_err( "All transactions must be signed; please either specify the key \ @@ -192,9 +189,9 @@ pub async fn sign_tx< wallet: &mut Wallet, tx: &mut Tx, args: &args::Tx, - default: TxSigningKey, + keypair: &common::PublicKey, ) -> Result<(), Error> { - let keypair = tx_signer::(client, wallet, args, default).await?; + let keypair = tx_signer::(client, wallet, args, TxSigningKey::WalletAddress(keypair.into())).await?; // Sign over the transacttion data tx.add_section(Section::Signature(Signature::new( tx.data_sechash(), diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index 01c2551cab..b9e27847c2 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -233,17 +233,17 @@ pub async fn prepare_tx< tx: Tx, default_signer: TxSigningKey, #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) -> Result<(Tx, TxSigningKey), Error> { +) -> Result<(Tx, common::PublicKey), Error> { + let keypair = tx_signer::( + client, + wallet, + args, + default_signer.clone(), + ).await? + .ref_to(); if args.dry_run { - Ok((tx, default_signer)) + Ok((tx, keypair)) } else { - let keypair = tx_signer::( - client, - wallet, - args, - default_signer.clone(), - ).await? - .ref_to(); let epoch = rpc::query_epoch(client).await; Ok((wrap_tx( client, @@ -255,7 +255,7 @@ pub async fn prepare_tx< #[cfg(not(feature = "mainnet"))] requires_pow, ) - .await, default_signer)) + .await, keypair)) } } @@ -319,47 +319,41 @@ pub async fn process_tx< } /// 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, 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 @@ -371,7 +365,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, >( @@ -379,7 +373,7 @@ pub async fn submit_reveal_pk_aux< wallet: &mut Wallet, public_key: &common::PublicKey, args: &args::Tx, -) -> Result { +) -> Result<(Tx, 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)?; @@ -397,58 +391,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( - tx.data_sechash(), - &keypair, - ))); - tx.add_section(Section::Signature(Signature::new( - 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 @@ -643,7 +595,7 @@ pub async fn build_validator_commission_change< client: &C, wallet: &mut Wallet, args: args::TxCommissionRateChange, -) -> Result<(Tx, TxSigningKey), Error> { +) -> Result<(Tx, common::PublicKey), Error> { let epoch = rpc::query_epoch(client).await; let tx_code_hash = @@ -737,7 +689,7 @@ pub async fn build_unjail_validator< client: &C, wallet: &mut Wallet, args: args::TxUnjailValidator, -) -> Result<(Tx, TxSigningKey), Error> { +) -> Result<(Tx, 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 { @@ -782,7 +734,7 @@ pub async fn build_withdraw< client: &C, wallet: &mut Wallet, args: args::Withdraw, -) -> Result<(Tx, TxSigningKey), Error> { +) -> Result<(Tx, common::PublicKey), Error> { let epoch = rpc::query_epoch(client).await; let validator = @@ -853,7 +805,7 @@ pub async fn build_unbond< client: &C, wallet: &mut Wallet, args: args::Unbond, -) -> Result<(Tx, TxSigningKey, Option<(Epoch, token::Amount)>), Error> { +) -> Result<(Tx, 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()); @@ -1002,7 +954,7 @@ pub async fn build_bond< client: &C, wallet: &mut Wallet, args: args::Bond, -) -> Result<(Tx, TxSigningKey), Error> { +) -> Result<(Tx, common::PublicKey), Error> { let validator = known_validator_or_err(args.validator.clone(), args.tx.force, client) .await?; @@ -1100,7 +1052,7 @@ pub async fn build_ibc_transfer< client: &C, wallet: &mut Wallet, args: args::TxIbcTransfer, -) -> Result<(Tx, TxSigningKey), Error> { +) -> Result<(Tx, common::PublicKey), Error> { // Check that the source address exists on chain let source = source_exists_or_err(args.source.clone(), args.tx.force, client) @@ -1286,7 +1238,7 @@ pub async fn build_transfer< wallet: &mut Wallet, shielded: &mut ShieldedContext, args: args::TxTransfer, -) -> Result<(Tx, TxSigningKey, Option), Error> { +) -> Result<(Tx, common::PublicKey, Option), Error> { let source = args.source.effective_address(); let target = args.target.effective_address(); let token = args.token.clone(); @@ -1478,7 +1430,7 @@ pub async fn build_init_account< client: &C, wallet: &mut Wallet, args: args::TxInitAccount, -) -> Result<(Tx, TxSigningKey), Error> { +) -> Result<(Tx, common::PublicKey), Error> { let public_key = args.public_key; let vp_code_hash = @@ -1526,7 +1478,7 @@ pub async fn build_update_vp< client: &C, wallet: &mut Wallet, args: args::TxUpdateVp, -) -> Result<(Tx, TxSigningKey), Error> { +) -> Result<(Tx, common::PublicKey), Error> { let addr = args.addr.clone(); // Check that the address is established and exists on chain @@ -1614,7 +1566,7 @@ pub async fn build_custom< client: &C, wallet: &mut Wallet, args: args::TxCustom, -) -> Result<(Tx, TxSigningKey), Error> { +) -> Result<(Tx, 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; From a01c863e32cf2a8e2926660d87a04476553f3fb6 Mon Sep 17 00:00:00 2001 From: Murisi Tarusenga Date: Thu, 8 Jun 2023 07:25:14 +0200 Subject: [PATCH 04/12] Now update proof of work solution in transaction header. --- apps/src/lib/client/tx.rs | 134 ++++++++++++++++++++++++----------- shared/src/ledger/signing.rs | 117 ++++++++++++++++++------------ shared/src/ledger/tx.rs | 4 +- 3 files changed, 167 insertions(+), 88 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index f267f55a50..49296a78e7 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -64,12 +64,16 @@ pub async fn submit_custom( &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, ).await?; - if let Some((mut tx, pk)) = reveal_pk { + if let Some((mut rtx, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; } signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; @@ -93,12 +97,16 @@ pub async fn submit_update_vp( &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, ).await?; - if let Some((mut tx, pk)) = reveal_pk { + if let Some((mut rtx, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; } signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; @@ -122,12 +130,16 @@ pub async fn submit_init_account( &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, ).await?; - if let Some((mut tx, pk)) = reveal_pk { + if let Some((mut rtx, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; } signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; @@ -297,12 +309,16 @@ pub async fn submit_init_validator< &mut ctx.wallet, args::RevealPk { tx: tx_args.clone(), public_key: pk.clone() }, ).await?; - if let Some((mut tx, pk)) = reveal_pk { + if let Some((mut rtx, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &tx_args, &pk) + signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &tx_args, &pk) .await?; // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &tx_args, tx).await?; + tx::process_tx(client, &mut ctx.wallet, &tx_args, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &tx_args, &mut tx, &pk, false) + .await; } signing::sign_tx(client, &mut ctx.wallet, &mut tx, &tx_args, &pk).await?; let result = tx::process_tx(client, &mut ctx.wallet, &tx_args, tx).await? @@ -521,7 +537,7 @@ pub async fn submit_transfer( .or_else(|| Some(ctx.config.ledger.chain_id.clone())); for _ in 0..2 { let arg = args.clone(); - let (mut tx, pk, tx_epoch) = + let (mut tx, pk, tx_epoch, isf) = tx::build_transfer(client, &mut ctx.wallet, &mut ctx.shielded, arg) .await?; // Build a transaction to reveal the signer of this transaction @@ -530,12 +546,16 @@ pub async fn submit_transfer( &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, ).await?; - if let Some((mut tx, pk)) = reveal_pk { + if let Some((mut rtx, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, isf) + .await; } signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) .await?; @@ -585,12 +605,16 @@ pub async fn submit_ibc_transfer( &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, ).await?; - if let Some((mut tx, pk)) = reveal_pk { + if let Some((mut rtx, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; } signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; @@ -757,12 +781,16 @@ pub async fn submit_init_proposal( &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, ).await?; - if let Some((mut tx, pk)) = reveal_pk { + if let Some((mut rtx, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; } signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) .await?; @@ -1031,12 +1059,16 @@ pub async fn submit_vote_proposal( &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, ).await?; - if let Some((mut tx, pk)) = reveal_pk { + if let Some((mut rtx, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; } signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) .await?; @@ -1139,12 +1171,16 @@ pub async fn submit_bond( &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, ).await?; - if let Some((mut tx, pk)) = reveal_pk { + if let Some((mut rtx, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; } signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; @@ -1168,16 +1204,20 @@ pub async fn submit_unbond( &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, ).await?; - if let Some((mut tx, pk)) = reveal_pk { + if let Some((mut rtx, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + #[cfg(not(feature = "mainnet"))] + // Update the stateful PoW challenge of the outer transaction + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; } signing::sign_tx(client, &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?; + tx::query_unbonds(client, args.clone(), latest_withdrawal_pre).await?; Ok(()) } @@ -1198,12 +1238,16 @@ pub async fn submit_withdraw( &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, ).await?; - if let Some((mut tx, pk)) = reveal_pk { + if let Some((mut rtx, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; } signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; @@ -1231,12 +1275,16 @@ pub async fn submit_validator_commission_change< &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, ).await?; - if let Some((mut tx, pk)) = reveal_pk { + if let Some((mut rtx, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; } signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; @@ -1263,12 +1311,16 @@ pub async fn submit_unjail_validator< &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, ).await?; - if let Some((mut tx, pk)) = reveal_pk { + if let Some((mut rtx, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; } signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; diff --git a/shared/src/ledger/signing.rs b/shared/src/ledger/signing.rs index dcc3efaa04..10c534261c 100644 --- a/shared/src/ledger/signing.rs +++ b/shared/src/ledger/signing.rs @@ -210,32 +210,24 @@ pub async fn sign_tx< Ok(()) } -/// 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, ->( +#[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< + C: crate::ledger::queries::Client + Sync, + >( 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 { - let fee_amount = if cfg!(feature = "mainnet") { - Amount::native_whole(MIN_FEE) - } else { - let wrapper_tx_fees_key = parameter_storage::get_wrapper_tx_fees_key(); - rpc::query_storage_value::( - client, - &wrapper_tx_fees_key, - ) + 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() - }; + .unwrap_or_default(); let fee_token = &args.fee_token; let source = Address::from(keypair); let balance_key = token::balance_key(fee_token, &source); @@ -256,34 +248,69 @@ pub async fn wrap_tx< ); } } - - #[cfg(not(feature = "mainnet"))] + let fee = Fee { amount: fee_amount, token: fee_token.clone() }; // A PoW solution can be used to allow zero-fee testnet transactions - let pow_solution: Option = { - // 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; + // 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) - } else { - None - } - }; + // 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< + C: crate::ledger::queries::Client + Sync, + >( + client: &C, + args: &args::Tx, + tx: &mut Tx, + keypair: &common::PublicKey, + requires_pow: bool, +) { + match &mut tx.header.tx_type { + TxType::Wrapper(wrapper) => { + 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 { - amount: fee_amount, - token: fee_token.clone(), - }, + fee, keypair.clone(), epoch, args.gas_limit.clone(), diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index b9e27847c2..a93f02de86 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -1238,7 +1238,7 @@ pub async fn build_transfer< wallet: &mut Wallet, shielded: &mut ShieldedContext, args: args::TxTransfer, -) -> Result<(Tx, common::PublicKey, Option), Error> { +) -> Result<(Tx, common::PublicKey, Option, bool), Error> { let source = args.source.effective_address(); let target = args.target.effective_address(); let token = args.token.clone(); @@ -1419,7 +1419,7 @@ pub async fn build_transfer< is_source_faucet, ) .await?; - Ok((tx, def_key, shielded_tx_epoch)) + Ok((tx, def_key, shielded_tx_epoch, is_source_faucet)) } /// Submit a transaction to initialize an account From b5cb0b169d12ff1e5b5527159f939ef92742c243 Mon Sep 17 00:00:00 2001 From: Murisi Tarusenga Date: Thu, 8 Jun 2023 10:22:05 +0200 Subject: [PATCH 05/12] Now only use TxBroadcastData::Wrapper for non dry runs. --- shared/src/ledger/tx.rs | 34 +++++++++++++++++----------------- tests/src/e2e/ledger_tests.rs | 16 +++------------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index a93f02de86..f350eccd7b 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -272,22 +272,6 @@ pub async fn process_tx< ) -> Result { // Remove all the sensitive sections tx.protocol_filter(); - // 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, - }; // NOTE: use this to print the request JSON body: // let request = @@ -299,8 +283,24 @@ 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 if args.broadcast_only { 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, ], From 5e37c1f618af29ca166418edf90dccb74c77d19a Mon Sep 17 00:00:00 2001 From: Murisi Tarusenga Date: Thu, 8 Jun 2023 16:25:57 +0200 Subject: [PATCH 06/12] Only reveal a public key when the signer is an implicit address. --- apps/src/lib/client/signing.rs | 6 +- apps/src/lib/client/tx.rs | 292 +++++++++++++++++++-------------- shared/src/ledger/signing.rs | 31 ++-- shared/src/ledger/tx.rs | 46 +++--- 4 files changed, 215 insertions(+), 160 deletions(-) diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index 2fae827702..489501e79f 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -38,7 +38,7 @@ pub async fn tx_signer< wallet: &mut Wallet, args: &args::Tx, default: TxSigningKey, -) -> Result { +) -> Result<(Option

, common::SecretKey), tx::Error> { namada::ledger::signing::tx_signer::(client, wallet, args, default) .await } @@ -55,14 +55,12 @@ pub async fn sign_tx< C: namada::ledger::queries::Client + Sync, U: WalletUtils, >( - client: &C, wallet: &mut Wallet, tx: &mut Tx, args: &args::Tx, default: &common::PublicKey, ) -> Result<(), tx::Error> { - namada::ledger::signing::sign_tx::( - client, + namada::ledger::signing::sign_tx( wallet, tx, args, diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 49296a78e7..443f42b1c7 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -56,17 +56,21 @@ pub async fn submit_custom( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, pk) = tx::build_custom(client, &mut ctx.wallet, args.clone()) - .await?; + let (mut tx, addr, pk) = + tx::build_custom(client, &mut ctx.wallet, args.clone()).await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; - if let Some((mut rtx, pk)) = reveal_pk { + let reveal_pk = if let Some(Address::Implicit(_)) = addr { + tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await? + } else { + None + }; + if let Some((mut rtx, _, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; @@ -75,7 +79,7 @@ pub async fn submit_custom( signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) .await; } - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).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(()) } @@ -89,17 +93,21 @@ pub async fn submit_update_vp( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, pk) = + let (mut tx, addr, pk) = tx::build_update_vp(client, &mut ctx.wallet, args.clone()).await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; - if let Some((mut rtx, pk)) = reveal_pk { + let reveal_pk = if let Some(Address::Implicit(_)) = addr { + tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await? + } else { + None + }; + if let Some((mut rtx, _, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; @@ -108,7 +116,7 @@ pub async fn submit_update_vp( signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) .await; } - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).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(()) } @@ -122,17 +130,21 @@ pub async fn submit_init_account( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, pk) = + let (mut tx, addr, pk) = tx::build_init_account(client, &mut ctx.wallet, args.clone()).await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; - if let Some((mut rtx, pk)) = reveal_pk { + let reveal_pk = if let Some(Address::Implicit(_)) = addr { + tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await? + } else { + None + }; + if let Some((mut rtx, _, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; @@ -141,7 +153,7 @@ pub async fn submit_init_account( signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) .await; } - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).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(()) } @@ -293,7 +305,7 @@ pub async fn submit_init_validator< tx.set_data(Data::new(data)); tx.set_code(Code::from_hash(tx_code_hash)); - let (mut tx, pk) = tx::prepare_tx( + let (mut tx, addr, pk) = tx::prepare_tx( client, &mut ctx.wallet, &tx_args, @@ -304,14 +316,18 @@ pub async fn submit_init_validator< ) .await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { tx: tx_args.clone(), public_key: pk.clone() }, - ).await?; - if let Some((mut rtx, pk)) = reveal_pk { + let reveal_pk = if let Some(Address::Implicit(_)) = addr { + tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: tx_args.clone(), public_key: pk.clone() }, + ).await? + } else { + None + }; + if let Some((mut rtx, _, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &tx_args, &pk) + signing::sign_tx(&mut ctx.wallet, &mut rtx, &tx_args, &pk) .await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &tx_args, rtx).await?; @@ -320,7 +336,7 @@ pub async fn submit_init_validator< signing::update_pow_challenge(client, &tx_args, &mut tx, &pk, false) .await; } - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &tx_args, &pk).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(); @@ -537,18 +553,22 @@ pub async fn submit_transfer( .or_else(|| Some(ctx.config.ledger.chain_id.clone())); for _ in 0..2 { let arg = args.clone(); - let (mut tx, pk, tx_epoch, isf) = + let (mut tx, addr, pk, tx_epoch, isf) = tx::build_transfer(client, &mut ctx.wallet, &mut ctx.shielded, arg) .await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; - if let Some((mut rtx, pk)) = reveal_pk { + let reveal_pk = if let Some(Address::Implicit(_)) = addr { + tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await? + } else { + None + }; + if let Some((mut rtx, _, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; @@ -557,7 +577,7 @@ pub async fn submit_transfer( signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, isf) .await; } - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + 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?; @@ -597,17 +617,21 @@ pub async fn submit_ibc_transfer( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, pk) = + let (mut tx, addr, pk) = tx::build_ibc_transfer(client, &mut ctx.wallet, args.clone()).await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; - if let Some((mut rtx, pk)) = reveal_pk { + let reveal_pk = if let Some(Address::Implicit(_)) = addr { + tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await? + } else { + None + }; + if let Some((mut rtx, _, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; @@ -616,7 +640,7 @@ pub async fn submit_ibc_transfer( signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) .await; } - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).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(()) } @@ -765,7 +789,7 @@ pub async fn submit_init_proposal( tx.set_data(Data::new(data)); tx.set_code(Code::from_hash(tx_code_hash)); - let (mut tx, pk) = tx::prepare_tx( + let (mut tx, addr, pk) = tx::prepare_tx( client, &mut ctx.wallet, &args.tx, @@ -776,14 +800,18 @@ pub async fn submit_init_proposal( ) .await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; - if let Some((mut rtx, pk)) = reveal_pk { + let reveal_pk = if let Some(Address::Implicit(_)) = addr { + tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await? + } else { + None + }; + if let Some((mut rtx, _, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; @@ -792,7 +820,7 @@ pub async fn submit_init_proposal( signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) .await; } - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk) .await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; Ok(()) @@ -1043,7 +1071,7 @@ pub async fn submit_vote_proposal( tx.set_data(Data::new(data)); tx.set_code(Code::from_hash(tx_code_hash)); - let (mut tx, pk) = tx::prepare_tx( + let (mut tx, addr, pk) = tx::prepare_tx( client, &mut ctx.wallet, &args.tx, @@ -1054,14 +1082,18 @@ pub async fn submit_vote_proposal( ) .await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; - if let Some((mut rtx, pk)) = reveal_pk { + let reveal_pk = if let Some(Address::Implicit(_)) = addr { + tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await? + } else { + None + }; + if let Some((mut rtx, _, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; @@ -1070,7 +1102,7 @@ pub async fn submit_vote_proposal( signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) .await; } - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk) .await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; Ok(()) @@ -1097,8 +1129,8 @@ pub async fn submit_reveal_pk( .or_else(|| Some(ctx.config.ledger.chain_id.clone())); 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(client, &mut ctx.wallet, &mut tx, &args.tx, &pk) + 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?; } @@ -1164,16 +1196,20 @@ pub async fn submit_bond( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, pk) = tx::build_bond::(client, &mut ctx.wallet, args.clone()).await?; + let (mut tx, addr, pk) = tx::build_bond::(client, &mut ctx.wallet, args.clone()).await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; - if let Some((mut rtx, pk)) = reveal_pk { + let reveal_pk = if let Some(Address::Implicit(_)) = addr { + tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await? + } else { + None + }; + if let Some((mut rtx, _, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; @@ -1182,7 +1218,7 @@ pub async fn submit_bond( signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) .await; } - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).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(()) } @@ -1196,17 +1232,21 @@ pub async fn submit_unbond( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, pk, latest_withdrawal_pre) = + let (mut tx, addr, pk, latest_withdrawal_pre) = tx::build_unbond(client, &mut ctx.wallet, args.clone()).await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; - if let Some((mut rtx, pk)) = reveal_pk { + let reveal_pk = if let Some(Address::Implicit(_)) = addr { + tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await? + } else { + None + }; + if let Some((mut rtx, _, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; @@ -1215,7 +1255,7 @@ pub async fn submit_unbond( signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) .await; } - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).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(()) @@ -1230,17 +1270,21 @@ pub async fn submit_withdraw( .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, pk) = tx::build_withdraw(client, &mut ctx.wallet, args.clone()) + let (mut tx, addr, pk) = tx::build_withdraw(client, &mut ctx.wallet, args.clone()) .await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; - if let Some((mut rtx, pk)) = reveal_pk { + let reveal_pk = if let Some(Address::Implicit(_)) = addr { + tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await? + } else { + None + }; + if let Some((mut rtx, _, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; @@ -1249,7 +1293,7 @@ pub async fn submit_withdraw( signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) .await; } - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).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(()) } @@ -1266,18 +1310,22 @@ pub async fn submit_validator_commission_change< .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); let arg = args.clone(); - let (mut tx, pk) = + let (mut tx, addr, pk) = tx::build_validator_commission_change(client, &mut ctx.wallet, arg) .await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; - if let Some((mut rtx, pk)) = reveal_pk { + let reveal_pk = if let Some(Address::Implicit(_)) = addr { + tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await? + } else { + None + }; + if let Some((mut rtx, _, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; @@ -1286,7 +1334,7 @@ pub async fn submit_validator_commission_change< signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) .await; } - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).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(()) } @@ -1302,18 +1350,22 @@ pub async fn submit_unjail_validator< .tx .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())); - let (mut tx, pk) = + let (mut tx, addr, pk) = tx::build_unjail_validator(client, &mut ctx.wallet, args.clone()) .await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; - if let Some((mut rtx, pk)) = reveal_pk { + let reveal_pk = if let Some(Address::Implicit(_)) = addr { + tx::build_reveal_pk( + client, + &mut ctx.wallet, + args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, + ).await? + } else { + None + }; + if let Some((mut rtx, _, pk)) = reveal_pk { // Sign the reveal public key transaction with the fee payer - signing::sign_tx(client, &mut ctx.wallet, &mut rtx, &args.tx, &pk) + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk) .await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; @@ -1322,7 +1374,7 @@ pub async fn submit_unjail_validator< signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) .await; } - signing::sign_tx(client, &mut ctx.wallet, &mut tx, &args.tx, &pk).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(()) } diff --git a/shared/src/ledger/signing.rs b/shared/src/ledger/signing.rs index 10c534261c..4c8ef5d058 100644 --- a/shared/src/ledger/signing.rs +++ b/shared/src/ledger/signing.rs @@ -134,13 +134,13 @@ pub async fn tx_signer< wallet: &mut Wallet, args: &args::Tx, default: TxSigningKey, -) -> Result { - let default = if args.dry_run { +) -> Result<(Option
, common::SecretKey), 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(signing_key.clone()); + return Ok((None, signing_key.clone())); } else if let Some(signer) = &args.signer { // Otherwise use the signer address provided by user TxSigningKey::WalletAddress(signer.clone()) @@ -149,21 +149,18 @@ pub async fn tx_signer< default }; // Now actually fetch the signing key and apply it - match default { + match signer { TxSigningKey::WalletAddress(signer) if signer == masp() => { - Ok(masp_tx_key()) - }, - TxSigningKey::WalletAddress(signer) if signer == (&masp_tx_key().ref_to()).into() => { - Ok(masp_tx_key()) + Ok((None, masp_tx_key())) }, TxSigningKey::WalletAddress(signer) => { - find_keypair::( + Ok((Some(signer.clone()), find_keypair::( client, wallet, &signer, args.password.clone(), ) - .await + .await?)) } TxSigningKey::None => other_err( "All transactions must be signed; please either specify the key \ @@ -182,16 +179,24 @@ pub async fn tx_signer< /// /// 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, wallet: &mut Wallet, tx: &mut Tx, args: &args::Tx, keypair: &common::PublicKey, ) -> Result<(), Error> { - let keypair = tx_signer::(client, wallet, args, TxSigningKey::WalletAddress(keypair.into())).await?; + let keypair = if *keypair == masp_tx_key().ref_to() { + masp_tx_key() + } else { + 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 + )) + })? + }; // Sign over the transacttion data tx.add_section(Section::Signature(Signature::new( tx.data_sechash(), diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index f350eccd7b..d6de40e793 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -233,16 +233,15 @@ pub async fn prepare_tx< tx: Tx, default_signer: TxSigningKey, #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) -> Result<(Tx, common::PublicKey), Error> { - let keypair = tx_signer::( +) -> Result<(Tx, Option
, common::PublicKey), Error> { + let (signer_addr, signer_pk) = tx_signer::( client, wallet, args, default_signer.clone(), - ).await? - .ref_to(); + ).await?; if args.dry_run { - Ok((tx, keypair)) + Ok((tx, signer_addr, signer_pk.ref_to())) } else { let epoch = rpc::query_epoch(client).await; Ok((wrap_tx( @@ -251,11 +250,11 @@ pub async fn prepare_tx< args, epoch, tx.clone(), - &keypair, + &signer_pk.ref_to(), #[cfg(not(feature = "mainnet"))] requires_pow, ) - .await, keypair)) + .await, signer_addr, signer_pk.ref_to())) } } @@ -326,7 +325,7 @@ pub async fn build_reveal_pk< client: &C, wallet: &mut Wallet, args: args::RevealPk, -) -> Result, Error> { +) -> Result, common::PublicKey)>, Error> { let args::RevealPk { tx: args, public_key, @@ -373,7 +372,7 @@ pub async fn build_reveal_pk_aux< wallet: &mut Wallet, public_key: &common::PublicKey, args: &args::Tx, -) -> Result<(Tx, common::PublicKey), Error> { +) -> 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)?; @@ -595,7 +594,7 @@ pub async fn build_validator_commission_change< client: &C, wallet: &mut Wallet, args: args::TxCommissionRateChange, -) -> Result<(Tx, common::PublicKey), Error> { +) -> Result<(Tx, Option
, common::PublicKey), Error> { let epoch = rpc::query_epoch(client).await; let tx_code_hash = @@ -689,7 +688,7 @@ pub async fn build_unjail_validator< client: &C, wallet: &mut Wallet, args: args::TxUnjailValidator, -) -> Result<(Tx, common::PublicKey), Error> { +) -> 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 { @@ -734,7 +733,7 @@ pub async fn build_withdraw< client: &C, wallet: &mut Wallet, args: args::Withdraw, -) -> Result<(Tx, common::PublicKey), Error> { +) -> Result<(Tx, Option
, common::PublicKey), Error> { let epoch = rpc::query_epoch(client).await; let validator = @@ -805,7 +804,7 @@ pub async fn build_unbond< client: &C, wallet: &mut Wallet, args: args::Unbond, -) -> Result<(Tx, common::PublicKey, Option<(Epoch, token::Amount)>), 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()); @@ -870,7 +869,7 @@ pub async fn build_unbond< tx.set_code(Code::from_hash(tx_code_hash)); let default_signer = args.source.unwrap_or_else(|| args.validator.clone()); - let (tx, default_signer) = prepare_tx::( + let (tx, signer_addr, default_signer) = prepare_tx::( client, wallet, &args.tx, @@ -881,7 +880,7 @@ pub async fn build_unbond< ) .await?; - Ok((tx, default_signer, latest_withdrawal_pre)) + Ok((tx, signer_addr, default_signer, latest_withdrawal_pre)) } /// Query the unbonds post-tx @@ -954,7 +953,7 @@ pub async fn build_bond< client: &C, wallet: &mut Wallet, args: args::Bond, -) -> Result<(Tx, common::PublicKey), Error> { +) -> Result<(Tx, Option
, common::PublicKey), Error> { let validator = known_validator_or_err(args.validator.clone(), args.tx.force, client) .await?; @@ -1052,7 +1051,7 @@ pub async fn build_ibc_transfer< client: &C, wallet: &mut Wallet, args: args::TxIbcTransfer, -) -> Result<(Tx, common::PublicKey), 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) @@ -1238,7 +1237,7 @@ pub async fn build_transfer< wallet: &mut Wallet, shielded: &mut ShieldedContext, args: args::TxTransfer, -) -> Result<(Tx, common::PublicKey, Option, bool), 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(); @@ -1320,6 +1319,7 @@ pub async fn build_transfer< let chosen_signer = tx_signer::(client, wallet, &args.tx, default_signer.clone()) .await? + .1 .ref_to(); let shielded_gas = masp_tx_key().ref_to() == chosen_signer; // Determine whether to pin this transaction to a storage key @@ -1409,7 +1409,7 @@ pub async fn build_transfer< tx.set_code(Code::from_hash(tx_code_hash)); // Dry-run/broadcast/submit the transaction - let (tx, def_key) = prepare_tx::( + let (tx, signer_addr, def_key) = prepare_tx::( client, wallet, &args.tx, @@ -1419,7 +1419,7 @@ pub async fn build_transfer< is_source_faucet, ) .await?; - Ok((tx, def_key, shielded_tx_epoch, is_source_faucet)) + Ok((tx, signer_addr, def_key, shielded_tx_epoch, is_source_faucet)) } /// Submit a transaction to initialize an account @@ -1430,7 +1430,7 @@ pub async fn build_init_account< client: &C, wallet: &mut Wallet, args: args::TxInitAccount, -) -> Result<(Tx, common::PublicKey), Error> { +) -> Result<(Tx, Option
, common::PublicKey), Error> { let public_key = args.public_key; let vp_code_hash = @@ -1478,7 +1478,7 @@ pub async fn build_update_vp< client: &C, wallet: &mut Wallet, args: args::TxUpdateVp, -) -> Result<(Tx, common::PublicKey), Error> { +) -> Result<(Tx, Option
, common::PublicKey), Error> { let addr = args.addr.clone(); // Check that the address is established and exists on chain @@ -1566,7 +1566,7 @@ pub async fn build_custom< client: &C, wallet: &mut Wallet, args: args::TxCustom, -) -> Result<(Tx, common::PublicKey), 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; From 826b628b8b3f568885882337792747e396a2904c Mon Sep 17 00:00:00 2001 From: Murisi Tarusenga Date: Thu, 8 Jun 2023 17:28:22 +0200 Subject: [PATCH 07/12] Increased usage of PublicKeys relative to SecretKeys in tx construction. --- apps/src/lib/client/signing.rs | 8 ++--- apps/src/lib/client/tx.rs | 48 ++++++-------------------- shared/src/ledger/signing.rs | 62 +++++++++++++++++++--------------- shared/src/ledger/tx.rs | 11 +++--- 4 files changed, 53 insertions(+), 76 deletions(-) diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index 489501e79f..402a8432c5 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -14,15 +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) +) -> Result { + namada::ledger::signing::find_pk(client, wallet, addr, None) .await } @@ -38,7 +38,7 @@ pub async fn tx_signer< wallet: &mut Wallet, args: &args::Tx, default: TxSigningKey, -) -> Result<(Option
, common::SecretKey), tx::Error> { +) -> Result<(Option
, common::PublicKey), tx::Error> { namada::ledger::signing::tx_signer::(client, wallet, args, default) .await } diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 443f42b1c7..f4b9547030 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -37,7 +37,7 @@ 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; @@ -344,40 +344,12 @@ pub async fn submit_init_validator< 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"); @@ -718,9 +690,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 @@ -921,9 +893,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, diff --git a/shared/src/ledger/signing.rs b/shared/src/ledger/signing.rs index 4c8ef5d058..ec8b395a3a 100644 --- a/shared/src/ledger/signing.rs +++ b/shared/src/ledger/signing.rs @@ -66,7 +66,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, >( @@ -74,36 +74,29 @@ 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( + 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| { + 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.", @@ -112,6 +105,29 @@ 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() { + Ok(masp_tx_key()) + } else if args.signing_key.as_ref().map(|x| x.ref_to() == *keypair).unwrap_or(false) { + Ok(args.signing_key.clone().unwrap()) + } else { + 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)] @@ -134,13 +150,13 @@ pub async fn tx_signer< wallet: &mut Wallet, args: &args::Tx, default: TxSigningKey, -) -> Result<(Option
, common::SecretKey), Error> { +) -> 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.clone())); + return Ok((None, signing_key.ref_to())); } else if let Some(signer) = &args.signer { // Otherwise use the signer address provided by user TxSigningKey::WalletAddress(signer.clone()) @@ -151,10 +167,10 @@ pub async fn tx_signer< // Now actually fetch the signing key and apply it match signer { TxSigningKey::WalletAddress(signer) if signer == masp() => { - Ok((None, masp_tx_key())) + Ok((None, masp_tx_key().ref_to())) }, TxSigningKey::WalletAddress(signer) => { - Ok((Some(signer.clone()), find_keypair::( + Ok((Some(signer.clone()), find_pk::( client, wallet, &signer, @@ -186,17 +202,7 @@ pub async fn sign_tx< args: &args::Tx, keypair: &common::PublicKey, ) -> Result<(), Error> { - let keypair = if *keypair == masp_tx_key().ref_to() { - masp_tx_key() - } else { - 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 - )) - })? - }; + let keypair = find_key_by_pk(wallet, args, keypair)?; // Sign over the transacttion data tx.add_section(Section::Signature(Signature::new( tx.data_sechash(), diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index d6de40e793..d744312c19 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -38,7 +38,7 @@ 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, wrap_tx}; +use crate::ledger::signing::{find_keypair, sign_tx, tx_signer, TxSigningKey, wrap_tx, find_pk}; use crate::ledger::wallet::{Wallet, WalletUtils}; use crate::proto::{Code, Data, MaspBuilder, Section, Signature, Tx}; use crate::tendermint_rpc::endpoint::broadcast::tx_sync::Response; @@ -241,7 +241,7 @@ pub async fn prepare_tx< default_signer.clone(), ).await?; if args.dry_run { - Ok((tx, signer_addr, signer_pk.ref_to())) + Ok((tx, signer_addr, signer_pk)) } else { let epoch = rpc::query_epoch(client).await; Ok((wrap_tx( @@ -250,11 +250,11 @@ pub async fn prepare_tx< args, epoch, tx.clone(), - &signer_pk.ref_to(), + &signer_pk, #[cfg(not(feature = "mainnet"))] requires_pow, ) - .await, signer_addr, signer_pk.ref_to())) + .await, signer_addr, signer_pk)) } } @@ -1319,8 +1319,7 @@ pub async fn build_transfer< let chosen_signer = tx_signer::(client, wallet, &args.tx, default_signer.clone()) .await? - .1 - .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 { From 0d0f3723f30a1b09546a876b1a05a819ec27dca9 Mon Sep 17 00:00:00 2001 From: Murisi Tarusenga Date: Fri, 9 Jun 2023 12:21:01 +0200 Subject: [PATCH 08/12] Allow Tx builders to take verification keys. --- apps/src/lib/cli.rs | 21 +++++++++++++++++++-- shared/src/ledger/args.rs | 2 ++ shared/src/ledger/signing.rs | 5 +++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 0a88306f18..f3f5a64e0a 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1935,6 +1935,7 @@ 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"); @@ -3340,6 +3341,7 @@ 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, @@ -3403,7 +3405,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 @@ -3412,7 +3415,19 @@ 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(SIGNING_KEY_OPT.name) + .conflicts_with(VERIFICATION_KEY.name), + ) + .arg( + VERIFICATION_KEY + .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(SIGNING_KEY_OPT.name), ) .arg(CHAIN_ID_OPT.def().help("The chain ID.")) } @@ -3431,6 +3446,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); @@ -3448,6 +3464,7 @@ pub mod args { gas_limit, expiration, signing_key, + verification_key, signer, tx_reveal_code_path, password, diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 0ad90faf0a..d042a31f1b 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 ec8b395a3a..99c218b2ed 100644 --- a/shared/src/ledger/signing.rs +++ b/shared/src/ledger/signing.rs @@ -114,10 +114,13 @@ pub fn find_key_by_pk( 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 \ @@ -157,6 +160,8 @@ pub async fn tx_signer< } 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()) From e4e58efdf076090ab2f958e1165f213f9efd7467 Mon Sep 17 00:00:00 2001 From: Murisi Tarusenga Date: Fri, 9 Jun 2023 13:49:07 +0200 Subject: [PATCH 09/12] Simplified the reveal PK transaction construction flow. Factored chain_id reading from submit functions. --- apps/src/lib/cli.rs | 3 +- apps/src/lib/client/tx.rs | 501 +++++++++++++++----------------------- 2 files changed, 197 insertions(+), 307 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index f3f5a64e0a..84967a9b81 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -3346,7 +3346,8 @@ pub mod args { 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())), } } } diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index f4b9547030..2b69d1b154 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -50,34 +50,28 @@ use crate::wallet::{ 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())); let (mut tx, addr, pk) = tx::build_custom(client, &mut ctx.wallet, args.clone()).await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = if let Some(Address::Implicit(_)) = addr { - tx::build_reveal_pk( + if let Some(Address::Implicit(_)) = addr { + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await? - } else { - None - }; - 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.tx, &pk) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + ).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.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; + } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; @@ -87,34 +81,28 @@ pub async fn submit_custom( 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())); let (mut tx, addr, pk) = tx::build_update_vp(client, &mut ctx.wallet, args.clone()).await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = if let Some(Address::Implicit(_)) = addr { - tx::build_reveal_pk( + if let Some(Address::Implicit(_)) = addr { + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await? - } else { - None - }; - 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.tx, &pk) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + ).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.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; + } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; @@ -124,34 +112,28 @@ pub async fn submit_update_vp( 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())); let (mut tx, addr, pk) = tx::build_init_account(client, &mut ctx.wallet, args.clone()).await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = if let Some(Address::Implicit(_)) = addr { - tx::build_reveal_pk( + if let Some(Address::Implicit(_)) = addr { + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await? - } else { - None - }; - 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.tx, &pk) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + ).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.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; + } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; @@ -177,13 +159,6 @@ pub async fn submit_init_validator< tx_code_path: _, }: args::TxInitValidator, ) -> Result<(), tx::Error> { - let tx_args = args::Tx { - chain_id: tx_args - .clone() - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())), - ..tx_args.clone() - }; let alias = tx_args .initialized_account_alias .as_ref() @@ -316,25 +291,23 @@ pub async fn submit_init_validator< ) .await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = if let Some(Address::Implicit(_)) = addr { - tx::build_reveal_pk( + if let Some(Address::Implicit(_)) = addr { + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, args::RevealPk { tx: tx_args.clone(), public_key: pk.clone() }, - ).await? - } else { - None - }; - 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, &tx_args, &pk) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &tx_args, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &tx_args, &mut tx, &pk, false) - .await; + ).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, &tx_args, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &tx_args, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &tx_args, &mut tx, &pk, false) + .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? @@ -517,37 +490,31 @@ 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())); 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?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = if let Some(Address::Implicit(_)) = addr { - tx::build_reveal_pk( + if let Some(Address::Implicit(_)) = addr { + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await? - } else { - None - }; - 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.tx, &pk) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, isf) - .await; + ).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.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, isf) + .await; + } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk) .await?; @@ -583,34 +550,28 @@ pub async fn submit_transfer( 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())); let (mut tx, addr, pk) = tx::build_ibc_transfer(client, &mut ctx.wallet, args.clone()).await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = if let Some(Address::Implicit(_)) = addr { - tx::build_reveal_pk( + if let Some(Address::Implicit(_)) = addr { + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await? - } else { - None - }; - 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.tx, &pk) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + ).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.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; + } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; @@ -620,12 +581,8 @@ pub async fn submit_ibc_transfer( 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"); @@ -772,25 +729,23 @@ pub async fn submit_init_proposal( ) .await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = if let Some(Address::Implicit(_)) = addr { - tx::build_reveal_pk( + if let Some(Address::Implicit(_)) = addr { + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await? - } else { - None - }; - 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.tx, &pk) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + ).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.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; + } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk) .await?; @@ -802,12 +757,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 { @@ -1054,25 +1005,23 @@ pub async fn submit_vote_proposal( ) .await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = if let Some(Address::Implicit(_)) = addr { - tx::build_reveal_pk( + if let Some(Address::Implicit(_)) = addr { + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await? - } else { - None - }; - 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.tx, &pk) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + ).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.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; + } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk) .await?; @@ -1093,12 +1042,8 @@ 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())); let reveal_tx = tx::build_reveal_pk(client, &mut ctx.wallet, args.clone()).await?; if let Some((mut tx, _, pk)) = reveal_tx { @@ -1162,33 +1107,27 @@ 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())); let (mut tx, addr, pk) = tx::build_bond::(client, &mut ctx.wallet, args.clone()).await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = if let Some(Address::Implicit(_)) = addr { - tx::build_reveal_pk( + if let Some(Address::Implicit(_)) = addr { + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await? - } else { - None - }; - 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.tx, &pk) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + ).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.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; + } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; @@ -1198,34 +1137,28 @@ pub async fn submit_bond( 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())); let (mut tx, addr, pk, latest_withdrawal_pre) = tx::build_unbond(client, &mut ctx.wallet, args.clone()).await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = if let Some(Address::Implicit(_)) = addr { - tx::build_reveal_pk( + if let Some(Address::Implicit(_)) = addr { + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await? - } else { - None - }; - 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.tx, &pk) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - #[cfg(not(feature = "mainnet"))] - // Update the stateful PoW challenge of the outer transaction - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + ).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.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + #[cfg(not(feature = "mainnet"))] + // Update the stateful PoW challenge of the outer transaction + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; + } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; @@ -1236,34 +1169,28 @@ pub async fn submit_unbond( 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())); let (mut tx, addr, pk) = tx::build_withdraw(client, &mut ctx.wallet, args.clone()) .await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = if let Some(Address::Implicit(_)) = addr { - tx::build_reveal_pk( + if let Some(Address::Implicit(_)) = addr { + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await? - } else { - None - }; - 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.tx, &pk) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + ).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.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; + } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; @@ -1275,36 +1202,30 @@ pub async fn submit_validator_commission_change< >( client: &C, mut ctx: Context, - mut args: args::TxCommissionRateChange, + args: args::TxCommissionRateChange, ) -> Result<(), tx::Error> { - args.tx.chain_id = args - .tx - .chain_id - .or_else(|| Some(ctx.config.ledger.chain_id.clone())); let arg = args.clone(); let (mut tx, addr, pk) = tx::build_validator_commission_change(client, &mut ctx.wallet, arg) .await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = if let Some(Address::Implicit(_)) = addr { - tx::build_reveal_pk( + if let Some(Address::Implicit(_)) = addr { + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await? - } else { - None - }; - 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.tx, &pk) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + ).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.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .await; + } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; @@ -1316,67 +1237,35 @@ 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())); let (mut tx, addr, pk) = tx::build_unjail_validator(client, &mut ctx.wallet, args.clone()) .await?; // Build a transaction to reveal the signer of this transaction - let reveal_pk = if let Some(Address::Implicit(_)) = addr { - tx::build_reveal_pk( + if let Some(Address::Implicit(_)) = addr { + let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await? - } else { - None - }; - 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.tx, &pk) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + ).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.tx, &pk) + .await?; + // Submit the reveal public key transaction first + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) + .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(()) } -/// 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, -) -> 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, - ) - .await? - .initialized_accounts(); - Ok((ctx, res)) -} - /// Save accounts initialized from a tx into the wallet, if any. pub async fn save_initialized_accounts( wallet: &mut Wallet, From 6c25dde316ac3232e8c2b0f1758382b1e69df326 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Wed, 5 Jul 2023 18:46:23 +0200 Subject: [PATCH 10/12] clippy, fmt --- apps/src/lib/cli.rs | 30 ++-- apps/src/lib/client/signing.rs | 11 +- apps/src/lib/client/tx.rs | 275 +++++++++++++++++++++------------ proof_of_stake/src/lib.rs | 8 +- shared/src/ledger/signing.rs | 114 +++++++------- shared/src/ledger/tx.rs | 194 +++++++++++++++-------- 6 files changed, 385 insertions(+), 247 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 84967a9b81..8380aa84e8 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1935,7 +1935,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 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"); @@ -3341,12 +3342,15 @@ 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)), + 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())), } } @@ -3419,16 +3423,16 @@ pub mod args { .conflicts_with(SIGNING_KEY_OPT.name) .conflicts_with(VERIFICATION_KEY.name), ) - .arg( - VERIFICATION_KEY - .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(SIGNING_KEY_OPT.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.")) } diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index 402a8432c5..4ae12c6b17 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -22,8 +22,7 @@ pub async fn find_pk< wallet: &mut Wallet, addr: &Address, ) -> Result { - namada::ledger::signing::find_pk(client, wallet, addr, None) - .await + namada::ledger::signing::find_pk(client, wallet, addr, None).await } /// Given CLI arguments and some defaults, determine the rightful transaction @@ -60,13 +59,7 @@ pub async fn sign_tx< args: &args::Tx, default: &common::PublicKey, ) -> Result<(), tx::Error> { - namada::ledger::signing::sign_tx( - wallet, - tx, - args, - default, - ) - .await + 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 2b69d1b154..a206345af2 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, signing}; +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; @@ -43,9 +41,7 @@ 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}; pub async fn submit_custom( client: &C, @@ -59,18 +55,23 @@ pub async fn submit_custom( let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; + args::RevealPk { + tx: args.tx.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.tx, &pk) - .await?; + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk).await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; // Update the stateful PoW challenge of the outer transaction #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + signing::update_pow_challenge( + client, &args.tx, &mut tx, &pk, false, + ) + .await; } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; @@ -90,18 +91,23 @@ pub async fn submit_update_vp( let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; + args::RevealPk { + tx: args.tx.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.tx, &pk) - .await?; + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk).await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; // Update the stateful PoW challenge of the outer transaction #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + signing::update_pow_challenge( + client, &args.tx, &mut tx, &pk, false, + ) + .await; } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; @@ -121,18 +127,23 @@ pub async fn submit_init_account( let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; + args::RevealPk { + tx: args.tx.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.tx, &pk) - .await?; + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk).await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; // Update the stateful PoW challenge of the outer transaction #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + signing::update_pow_challenge( + client, &args.tx, &mut tx, &pk, false, + ) + .await; } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; @@ -289,35 +300,41 @@ pub async fn submit_init_validator< #[cfg(not(feature = "mainnet"))] false, ) - .await?; + .await?; // Build a transaction to reveal the signer of this transaction if let Some(Address::Implicit(_)) = addr { let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - args::RevealPk { tx: tx_args.clone(), public_key: pk.clone() }, - ).await?; + args::RevealPk { + tx: 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, &tx_args, &pk) - .await?; + signing::sign_tx(&mut ctx.wallet, &mut rtx, &tx_args, &pk).await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &tx_args, rtx).await?; // Update the stateful PoW challenge of the outer transaction #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &tx_args, &mut tx, &pk, false) - .await; + signing::update_pow_challenge( + client, &tx_args, &mut tx, &pk, false, + ) + .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? + 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] => { - if let Some(alias) = ctx.wallet.find_alias(validator_address) { + if let Some(alias) = ctx.wallet.find_alias(validator_address) { (alias.clone(), validator_address.clone()) } else { eprintln!("Expected one account to be created"); @@ -496,14 +513,18 @@ pub async fn submit_transfer( let arg = args.clone(); let (mut tx, addr, pk, tx_epoch, isf) = tx::build_transfer(client, &mut ctx.wallet, &mut ctx.shielded, arg) - .await?; + .await?; // Build a transaction to reveal the signer of this transaction if let Some(Address::Implicit(_)) = addr { let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; + args::RevealPk { + tx: args.tx.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.tx, &pk) @@ -512,14 +533,15 @@ pub async fn submit_transfer( tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; // Update the stateful PoW challenge of the outer transaction #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, isf) - .await; + signing::update_pow_challenge( + client, &args.tx, &mut tx, &pk, isf, + ) + .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?; + 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; @@ -559,18 +581,23 @@ pub async fn submit_ibc_transfer( let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; + args::RevealPk { + tx: args.tx.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.tx, &pk) - .await?; + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk).await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; // Update the stateful PoW challenge of the outer transaction #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + signing::update_pow_challenge( + client, &args.tx, &mut tx, &pk, false, + ) + .await; } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; @@ -727,14 +754,18 @@ pub async fn submit_init_proposal( #[cfg(not(feature = "mainnet"))] false, ) - .await?; + .await?; // Build a transaction to reveal the signer of this transaction if let Some(Address::Implicit(_)) = addr { let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; + args::RevealPk { + tx: args.tx.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.tx, &pk) @@ -743,12 +774,13 @@ pub async fn submit_init_proposal( tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; // Update the stateful PoW challenge of the outer transaction #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + signing::update_pow_challenge( + client, &args.tx, &mut tx, &pk, false, + ) + .await; } } - signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk) - .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(()) } @@ -1003,24 +1035,38 @@ pub async fn submit_vote_proposal( #[cfg(not(feature = "mainnet"))] false, ) - .await?; + .await?; // Build a transaction to reveal the signer of this transaction if let Some(Address::Implicit(_)) = addr { let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; + args::RevealPk { + tx: args.tx.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.tx, &pk) - .await?; + // Sign the reveal public key transaction with the fee + // payer + signing::sign_tx( + &mut ctx.wallet, + &mut rtx, + &args.tx, + &pk, + ) + .await?; // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction + tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx) + .await?; + // Update the stateful PoW challenge of the outer + // transaction #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + signing::update_pow_challenge( + client, &args.tx, &mut tx, &pk, false, + ) + .await; } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk) @@ -1047,8 +1093,7 @@ pub async fn submit_reveal_pk( 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?; + signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; } Ok(()) @@ -1109,24 +1154,30 @@ pub async fn submit_bond( ctx: &mut Context, args: args::Bond, ) -> Result<(), tx::Error> { - let (mut tx, addr, pk) = tx::build_bond::(client, &mut ctx.wallet, args.clone()).await?; + let (mut tx, addr, pk) = + tx::build_bond::(client, &mut ctx.wallet, args.clone()).await?; // Build a transaction to reveal the signer of this transaction if let Some(Address::Implicit(_)) = addr { let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; + args::RevealPk { + tx: args.tx.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.tx, &pk) - .await?; + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk).await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; // Update the stateful PoW challenge of the outer transaction #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + signing::update_pow_challenge( + client, &args.tx, &mut tx, &pk, false, + ) + .await; } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; @@ -1146,18 +1197,23 @@ pub async fn submit_unbond( let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; + args::RevealPk { + tx: args.tx.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.tx, &pk) - .await?; + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk).await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; #[cfg(not(feature = "mainnet"))] // Update the stateful PoW challenge of the outer transaction - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + signing::update_pow_challenge( + client, &args.tx, &mut tx, &pk, false, + ) + .await; } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; @@ -1171,25 +1227,30 @@ pub async fn submit_withdraw( mut ctx: Context, args: args::Withdraw, ) -> Result<(), tx::Error> { - let (mut tx, addr, pk) = tx::build_withdraw(client, &mut ctx.wallet, args.clone()) - .await?; + let (mut tx, addr, pk) = + tx::build_withdraw(client, &mut ctx.wallet, args.clone()).await?; // Build a transaction to reveal the signer of this transaction if let Some(Address::Implicit(_)) = addr { let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; + args::RevealPk { + tx: args.tx.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.tx, &pk) - .await?; + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk).await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; // Update the stateful PoW challenge of the outer transaction #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + signing::update_pow_challenge( + client, &args.tx, &mut tx, &pk, false, + ) + .await; } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; @@ -1207,24 +1268,29 @@ pub async fn submit_validator_commission_change< let arg = args.clone(); let (mut tx, addr, pk) = tx::build_validator_commission_change(client, &mut ctx.wallet, arg) - .await?; + .await?; // Build a transaction to reveal the signer of this transaction if let Some(Address::Implicit(_)) = addr { let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; + args::RevealPk { + tx: args.tx.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.tx, &pk) - .await?; + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk).await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; // Update the stateful PoW challenge of the outer transaction #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + signing::update_pow_challenge( + client, &args.tx, &mut tx, &pk, false, + ) + .await; } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; @@ -1241,24 +1307,29 @@ pub async fn submit_unjail_validator< ) -> Result<(), tx::Error> { let (mut tx, addr, pk) = tx::build_unjail_validator(client, &mut ctx.wallet, args.clone()) - .await?; + .await?; // Build a transaction to reveal the signer of this transaction if let Some(Address::Implicit(_)) = addr { let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, - args::RevealPk { tx: args.tx.clone(), public_key: pk.clone() }, - ).await?; + args::RevealPk { + tx: args.tx.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.tx, &pk) - .await?; + signing::sign_tx(&mut ctx.wallet, &mut rtx, &args.tx, &pk).await?; // Submit the reveal public key transaction first tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; // Update the stateful PoW challenge of the outer transaction #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, &args.tx, &mut tx, &pk, false) - .await; + signing::update_pow_challenge( + client, &args.tx, &mut tx, &pk, false, + ) + .await; } } signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 4e8cbb560f..9815520f72 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::{ diff --git a/shared/src/ledger/signing.rs b/shared/src/ledger/signing.rs index 99c218b2ed..cbd05d9959 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, masp_tx_key, 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, @@ -81,23 +83,24 @@ pub async fn find_pk< "Looking-up public key of {} from the ledger...", addr.encode() ); - 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() - )), - ) + ), + )) } - Address::Implicit(ImplicitAddress(pkh)) => { - Ok(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()) - } + })? + .ref_to()), Address::Internal(_) => other_err(format!( "Internal address {} doesn't have any signing keys.", addr @@ -116,18 +119,25 @@ pub fn find_key_by_pk( 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) { + } 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 - )) - }) + 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 + )) + }) } } @@ -173,16 +183,12 @@ pub async fn tx_signer< match signer { TxSigningKey::WalletAddress(signer) if signer == masp() => { Ok((None, masp_tx_key().ref_to())) - }, - TxSigningKey::WalletAddress(signer) => { - Ok((Some(signer.clone()), find_pk::( - client, - wallet, - &signer, - args.password.clone(), - ) - .await?)) } + 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." @@ -199,9 +205,7 @@ 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< - U: WalletUtils, ->( +pub async fn sign_tx( wallet: &mut Wallet, tx: &mut Tx, args: &args::Tx, @@ -229,9 +233,7 @@ pub async fn sign_tx< #[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< - C: crate::ledger::queries::Client + Sync, - >( +pub async fn solve_pow_challenge( client: &C, args: &args::Tx, keypair: &common::PublicKey, @@ -242,8 +244,8 @@ pub async fn solve_pow_challenge< client, &wrapper_tx_fees_key, ) - .await - .unwrap_or_default(); + .await + .unwrap_or_default(); let fee_token = &args.fee_token; let source = Address::from(keypair); let balance_key = token::balance_key(fee_token, &source); @@ -253,28 +255,31 @@ pub async fn solve_pow_challenge< .unwrap_or_default(); let is_bal_sufficient = fee_amount <= balance; if !is_bal_sufficient { - eprintln!( + 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 {fee_amount}, got {balance}." + 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!( - "The wrapper transaction source doesn't have enough balance \ - to pay fee {fee_amount}, got {balance}." - ); + panic!("{}", err_msg); } } - let fee = Fee { amount: fee_amount, token: fee_token.clone() }; + 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." - ); + 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; + let challenge = rpc::get_testnet_pow_challenge(client, source).await; // Solve the solution, this blocks until a solution is found let solution = challenge.solve(); @@ -286,23 +291,18 @@ pub async fn solve_pow_challenge< #[cfg(not(feature = "mainnet"))] /// Update the PoW challenge inside the given transaction -pub async fn update_pow_challenge< - C: crate::ledger::queries::Client + Sync, - >( +pub async fn update_pow_challenge( client: &C, args: &args::Tx, tx: &mut Tx, keypair: &common::PublicKey, requires_pow: bool, ) { - match &mut tx.header.tx_type { - TxType::Wrapper(wrapper) => { - let (pow_solution, fee) = - solve_pow_challenge(client, args, keypair, requires_pow).await; - wrapper.fee = fee; - wrapper.pow_solution = pow_solution; - }, - _ => {}, + 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; } } diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index d744312c19..c63da9f62c 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; @@ -38,9 +37,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, wrap_tx, find_pk}; +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::hash::Hash; @@ -160,7 +159,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), @@ -224,9 +229,9 @@ impl ProcessTxResponse { /// 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, + C: crate::ledger::queries::Client + Sync, U: WalletUtils, - >( +>( client: &C, wallet: &mut Wallet, args: &args::Tx, @@ -234,34 +239,34 @@ pub async fn prepare_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?; + 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)) + 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, + C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( client: &C, @@ -303,14 +308,20 @@ pub async fn process_tx< // Either broadcast or submit transaction and collect result into // sum type if args.broadcast_only { - broadcast_tx(client, &to_broadcast).await.map(ProcessTxResponse::Broadcast) + broadcast_tx(client, &to_broadcast) + .await + .map(ProcessTxResponse::Broadcast) } else { match submit_tx(client, to_broadcast).await { Ok(x) => { - save_initialized_accounts::(wallet, &args, x.initialized_accounts.clone()) - .await; + save_initialized_accounts::( + wallet, + args, + x.initialized_accounts.clone(), + ) + .await; Ok(ProcessTxResponse::Applied(x)) - }, + } Err(x) => Err(x), } } @@ -337,7 +348,10 @@ pub async fn build_reveal_pk< Ok(None) } else { // If not, submit it - Ok(Some(build_reveal_pk_aux::(client, wallet, &public_key, &args).await?)) + Ok(Some( + build_reveal_pk_aux::(client, wallet, &public_key, &args) + .await?, + )) } } @@ -393,7 +407,7 @@ pub async fn build_reveal_pk_aux< prepare_tx::( client, wallet, - &args, + args, tx, TxSigningKey::WalletAddress(addr), #[cfg(not(feature = "mainnet"))] @@ -725,6 +739,52 @@ pub async fn build_unjail_validator< .await } +/// Submit transaction to unjail a jailed validator +pub async fn submit_unjail_validator< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::TxUnjailValidator, +) -> Result<(), 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?; + Ok(()) +} + /// Submit transaction to withdraw an unbond pub async fn build_withdraw< C: crate::ledger::queries::Client + Sync, @@ -804,7 +864,15 @@ pub async fn build_unbond< client: &C, wallet: &mut Wallet, args: args::Unbond, -) -> Result<(Tx, Option
, common::PublicKey, Option<(Epoch, token::Amount)>), 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()); @@ -892,7 +960,7 @@ pub async fn query_unbonds( 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) @@ -1236,8 +1304,9 @@ pub async fn build_transfer< client: &C, wallet: &mut Wallet, shielded: &mut ShieldedContext, - args: args::TxTransfer, -) -> Result<(Tx, Option
, common::PublicKey, Option, bool), Error> { + mut args: args::TxTransfer, +) -> 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 +1362,27 @@ pub async fn build_transfer< args.tx.force, client, ) - .await?; + .await?; let masp_addr = masp(); // For MASP sources, use a special sentinel key recognized by VPs as default // 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 (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()); + 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? - .1; + .await? + .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 { @@ -1332,8 +1395,8 @@ pub async fn build_transfer< let tx_code_hash = query_wasm_code_hash(client, args.tx_code_path.to_str().unwrap()) - .await - .unwrap(); + .await + .unwrap(); // Construct the shielded part of the transaction, if any let stx_result = shielded @@ -1344,11 +1407,11 @@ pub async fn build_transfer< Ok(stx) => Ok(stx), Err(builder::Error::InsufficientFunds(_)) => { Err(Error::NegativeBalanceAfterTransfer( - source.clone(), - args.amount, - token.clone(), - args.tx.fee_amount, - args.tx.fee_token.clone(), + 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()), )) } Err(err) => Err(Error::MaspError(err)), @@ -1368,8 +1431,7 @@ pub async fn build_transfer< Hash(masp_tx.hash(&mut Sha256::new()).finalize_reset().into()); // 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) + let asset_types = used_asset_types(shielded, client, &shielded_parts.0) .await .unwrap_or_default(); // Add the MASP Transaction's Builder to the Tx @@ -1393,7 +1455,7 @@ pub async fn build_transfer< target: target.clone(), token: token.clone(), sub_prefix: sub_prefix.clone(), - amount, + amount: validated_amount, key: key.clone(), // Link the Transfer to the MASP Transaction by hash code shielded: masp_hash, @@ -1417,8 +1479,14 @@ pub async fn build_transfer< #[cfg(not(feature = "mainnet"))] is_source_faucet, ) - .await?; - Ok((tx, signer_addr, def_key, shielded_tx_epoch, is_source_faucet)) + .await?; + Ok(( + tx, + signer_addr, + def_key, + shielded_tx_epoch, + is_source_faucet, + )) } /// Submit a transaction to initialize an account From d22f12354ff5318bef2c010d095a564fc209610d Mon Sep 17 00:00:00 2001 From: Murisi Tarusenga Date: Mon, 3 Jul 2023 11:49:01 +0200 Subject: [PATCH 11/12] Added changelog entry. --- .changelog/unreleased/improvements/1498-separate-signing.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/unreleased/improvements/1498-separate-signing.md 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 From 673ef1b837778d98facef3d2046ad667fe5aa39d Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Wed, 5 Jul 2023 19:13:23 +0200 Subject: [PATCH 12/12] added chagelog --- apps/src/lib/cli.rs | 10 +- apps/src/lib/client/tx.rs | 369 +++++++------------------------------- proof_of_stake/src/lib.rs | 5 - shared/src/ledger/args.rs | 2 +- shared/src/ledger/tx.rs | 2 +- 5 files changed, 67 insertions(+), 321 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 8380aa84e8..27052b1666 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -3103,11 +3103,11 @@ pub mod args { } } - impl CliToSdk> - for TxCommissionRateChange + impl CliToSdk> + for CommissionRateChange { - fn to_sdk(self, ctx: &mut Context) -> TxCommissionRateChange { - TxCommissionRateChange:: { + fn to_sdk(self, ctx: &mut Context) -> CommissionRateChange { + CommissionRateChange:: { tx: self.tx.to_sdk(ctx), validator: ctx.get(&self.validator), rate: self.rate, @@ -3116,7 +3116,7 @@ pub mod args { } } - impl Args for TxCommissionRateChange { + impl Args for CommissionRateChange { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let validator = VALIDATOR.parse(matches); diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index a206345af2..a64bdb034c 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -43,37 +43,46 @@ use crate::facade::tendermint_rpc::HttpClient; use crate::node::ledger::tendermint_node; use crate::wallet::{gen_validator_keys, read_and_confirm_encryption_password}; -pub async fn submit_custom( +// Build a transaction to reveal the signer of the given transaction. +pub async fn submit_reveal_aux( client: &C, ctx: &mut Context, - args: args::TxCustom, + args: &args::Tx, + addr: Option
, + pk: common::PublicKey, + tx: &mut Tx, ) -> Result<(), tx::Error> { - let (mut tx, addr, pk) = - tx::build_custom(client, &mut ctx.wallet, args.clone()).await?; - // Build a transaction to reveal the signer of this transaction if let Some(Address::Implicit(_)) = addr { let reveal_pk = tx::build_reveal_pk( client, &mut ctx.wallet, args::RevealPk { - tx: args.tx.clone(), + 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.tx, &pk).await?; + 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.tx, rtx).await?; + 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, &mut tx, &pk, false, - ) - .await; + signing::update_pow_challenge(client, args, tx, &pk, false).await; } } + Ok(()) +} + +pub async fn submit_custom( + client: &C, + ctx: &mut Context, + args: args::TxCustom, +) -> Result<(), tx::Error> { + 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(()) @@ -86,30 +95,7 @@ pub async fn submit_update_vp( ) -> Result<(), tx::Error> { let (mut tx, addr, pk) = tx::build_update_vp(client, &mut ctx.wallet, args.clone()).await?; - // Build a transaction to reveal the signer of this transaction - if let Some(Address::Implicit(_)) = addr { - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { - tx: args.tx.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.tx, &pk).await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge( - client, &args.tx, &mut tx, &pk, false, - ) - .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(()) @@ -122,30 +108,7 @@ pub async fn submit_init_account( ) -> Result<(), tx::Error> { let (mut tx, addr, pk) = tx::build_init_account(client, &mut ctx.wallet, args.clone()).await?; - // Build a transaction to reveal the signer of this transaction - if let Some(Address::Implicit(_)) = addr { - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { - tx: args.tx.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.tx, &pk).await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge( - client, &args.tx, &mut tx, &pk, false, - ) - .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(()) @@ -301,30 +264,8 @@ pub async fn submit_init_validator< false, ) .await?; - // Build a transaction to reveal the signer of this transaction - if let Some(Address::Implicit(_)) = addr { - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { - tx: tx_args.clone(), - public_key: pk.clone(), - }, - ) + submit_reveal_aux(client, &mut ctx, &tx_args, addr, pk.clone(), &mut tx) .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, &tx_args, &pk).await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &tx_args, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge( - client, &tx_args, &mut tx, &pk, false, - ) - .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? @@ -511,34 +452,18 @@ pub async fn submit_transfer( ) -> Result<(), tx::Error> { for _ in 0..2 { let arg = args.clone(); - let (mut tx, addr, pk, tx_epoch, isf) = + let (mut tx, addr, pk, tx_epoch, _isf) = tx::build_transfer(client, &mut ctx.wallet, &mut ctx.shielded, arg) .await?; - // Build a transaction to reveal the signer of this transaction - if let Some(Address::Implicit(_)) = addr { - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { - tx: args.tx.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.tx, &pk) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge( - client, &args.tx, &mut tx, &pk, isf, - ) - .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?; @@ -576,30 +501,8 @@ pub async fn submit_ibc_transfer( ) -> Result<(), tx::Error> { let (mut tx, addr, pk) = tx::build_ibc_transfer(client, &mut ctx.wallet, args.clone()).await?; - // Build a transaction to reveal the signer of this transaction - if let Some(Address::Implicit(_)) = addr { - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { - tx: args.tx.clone(), - public_key: pk.clone(), - }, - ) + submit_reveal_aux(client, &mut ctx, &args.tx, addr, pk.clone(), &mut tx) .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.tx, &pk).await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge( - client, &args.tx, &mut tx, &pk, false, - ) - .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(()) @@ -755,31 +658,15 @@ pub async fn submit_init_proposal( false, ) .await?; - // Build a transaction to reveal the signer of this transaction - if let Some(Address::Implicit(_)) = addr { - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { - tx: args.tx.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.tx, &pk) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge( - client, &args.tx, &mut tx, &pk, 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(()) @@ -1036,39 +923,15 @@ pub async fn submit_vote_proposal( false, ) .await?; - // Build a transaction to reveal the signer of this transaction - if let Some(Address::Implicit(_)) = addr { - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { - tx: args.tx.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.tx, - &pk, - ) - .await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx) - .await?; - // Update the stateful PoW challenge of the outer - // transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge( - client, &args.tx, &mut tx, &pk, 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?; @@ -1156,30 +1019,7 @@ pub async fn submit_bond( ) -> Result<(), tx::Error> { let (mut tx, addr, pk) = tx::build_bond::(client, &mut ctx.wallet, args.clone()).await?; - // Build a transaction to reveal the signer of this transaction - if let Some(Address::Implicit(_)) = addr { - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { - tx: args.tx.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.tx, &pk).await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge( - client, &args.tx, &mut tx, &pk, false, - ) - .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(()) @@ -1192,30 +1032,7 @@ pub async fn submit_unbond( ) -> Result<(), tx::Error> { let (mut tx, addr, pk, latest_withdrawal_pre) = tx::build_unbond(client, &mut ctx.wallet, args.clone()).await?; - // Build a transaction to reveal the signer of this transaction - if let Some(Address::Implicit(_)) = addr { - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { - tx: args.tx.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.tx, &pk).await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - #[cfg(not(feature = "mainnet"))] - // Update the stateful PoW challenge of the outer transaction - signing::update_pow_challenge( - client, &args.tx, &mut tx, &pk, false, - ) - .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?; @@ -1229,30 +1046,8 @@ pub async fn submit_withdraw( ) -> Result<(), tx::Error> { let (mut tx, addr, pk) = tx::build_withdraw(client, &mut ctx.wallet, args.clone()).await?; - // Build a transaction to reveal the signer of this transaction - if let Some(Address::Implicit(_)) = addr { - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { - tx: args.tx.clone(), - public_key: pk.clone(), - }, - ) + submit_reveal_aux(client, &mut ctx, &args.tx, addr, pk.clone(), &mut tx) .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.tx, &pk).await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge( - client, &args.tx, &mut tx, &pk, false, - ) - .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(()) @@ -1263,36 +1058,14 @@ pub async fn submit_validator_commission_change< >( client: &C, mut ctx: Context, - args: args::TxCommissionRateChange, + args: args::CommissionRateChange, ) -> Result<(), tx::Error> { let arg = args.clone(); let (mut tx, addr, pk) = tx::build_validator_commission_change(client, &mut ctx.wallet, arg) .await?; - // Build a transaction to reveal the signer of this transaction - if let Some(Address::Implicit(_)) = addr { - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { - tx: args.tx.clone(), - public_key: pk.clone(), - }, - ) + submit_reveal_aux(client, &mut ctx, &args.tx, addr, pk.clone(), &mut tx) .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.tx, &pk).await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge( - client, &args.tx, &mut tx, &pk, false, - ) - .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(()) @@ -1308,30 +1081,8 @@ pub async fn submit_unjail_validator< let (mut tx, addr, pk) = tx::build_unjail_validator(client, &mut ctx.wallet, args.clone()) .await?; - // Build a transaction to reveal the signer of this transaction - if let Some(Address::Implicit(_)) = addr { - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { - tx: args.tx.clone(), - public_key: pk.clone(), - }, - ) + submit_reveal_aux(client, &mut ctx, &args.tx, addr, pk.clone(), &mut tx) .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.tx, &pk).await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, &args.tx, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge( - client, &args.tx, &mut tx, &pk, false, - ) - .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(()) @@ -1418,4 +1169,4 @@ mod test_tx { // MaspDenom::One // assert_eq!(zero.abs(), Uint::zero()); } -} +} \ No newline at end of file diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 9815520f72..d9b28f8ab7 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -70,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 d042a31f1b..7ad213f327 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -332,7 +332,7 @@ pub struct QueryBondedStake { #[derive(Clone, Debug)] /// Commission rate change args -pub struct TxCommissionRateChange { +pub struct CommissionRateChange { /// Common tx arguments pub tx: Tx, /// Validator address (should be self) diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index c63da9f62c..72d02d7c7a 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -607,7 +607,7 @@ pub async fn build_validator_commission_change< >( client: &C, wallet: &mut Wallet, - args: args::TxCommissionRateChange, + args: args::CommissionRateChange, ) -> Result<(Tx, Option
, common::PublicKey), Error> { let epoch = rpc::query_epoch(client).await;