diff --git a/.changelog/unreleased/improvements/2458-masp-scanning.md b/.changelog/unreleased/improvements/2458-masp-scanning.md new file mode 100644 index 0000000000..cafbd11a83 --- /dev/null +++ b/.changelog/unreleased/improvements/2458-masp-scanning.md @@ -0,0 +1,2 @@ +- Simplified the transaction fetching algorithm to enable it to be saved to + storage more frequently. ([\#2458](https://github.com/anoma/namada/pull/2458)) \ No newline at end of file diff --git a/Makefile b/Makefile index 3dc399433d..bd85b444d4 100644 --- a/Makefile +++ b/Makefile @@ -193,7 +193,7 @@ test-integration-save-proofs: # Run integration tests without specifying any pre-built MASP proofs option test-integration-slow: RUST_BACKTRACE=$(RUST_BACKTRACE) \ - $(cargo) +$(nightly) test integration::$(TEST_FILTER) --features integration \ + $(cargo) +$(nightly) test $(jobs) integration::$(TEST_FILTER) --features integration \ -Z unstable-options \ -- \ --test-threads=1 \ diff --git a/crates/apps/src/lib/bench_utils.rs b/crates/apps/src/lib/bench_utils.rs index ea9e463340..bcb4aac198 100644 --- a/crates/apps/src/lib/bench_utils.rs +++ b/crates/apps/src/lib/bench_utils.rs @@ -117,6 +117,8 @@ const BERTHA_SPENDING_KEY: &str = "bertha_spending"; const FILE_NAME: &str = "shielded.dat"; const TMP_FILE_NAME: &str = "shielded.tmp"; +const SPECULATIVE_FILE_NAME: &str = "speculative_shielded.dat"; +const SPECULATIVE_TMP_FILE_NAME: &str = "speculative_shielded.tmp"; /// For `tracing_subscriber`, which fails if called more than once in the same /// process @@ -669,6 +671,31 @@ impl ShieldedUtils for BenchShieldedUtils { Ok(()) } + /// Try to load the last saved speculative shielded context from the given + /// context directory. If this fails, then leave the current context + /// unchanged. + async fn load_speculative( + &self, + ctx: &mut ShieldedContext, + ) -> std::io::Result<()> { + // Try to load shielded context from file + let mut ctx_file = File::open( + self.context_dir + .0 + .path() + .to_path_buf() + .join(SPECULATIVE_FILE_NAME), + )?; + let mut bytes = Vec::new(); + ctx_file.read_to_end(&mut bytes)?; + // Fill the supplied context with the deserialized object + *ctx = ShieldedContext { + utils: ctx.utils.clone(), + ..ShieldedContext::::deserialize(&mut &bytes[..])? + }; + Ok(()) + } + /// Save this shielded context into its associated context directory async fn save( &self, @@ -695,12 +722,56 @@ impl ShieldedUtils for BenchShieldedUtils { // Atomicity is required to prevent other client instances from reading // corrupt data. std::fs::rename( - tmp_path.clone(), + tmp_path, self.context_dir.0.path().to_path_buf().join(FILE_NAME), )?; - // Finally, remove our temporary file to allow future saving of shielded - // contexts. - std::fs::remove_file(tmp_path)?; + + // Remove the speculative file if present since it's state is + // overwritten by the confirmed one we just saved + let _ = std::fs::remove_file(SPECULATIVE_FILE_NAME); + Ok(()) + } + + /// Save this speculative shielded context into its associated context + /// directory + async fn save_speculative( + &self, + ctx: &ShieldedContext, + ) -> std::io::Result<()> { + // TODO: use mktemp crate? + let tmp_path = self + .context_dir + .0 + .path() + .to_path_buf() + .join(SPECULATIVE_TMP_FILE_NAME); + { + // First serialize the shielded context into a temporary file. + // Inability to create this file implies a simultaneuous write + // is in progress. In this case, immediately + // fail. This is unproblematic because the data + // intended to be stored can always be re-fetched + // from the blockchain. + let mut ctx_file = OpenOptions::new() + .write(true) + .create_new(true) + .open(tmp_path.clone())?; + let mut bytes = Vec::new(); + ctx.serialize(&mut bytes) + .expect("cannot serialize shielded context"); + ctx_file.write_all(&bytes[..])?; + } + // Atomically update the old shielded context file with new data. + // Atomicity is required to prevent other client instances from + // reading corrupt data. + std::fs::rename( + tmp_path, + self.context_dir + .0 + .path() + .to_path_buf() + .join(SPECULATIVE_FILE_NAME), + )?; Ok(()) } } @@ -983,9 +1054,13 @@ impl BenchShieldedCtx { .wallet .find_spending_key(ALBERT_SPENDING_KEY, None) .unwrap(); - async_runtime - .block_on(self.shielded.fetch( + self.shielded = async_runtime + .block_on(crate::client::masp::syncing( + self.shielded, &self.shell, + &StdIo, + 1, + None, &[spending_key.into()], &[], )) diff --git a/crates/apps/src/lib/cli.rs b/crates/apps/src/lib/cli.rs index 8cb65ebcd5..716b072eb7 100644 --- a/crates/apps/src/lib/cli.rs +++ b/crates/apps/src/lib/cli.rs @@ -267,6 +267,7 @@ pub mod cmds { .subcommand(QueryMetaData::def().display_order(5)) // Actions .subcommand(SignTx::def().display_order(6)) + .subcommand(ShieldedSync::def().display_order(6)) .subcommand(GenIbcShieldedTransafer::def().display_order(6)) // Utils .subcommand(Utils::def().display_order(7)) @@ -346,6 +347,7 @@ pub mod cmds { let add_to_eth_bridge_pool = Self::parse_with_ctx(matches, AddToEthBridgePool); let sign_tx = Self::parse_with_ctx(matches, SignTx); + let shielded_sync = Self::parse_with_ctx(matches, ShieldedSync); let gen_ibc_shielded = Self::parse_with_ctx(matches, GenIbcShieldedTransafer); let utils = SubCmd::parse(matches).map(Self::WithoutContext); @@ -397,6 +399,7 @@ pub mod cmds { .or(query_metadata) .or(query_account) .or(sign_tx) + .or(shielded_sync) .or(gen_ibc_shielded) .or(utils) } @@ -483,6 +486,7 @@ pub mod cmds { QueryValidatorState(QueryValidatorState), QueryRewards(QueryRewards), SignTx(SignTx), + ShieldedSync(ShieldedSync), GenIbcShieldedTransafer(GenIbcShieldedTransafer), } @@ -1344,6 +1348,29 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct ShieldedSync(pub args::ShieldedSync); + + impl SubCmd for ShieldedSync { + const CMD: &'static str = "shielded-sync"; + + fn parse(matches: &ArgMatches) -> Option { + matches + .subcommand_matches(Self::CMD) + .map(|matches| ShieldedSync(args::ShieldedSync::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about( + "Sync the local shielded context with MASP notes owned by \ + the provided viewing / spending keys up to an optional \ + specified block height.", + ) + .add_args::>() + } + } + #[derive(Clone, Debug)] pub struct Bond(pub args::Bond); @@ -2878,6 +2905,8 @@ pub mod args { Err(_) => config::get_default_namada_folder(), }), ); + pub const BATCH_SIZE_OPT: ArgDefault = + arg_default("batch-size", DefaultFn(|| 1)); pub const BLOCK_HEIGHT: Arg = arg("block-height"); pub const BLOCK_HEIGHT_OPT: ArgOpt = arg_opt("height"); pub const BRIDGE_POOL_GAS_AMOUNT: ArgDefault = @@ -3061,6 +3090,8 @@ pub mod args { pub const SIGNATURES: ArgMulti = arg_multi("signatures"); pub const SOURCE: Arg = arg("source"); pub const SOURCE_OPT: ArgOpt = SOURCE.opt(); + pub const SPENDING_KEYS: ArgMulti = + arg_multi("spending-keys"); pub const STEWARD: Arg = arg("steward"); pub const SOURCE_VALIDATOR: Arg = arg("source-validator"); pub const STORAGE_KEY: Arg = arg("storage-key"); @@ -3097,6 +3128,8 @@ pub mod args { pub const VALUE: Arg = arg("value"); pub const VOTER_OPT: ArgOpt = arg_opt("voter"); pub const VIEWING_KEY: Arg = arg("key"); + pub const VIEWING_KEYS: ArgMulti = + arg_multi("viewing-keys"); pub const VP: ArgOpt = arg_opt("vp"); pub const WALLET_ALIAS_FORCE: ArgFlag = flag("wallet-alias-force"); pub const WASM_CHECKSUMS_PATH: Arg = arg("wasm-checksums-path"); @@ -5608,6 +5641,63 @@ pub mod args { } } + impl Args for ShieldedSync { + fn parse(matches: &ArgMatches) -> Self { + let ledger_address = LEDGER_ADDRESS.parse(matches); + let batch_size = BATCH_SIZE_OPT.parse(matches); + let last_query_height = BLOCK_HEIGHT_OPT.parse(matches); + let spending_keys = SPENDING_KEYS.parse(matches); + let viewing_keys = VIEWING_KEYS.parse(matches); + Self { + ledger_address, + batch_size, + last_query_height, + spending_keys, + viewing_keys, + } + } + + fn def(app: App) -> App { + app.arg(LEDGER_ADDRESS.def().help(LEDGER_ADDRESS_ABOUT)) + .arg(BATCH_SIZE_OPT.def().help( + "Optional batch size which determines how many txs to \ + fetch before caching locally. Default is 1.", + )) + .arg(BLOCK_HEIGHT_OPT.def().help( + "Option block height to sync up to. Default is latest.", + )) + .arg(SPENDING_KEYS.def().help( + "List of new spending keys with which to check note \ + ownership. These will be added to the shielded context.", + )) + .arg(VIEWING_KEYS.def().help( + "List of new viewing keys with which to check note \ + ownership. These will be added to the shielded context.", + )) + } + } + + impl CliToSdk> for ShieldedSync { + fn to_sdk(self, ctx: &mut Context) -> ShieldedSync { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + ShieldedSync { + ledger_address: self.ledger_address, + batch_size: self.batch_size, + last_query_height: self.last_query_height, + spending_keys: self + .spending_keys + .iter() + .map(|sk| chain_ctx.get_cached(sk)) + .collect(), + viewing_keys: self + .viewing_keys + .iter() + .map(|vk| chain_ctx.get_cached(vk)) + .collect(), + } + } + } + impl CliToSdk> for GenIbcShieldedTransafer { @@ -5892,6 +5982,7 @@ pub mod args { type EthereumAddress = String; type Keypair = WalletKeypair; type PublicKey = WalletPublicKey; + type SpendingKey = WalletSpendingKey; type TendermintAddress = tendermint_config::net::Address; type TransferSource = WalletTransferSource; type TransferTarget = WalletTransferTarget; diff --git a/crates/apps/src/lib/cli/client.rs b/crates/apps/src/lib/cli/client.rs index b7f41dc6a2..969e85f2d3 100644 --- a/crates/apps/src/lib/cli/client.rs +++ b/crates/apps/src/lib/cli/client.rs @@ -1,4 +1,5 @@ use color_eyre::eyre::Result; +use masp_primitives::zip32::ExtendedFullViewingKey; use namada::types::io::Io; use namada_sdk::{Namada, NamadaImpl}; @@ -315,6 +316,39 @@ impl CliApi { tx::submit_validator_metadata_change(&namada, args) .await?; } + Sub::ShieldedSync(ShieldedSync(args)) => { + let client = client.unwrap_or_else(|| { + C::from_tendermint_address(&args.ledger_address) + }); + client.wait_until_node_is_synced(&io).await?; + let args = args.to_sdk(&mut ctx); + let chain_ctx = ctx.take_chain_or_exit(); + let vks = chain_ctx + .wallet + .get_viewing_keys() + .values() + .copied() + .map(|vk| ExtendedFullViewingKey::from(vk).fvk.vk) + .chain(args.viewing_keys.into_iter().map(|vk| { + ExtendedFullViewingKey::from(vk).fvk.vk + })) + .collect::>(); + let sks = args + .spending_keys + .into_iter() + .map(|sk| sk.into()) + .collect::>(); + crate::client::masp::syncing( + chain_ctx.shielded, + &client, + &io, + args.batch_size, + args.last_query_height, + &sks, + &vks, + ) + .await?; + } // Eth bridge Sub::AddToEthBridgePool(args) => { let args = args.0; diff --git a/crates/apps/src/lib/client/masp.rs b/crates/apps/src/lib/client/masp.rs new file mode 100644 index 0000000000..ad23a104c5 --- /dev/null +++ b/crates/apps/src/lib/client/masp.rs @@ -0,0 +1,154 @@ +use std::fmt::Debug; + +use color_eyre::owo_colors::OwoColorize; +use masp_primitives::sapling::ViewingKey; +use masp_primitives::zip32::ExtendedSpendingKey; +use namada_sdk::error::Error; +use namada_sdk::io::Io; +use namada_sdk::masp::{ + IndexedNoteEntry, ProgressLogger, ProgressType, ShieldedContext, + ShieldedUtils, +}; +use namada_sdk::queries::Client; +use namada_sdk::types::storage::BlockHeight; +use namada_sdk::{display, display_line, MaybeSend, MaybeSync}; + +pub async fn syncing< + U: ShieldedUtils + MaybeSend + MaybeSync, + C: Client + Sync, + IO: Io, +>( + mut shielded: ShieldedContext, + client: &C, + io: &IO, + batch_size: u64, + last_query_height: Option, + sks: &[ExtendedSpendingKey], + fvks: &[ViewingKey], +) -> Result, Error> { + let shutdown_signal = async { + let (tx, rx) = tokio::sync::oneshot::channel(); + namada_sdk::control_flow::shutdown_send(tx).await; + rx.await + }; + + display_line!(io, "{}", "==== Shielded sync started ====".on_white()); + display_line!(io, "\n\n"); + let logger = CliLogger::new(io); + let sync = async move { + shielded + .fetch(client, &logger, last_query_height, batch_size, sks, fvks) + .await + .map(|_| shielded) + }; + tokio::select! { + sync = sync => { + let shielded = sync?; + display!(io, "Syncing finished\n"); + Ok(shielded) + }, + sig = shutdown_signal => { + sig.map_err(|e| Error::Other(e.to_string()))?; + display!(io, "\n"); + Ok(ShieldedContext::default()) + }, + } +} + +pub struct CliLogging<'io, T, IO: Io> { + items: Vec, + index: usize, + length: usize, + io: &'io IO, + r#type: ProgressType, +} + +impl<'io, T: Debug, IO: Io> CliLogging<'io, T, IO> { + fn new(items: I, io: &'io IO, r#type: ProgressType) -> Self + where + I: IntoIterator, + { + let items: Vec<_> = items.into_iter().collect(); + Self { + length: items.len(), + items, + index: 0, + io, + r#type, + } + } +} + +impl<'io, T: Debug, IO: Io> Iterator for CliLogging<'io, T, IO> { + type Item = T; + + fn next(&mut self) -> Option { + if self.index == 0 { + self.items = { + let mut new_items = vec![]; + std::mem::swap(&mut new_items, &mut self.items); + new_items.into_iter().rev().collect() + }; + } + if self.items.is_empty() { + return None; + } + self.index += 1; + let percent = (100 * self.index) / self.length; + let completed: String = vec!['#'; percent].iter().collect(); + let incomplete: String = vec!['.'; 100 - percent].iter().collect(); + display_line!(self.io, "\x1b[2A\x1b[J"); + match self.r#type { + ProgressType::Fetch => display_line!( + self.io, + "Fetched block {:?} of {:?}", + self.items.last().unwrap(), + self.items[0] + ), + ProgressType::Scan => display_line!( + self.io, + "Scanning {} of {}", + self.index, + self.length + ), + } + display!(self.io, "[{}{}] ~~ {} %", completed, incomplete, percent); + self.io.flush(); + self.items.pop() + } +} + +/// A progress logger for the CLI +#[derive(Debug, Clone)] +pub struct CliLogger<'io, IO: Io> { + io: &'io IO, +} + +impl<'io, IO: Io> CliLogger<'io, IO> { + pub fn new(io: &'io IO) -> Self { + Self { io } + } +} + +impl<'io, IO: Io> ProgressLogger for CliLogger<'io, IO> { + type Fetch = CliLogging<'io, u64, IO>; + type Scan = CliLogging<'io, IndexedNoteEntry, IO>; + + fn io(&self) -> &IO { + self.io + } + + fn fetch(&self, items: I) -> Self::Fetch + where + I: IntoIterator, + { + CliLogging::new(items, self.io, ProgressType::Fetch) + } + + fn scan(&self, items: I) -> Self::Scan + where + I: IntoIterator, + { + CliLogging::new(items, self.io, ProgressType::Scan) + } +} diff --git a/crates/apps/src/lib/client/mod.rs b/crates/apps/src/lib/client/mod.rs index 8862c5a564..7ad6d57e5f 100644 --- a/crates/apps/src/lib/client/mod.rs +++ b/crates/apps/src/lib/client/mod.rs @@ -1,3 +1,4 @@ +pub mod masp; pub mod rpc; pub mod tx; pub mod utils; diff --git a/crates/apps/src/lib/client/rpc.rs b/crates/apps/src/lib/client/rpc.rs index cedc239b3f..5852af8400 100644 --- a/crates/apps/src/lib/client/rpc.rs +++ b/crates/apps/src/lib/client/rpc.rs @@ -137,6 +137,7 @@ pub async fn query_transfers( let transfers = shielded .query_tx_deltas( context.client(), + context.io(), &query_owner, &query_token, &wallet.get_viewing_keys(), @@ -878,11 +879,6 @@ pub async fn query_shielded_balance( { let mut shielded = context.shielded_mut().await; let _ = shielded.load().await; - let fvks: Vec<_> = viewing_keys - .iter() - .map(|fvk| ExtendedFullViewingKey::from(*fvk).fvk.vk) - .collect(); - shielded.fetch(context.client(), &[], &fvks).await.unwrap(); // Precompute asset types to increase chances of success in decoding let _ = shielded.precompute_asset_types(context).await; // Save the update state so that future fetches can be short-circuited diff --git a/crates/apps/src/lib/client/tx.rs b/crates/apps/src/lib/client/tx.rs index 911e1b475a..c7edf4adba 100644 --- a/crates/apps/src/lib/client/tx.rs +++ b/crates/apps/src/lib/client/tx.rs @@ -23,6 +23,7 @@ use namada::types::dec::Dec; use namada::types::io::Io; use namada::types::key::{self, *}; use namada_sdk::rpc::{InnerTxResult, TxBroadcastData, TxResponse}; +use namada_sdk::signing::validate_fee_and_gen_unshield; use namada_sdk::wallet::alias::validator_consensus_key; use namada_sdk::wallet::{Wallet, WalletIo}; use namada_sdk::{display_line, edisplay_line, error, signing, tx, Namada}; @@ -414,13 +415,20 @@ pub async fn submit_change_consensus_key( let signing_data = init_validator_signing_data(namada, &tx_args, vec![new_key]).await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + namada, + &tx_args, + &signing_data.fee_payer, + ) + .await?; tx::prepare_tx( - namada, + namada.client(), &tx_args, &mut tx, + unshield, + fee_amount, signing_data.fee_payer.clone(), - None, ) .await?; @@ -742,13 +750,20 @@ pub async fn submit_become_validator( let signing_data = init_validator_signing_data(namada, &tx_args, all_pks).await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + namada, + &tx_args, + &signing_data.fee_payer, + ) + .await?; tx::prepare_tx( - namada, + namada.client(), &tx_args, &mut tx, + unshield, + fee_amount, signing_data.fee_payer.clone(), - None, ) .await?; diff --git a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs index a65c2af8e7..d963e8f92c 100644 --- a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1,8 +1,6 @@ //! Implementation of the `FinalizeBlock` ABCI++ method for the Shell use data_encoding::HEXUPPER; -use masp_primitives::merkle_tree::CommitmentTree; -use masp_primitives::sapling::Node; use namada::governance::pgf::inflation as pgf_inflation; use namada::ledger::events::EventType; use namada::ledger::gas::{GasMetering, TxGasMeter}; @@ -13,10 +11,7 @@ use namada::proof_of_stake::storage::{ write_last_block_proposer_address, }; use namada::state::wl_storage::WriteLogAndStorage; -use namada::state::write_log::StorageModification; -use namada::state::{ - ResultExt, StorageRead, StorageWrite, EPOCH_SWITCH_BLOCKS_DELAY, -}; +use namada::state::{StorageRead, EPOCH_SWITCH_BLOCKS_DELAY}; use namada::token::conversion::update_allowed_conversions; use namada::tx::data::protocol::ProtocolTxType; use namada::types::key::tm_raw_hash_to_string; @@ -599,19 +594,6 @@ where tracing::info!("{}", stats); tracing::info!("{}", stats.format_tx_executed()); - // Update the MASP commitment tree anchor if the tree was updated - let tree_key = token::storage_key::masp_commitment_tree_key(); - if let Some(StorageModification::Write { value }) = - self.wl_storage.write_log.read(&tree_key).0 - { - let updated_tree = CommitmentTree::::try_from_slice(value) - .into_storage_result()?; - let anchor_key = token::storage_key::masp_commitment_anchor_key( - updated_tree.root(), - ); - self.wl_storage.write(&anchor_key, ())?; - } - if update_for_tendermint { self.update_epoch(&mut response); // send the latest oracle configs. These may have changed due to diff --git a/crates/apps/src/lib/node/ledger/shell/testing/node.rs b/crates/apps/src/lib/node/ledger/shell/testing/node.rs index a3ab32dcb6..ceb4fe1637 100644 --- a/crates/apps/src/lib/node/ledger/shell/testing/node.rs +++ b/crates/apps/src/lib/node/ledger/shell/testing/node.rs @@ -438,7 +438,26 @@ impl MockNode { votes, }; - locked.finalize_block(req).expect("Test failed"); + let resp = locked.finalize_block(req).expect("Test failed"); + let mut error_codes = resp + .events + .into_iter() + .map(|e| { + let code = ResultCode::from_u32( + e.attributes + .get("code") + .map(|e| u32::from_str(e).unwrap()) + .unwrap_or_default(), + ) + .unwrap(); + if code == ResultCode::Ok { + NodeResults::Ok + } else { + NodeResults::Failed(code) + } + }) + .collect::>(); + self.results.lock().unwrap().append(&mut error_codes); locked.commit(); // Cache the block @@ -501,7 +520,7 @@ impl MockNode { /// Send a tx through Process Proposal and Finalize Block /// and register the results. - fn submit_txs(&self, txs: Vec>) { + pub fn submit_txs(&self, txs: Vec>) { // The block space allocator disallows encrypted txs in certain blocks. // Advance to block height that allows txs. self.advance_to_allowed_block(); diff --git a/crates/core/src/types/masp.rs b/crates/core/src/types/masp.rs index 61b63dab60..64b18eb6b9 100644 --- a/crates/core/src/types/masp.rs +++ b/crates/core/src/types/masp.rs @@ -202,6 +202,13 @@ impl From } } +impl From for masp_primitives::sapling::ViewingKey { + fn from(value: ExtendedViewingKey) -> Self { + let fvk = masp_primitives::zip32::ExtendedFullViewingKey::from(value); + fvk.fvk.vk + } +} + impl serde::Serialize for ExtendedViewingKey { fn serialize( &self, diff --git a/crates/core/src/types/storage.rs b/crates/core/src/types/storage.rs index 1ac99d11b4..da1ed2920e 100644 --- a/crates/core/src/types/storage.rs +++ b/crates/core/src/types/storage.rs @@ -1463,6 +1463,7 @@ impl GetEventNonce for InnerEthEventsQueue { PartialEq, Ord, PartialOrd, + Hash, )] pub struct IndexedTx { /// The block height of the indexed tx diff --git a/crates/namada/src/ledger/native_vp/masp.rs b/crates/namada/src/ledger/native_vp/masp.rs index b222aeaf9b..52e614f485 100644 --- a/crates/namada/src/ledger/native_vp/masp.rs +++ b/crates/namada/src/ledger/native_vp/masp.rs @@ -25,8 +25,8 @@ use sha2::Digest as Sha2Digest; use thiserror::Error; use token::storage_key::{ balance_key, is_any_shielded_action_balance_key, is_masp_allowed_key, - is_masp_key, is_masp_nullifier_key, is_masp_tx_pin_key, - masp_commitment_anchor_key, masp_commitment_tree_key, + is_masp_commitment_anchor_key, is_masp_key, is_masp_nullifier_key, + is_masp_tx_pin_key, masp_commitment_anchor_key, masp_commitment_tree_key, masp_convert_anchor_key, masp_nullifier_key, }; use token::Amount; @@ -135,6 +135,7 @@ where // the tree and anchor in storage fn valid_note_commitment_update( &self, + keys_changed: &BTreeSet, transaction: &Transaction, ) -> Result { // Check that the merkle tree in storage has been correctly updated with @@ -171,6 +172,35 @@ where return Ok(false); } + let expected_anchor_key = + token::storage_key::masp_commitment_anchor_key(post_tree.root()); + let changed_anchor_keys: Vec<_> = keys_changed + .iter() + .filter(|key| is_masp_commitment_anchor_key(key)) + .collect(); + // Check that at most one anchor was modified, the anchor is indeed + // committed (no temp write and no delete), it is the expected + // one and carries no associated data (the latter not + // strictly necessary for validation, but we don't expect any + // value for this key anyway) + match changed_anchor_keys.len() { + 0 => (), + 1 => { + if changed_anchor_keys.first().unwrap() != &&expected_anchor_key + { + tracing::debug!( + "The masp transaction wrote an unexpected anchor key" + ); + return Ok(false); + } + } + _ => return Ok(false), + } + match self.ctx.read_bytes_post(&expected_anchor_key)? { + Some(value) if value.is_empty() => (), + _ => return Ok(false), + } + Ok(true) } @@ -578,7 +608,6 @@ where return Ok(false); } } - if !(self.valid_spend_descriptions_anchor(&shielded_tx)? && self.valid_convert_descriptions_anchor(&shielded_tx)? && self.valid_nullifiers_reveal(keys_changed, &shielded_tx)?) @@ -589,7 +618,7 @@ where // The transaction must correctly update the note commitment tree // in storage with the new output descriptions - if !self.valid_note_commitment_update(&shielded_tx)? { + if !self.valid_note_commitment_update(keys_changed, &shielded_tx)? { return Ok(false); } diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index e8d2cbcdd2..8ab98d3254 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -11,7 +11,7 @@ use namada_core::types::ethereum_events::EthAddress; use namada_core::types::keccak::KeccakHash; use namada_core::types::key::{common, SchemeType}; use namada_core::types::masp::PaymentAddress; -use namada_core::types::storage::Epoch; +use namada_core::types::storage::{BlockHeight, Epoch}; use namada_core::types::time::DateTimeUtc; use namada_core::types::{storage, token}; use namada_governance::cli::onchain::{ @@ -61,6 +61,8 @@ pub trait NamadaTypes: Clone + std::fmt::Debug { type EthereumAddress: Clone + std::fmt::Debug; /// Represents a viewing key type ViewingKey: Clone + std::fmt::Debug; + /// Represents a spending key + type SpendingKey: Clone + std::fmt::Debug; /// Represents the owner of a balance type BalanceOwner: Clone + std::fmt::Debug; /// Represents a public key @@ -100,6 +102,7 @@ impl NamadaTypes for SdkTypes { type EthereumAddress = (); type Keypair = namada_core::types::key::common::SecretKey; type PublicKey = namada_core::types::key::common::PublicKey; + type SpendingKey = namada_core::types::masp::ExtendedSpendingKey; type TendermintAddress = tendermint_config::net::Address; type TransferSource = namada_core::types::masp::TransferSource; type TransferTarget = namada_core::types::masp::TransferTarget; @@ -1806,6 +1809,23 @@ pub struct SignTx { pub owner: C::Address, } +#[derive(Clone, Debug)] +/// Sync notes from MASP owned by the provided spending / +/// viewing keys. Syncing can be told to stop at a given +/// block height. +pub struct ShieldedSync { + /// The ledger address + pub ledger_address: C::TendermintAddress, + /// The number of txs to fetch before caching + pub batch_size: u64, + /// Height to sync up to. Defaults to most recent + pub last_query_height: Option, + /// Spending keys used to determine note ownership + pub spending_keys: Vec, + /// Viewing keys used to determine note ownership + pub viewing_keys: Vec, +} + /// Query PoS commission rate #[derive(Clone, Debug)] pub struct QueryCommissionRate { diff --git a/crates/sdk/src/eth_bridge/bridge_pool.rs b/crates/sdk/src/eth_bridge/bridge_pool.rs index 6712e463c8..c8aeb0bb92 100644 --- a/crates/sdk/src/eth_bridge/bridge_pool.rs +++ b/crates/sdk/src/eth_bridge/bridge_pool.rs @@ -40,7 +40,7 @@ use crate::queries::{ TransferToEthereumStatus, RPC, }; use crate::rpc::{query_storage_value, query_wasm_code_hash, validate_amount}; -use crate::signing::aux_signing_data; +use crate::signing::{aux_signing_data, validate_fee_and_gen_unshield}; use crate::tx::prepare_tx; use crate::{ args, display, display_line, edisplay_line, MaybeSync, Namada, @@ -87,6 +87,12 @@ pub async fn build_bridge_pool_tx( Some(sender_), ), )?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + context, + &tx_args, + &signing_data.fee_payer, + ) + .await?; let chain_id = tx_args .chain_id @@ -104,11 +110,12 @@ pub async fn build_bridge_pool_tx( .add_data(transfer); prepare_tx( - context, + context.client(), &tx_args, &mut tx, + unshield, + fee_amount, signing_data.fee_payer.clone(), - None, ) .await?; diff --git a/crates/sdk/src/masp.rs b/crates/sdk/src/masp.rs index c3cf8d1bf3..4c895fe5f3 100644 --- a/crates/sdk/src/masp.rs +++ b/crates/sdk/src/masp.rs @@ -68,7 +68,7 @@ use rand_core::{CryptoRng, OsRng, RngCore}; use ripemd::Digest as RipemdDigest; use sha2::Digest; use thiserror::Error; -use token::storage_key::is_any_shielded_action_balance_key; +use token::storage_key::{balance_key, is_any_shielded_action_balance_key}; use token::Amount; #[cfg(feature = "testing")] @@ -113,6 +113,26 @@ pub const OUTPUT_NAME: &str = "masp-output.params"; /// Convert circuit name pub const CONVERT_NAME: &str = "masp-convert.params"; +/// Type alias for convenience and profit +pub type IndexedNoteData = BTreeMap< + IndexedTx, + ( + Epoch, + BTreeSet, + Transaction, + ), +>; + +/// Type alias for the entries of [`IndexedNoteData`] iterators +pub type IndexedNoteEntry = ( + IndexedTx, + ( + Epoch, + BTreeSet, + Transaction, + ), +); + /// Shielded transfer #[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] pub struct ShieldedTransfer { @@ -441,17 +461,29 @@ pub trait ShieldedUtils: /// Get a MASP transaction prover fn local_tx_prover(&self) -> LocalTxProver; - /// Load up the currently saved ShieldedContext + /// Load up the currently confirmed saved ShieldedContext async fn load( &self, ctx: &mut ShieldedContext, ) -> std::io::Result<()>; - /// Save the given ShieldedContext for future loads + /// Load up the currently saved speculative ShieldedContext + async fn load_speculative( + &self, + ctx: &mut ShieldedContext, + ) -> std::io::Result<()>; + + /// Save the given confirmed ShieldedContext for future loads async fn save( &self, ctx: &ShieldedContext, ) -> std::io::Result<()>; + + /// Save the given speculative ShieldedContext for future loads + async fn save_speculative( + &self, + ctx: &ShieldedContext, + ) -> std::io::Result<()>; } /// Make a ViewingKey that can view notes encrypted by given ExtendedSpendingKey @@ -517,6 +549,53 @@ pub type TransferDelta = HashMap; /// Represents the changes that were made to a list of shielded accounts pub type TransactionDelta = HashMap; +/// A cache of fetched indexed transactions. +/// +/// The cache is designed so that it either contains +/// all transactions from a given height, or none. +#[derive(BorshSerialize, BorshDeserialize, Debug, Default, Clone)] +pub struct Unscanned { + txs: IndexedNoteData, +} + +impl Unscanned { + fn extend(&mut self, items: I) + where + I: IntoIterator, + { + self.txs.extend(items.into_iter()); + } + + fn contains_height(&self, height: u64) -> bool { + self.txs.keys().any(|k| k.height.0 == height) + } + + /// We remove all indices from blocks that have been entirely scanned. + /// If a block is only partially scanned, we leave all the events in the + /// cache. + fn scanned(&mut self, ix: &IndexedTx) { + self.txs.retain(|i, _| i.height >= ix.height); + } +} + +impl IntoIterator for Unscanned { + type IntoIter = ::IntoIter; + type Item = IndexedNoteEntry; + + fn into_iter(self) -> Self::IntoIter { + self.txs.into_iter() + } +} + +#[derive(BorshSerialize, BorshDeserialize, Debug)] +/// The possible sync states of the shielded context +pub enum ContextSyncStatus { + /// The context contains only data that has been confirmed by the protocol + Confirmed, + /// The context contains that that has not yet been confirmed by the + /// protocol and could end up being invalid + Speculative, +} /// Represents the current state of the shielded pool from the perspective of /// the chosen viewing keys. @@ -525,10 +604,11 @@ pub struct ShieldedContext { /// Location where this shielded context is saved #[borsh(skip)] pub utils: U, - /// The last indexed transaction to be processed in this context - pub last_indexed: Option, /// The commitment tree produced by scanning all transactions up to tx_pos pub tree: CommitmentTree, + /// Maps viewing keys to the block height to which they are synced. + /// In particular, the height given by the value *has been scanned*. + pub vk_heights: BTreeMap>, /// Maps viewing keys to applicable note positions pub pos_map: HashMap>, /// Maps a nullifier to the note position to which it applies @@ -550,6 +630,12 @@ pub struct ShieldedContext { pub asset_types: HashMap, /// Maps note positions to their corresponding viewing keys pub vk_map: HashMap, + /// Maps a shielded tx to the index of its first output note. + pub tx_note_map: BTreeMap, + /// A cache of fetched indexed txs. + pub unscanned: Unscanned, + /// The sync state of the context + pub sync_status: ContextSyncStatus, } /// Default implementation to ease construction of TxContexts. Derive cannot be @@ -558,7 +644,8 @@ impl Default for ShieldedContext { fn default() -> ShieldedContext { ShieldedContext:: { utils: U::default(), - last_indexed: None, + vk_heights: BTreeMap::new(), + tx_note_map: BTreeMap::default(), tree: CommitmentTree::empty(), pos_map: HashMap::default(), nf_map: HashMap::default(), @@ -570,6 +657,8 @@ impl Default for ShieldedContext { delta_map: BTreeMap::default(), asset_types: HashMap::default(), vk_map: HashMap::default(), + unscanned: Default::default(), + sync_status: ContextSyncStatus::Confirmed, } } } @@ -578,150 +667,181 @@ impl ShieldedContext { /// Try to load the last saved shielded context from the given context /// directory. If this fails, then leave the current context unchanged. pub async fn load(&mut self) -> std::io::Result<()> { - self.utils.clone().load(self).await + match self.sync_status { + ContextSyncStatus::Confirmed => self.utils.clone().load(self).await, + ContextSyncStatus::Speculative => { + self.utils.clone().load_speculative(self).await + } + } + } + + /// Try to load the last saved confirmed shielded context from the given + /// context directory. If this fails, then leave the current context + /// unchanged. + pub async fn load_confirmed(&mut self) -> std::io::Result<()> { + self.utils.clone().load(self).await?; + + Ok(()) } - /// Save this shielded context into its associated context directory + /// Save this shielded context into its associated context directory. If the + /// state to be saved is confirmed than also delete the speculative one (if + /// available) pub async fn save(&self) -> std::io::Result<()> { - self.utils.save(self).await - } - - /// Merge data from the given shielded context into the current shielded - /// context. It must be the case that the two shielded contexts share the - /// same last transaction ID and share identical commitment trees. - pub fn merge(&mut self, new_ctx: ShieldedContext) { - debug_assert_eq!(self.last_indexed, new_ctx.last_indexed); - // Merge by simply extending maps. Identical keys should contain - // identical values, so overwriting should not be problematic. - self.pos_map.extend(new_ctx.pos_map); - self.nf_map.extend(new_ctx.nf_map); - self.note_map.extend(new_ctx.note_map); - self.memo_map.extend(new_ctx.memo_map); - self.div_map.extend(new_ctx.div_map); - self.witness_map.extend(new_ctx.witness_map); - self.spents.extend(new_ctx.spents); - self.asset_types.extend(new_ctx.asset_types); - self.vk_map.extend(new_ctx.vk_map); - // The deltas are the exception because different keys can reveal - // different parts of the same transaction. Hence each delta needs to be - // merged separately. - for (height, (ep, ntfer_delta, ntx_delta)) in new_ctx.delta_map { - let (_ep, tfer_delta, tx_delta) = self - .delta_map - .entry(height) - .or_insert((ep, TransferDelta::new(), TransactionDelta::new())); - tfer_delta.extend(ntfer_delta); - tx_delta.extend(ntx_delta); + match self.sync_status { + ContextSyncStatus::Confirmed => self.utils.save(self).await, + ContextSyncStatus::Speculative => { + self.utils.save_speculative(self).await + } } } + /// Update the merkle tree of witnesses the first time we + /// scan a new MASP transaction. + fn update_witness_map( + &mut self, + indexed_tx: IndexedTx, + shielded: &Transaction, + ) -> Result<(), Error> { + let mut note_pos = self.tree.size(); + self.tx_note_map.insert(indexed_tx, note_pos); + for so in shielded + .sapling_bundle() + .map_or(&vec![], |x| &x.shielded_outputs) + { + // Create merkle tree leaf node from note commitment + let node = Node::new(so.cmu.to_repr()); + // Update each merkle tree in the witness map with the latest + // addition + for (_, witness) in self.witness_map.iter_mut() { + witness.append(node).map_err(|()| { + Error::Other("note commitment tree is full".to_string()) + })?; + } + self.tree.append(node).map_err(|()| { + Error::Other("note commitment tree is full".to_string()) + })?; + // Finally, make it easier to construct merkle paths to this new + // note + let witness = IncrementalWitness::::from_tree(&self.tree); + self.witness_map.insert(note_pos, witness); + note_pos += 1; + } + Ok(()) + } + /// Fetch the current state of the multi-asset shielded pool into a /// ShieldedContext - pub async fn fetch( + pub async fn fetch( &mut self, client: &C, + logger: &impl ProgressLogger, + last_query_height: Option, + _batch_size: u64, sks: &[ExtendedSpendingKey], fvks: &[ViewingKey], ) -> Result<(), Error> { - // First determine which of the keys requested to be fetched are new. - // Necessary because old transactions will need to be scanned for new - // keys. - let mut unknown_keys = Vec::new(); + // add new viewing keys + // Reload the state from file to get the last confirmed state and + // discard any speculative data, we cannot fetch on top of a + // speculative state + // Always reload the confirmed context or initialize a new one if not + // found + if self.load_confirmed().await.is_err() { + // Initialize a default context if we couldn't load a valid one + // from storage + *self = Self { + utils: std::mem::take(&mut self.utils), + ..Default::default() + }; + } + for esk in sks { let vk = to_viewing_key(esk).vk; - if !self.pos_map.contains_key(&vk) { - unknown_keys.push(vk); - } + self.vk_heights.entry(vk).or_default(); } for vk in fvks { - if !self.pos_map.contains_key(vk) { - unknown_keys.push(*vk); - } + self.vk_heights.entry(*vk).or_default(); } - - // If unknown keys are being used, we need to scan older transactions - // for any unspent notes + let _ = self.save().await; let native_token = query_native_token(client).await?; - let (txs, mut tx_iter); - if !unknown_keys.is_empty() { - // Load all transactions accepted until this point - txs = Self::fetch_shielded_transfers(client, None).await?; - tx_iter = txs.iter(); - // Do this by constructing a shielding context only for unknown keys - let mut tx_ctx = Self { - utils: self.utils.clone(), - ..Default::default() - }; - for vk in unknown_keys { - tx_ctx.pos_map.entry(vk).or_insert_with(BTreeSet::new); + // the latest block height which has been added to the witness Merkle + // tree + let Some(least_idx) = self.vk_heights.values().min().cloned() else { + return Ok(()); + }; + let last_witnessed_tx = self.tx_note_map.keys().max().cloned(); + // get the bounds on the block heights to fetch + let start_idx = + std::cmp::min(last_witnessed_tx, least_idx).map(|ix| ix.height); + // Load all transactions accepted until this point + // N.B. the cache is a hash map + self.unscanned.extend( + self.fetch_shielded_transfers( + client, + logger, + start_idx, + last_query_height, + ) + .await?, + ); + // persist the cache in case of interruptions. + let _ = self.save().await; + + let txs = logger.scan(self.unscanned.clone()); + for (indexed_tx, (epoch, tx, stx)) in txs { + if Some(indexed_tx) > last_witnessed_tx { + self.update_witness_map(indexed_tx, &stx)?; } - // Update this unknown shielded context until it is level with self - while tx_ctx.last_indexed != self.last_indexed { - if let Some((indexed_tx, (epoch, changed_keys, stx))) = - tx_iter.next() - { - tx_ctx.scan_tx( - *indexed_tx, - *epoch, - changed_keys, - stx, - native_token.clone(), - )?; - } else { - break; - } + let mut vk_heights = BTreeMap::new(); + std::mem::swap(&mut vk_heights, &mut self.vk_heights); + for (vk, h) in vk_heights + .iter_mut() + .filter(|(_vk, h)| **h < Some(indexed_tx)) + { + self.scan_tx( + indexed_tx, + epoch, + &tx, + &stx, + vk, + native_token.clone(), + )?; + *h = Some(indexed_tx); } - // Merge the context data originating from the unknown keys into the - // current context - self.merge(tx_ctx); - } else { - // Load only transactions accepted from last_txid until this point - txs = Self::fetch_shielded_transfers(client, self.last_indexed) - .await?; - tx_iter = txs.iter(); - } - // Now that we possess the unspent notes corresponding to both old and - // new keys up until tx_pos, proceed to scan the new transactions. - for (indexed_tx, (epoch, changed_keys, stx)) in &mut tx_iter { - self.scan_tx( - *indexed_tx, - *epoch, - changed_keys, - stx, - native_token.clone(), - )?; + // possibly remove unneeded elements from the cache. + self.unscanned.scanned(&indexed_tx); + std::mem::swap(&mut vk_heights, &mut self.vk_heights); + let _ = self.save().await; } + Ok(()) } /// Obtain a chronologically-ordered list of all accepted shielded /// transactions from a node. - pub async fn fetch_shielded_transfers( + pub async fn fetch_shielded_transfers( + &self, client: &C, - last_indexed_tx: Option, - ) -> Result< - BTreeMap< - IndexedTx, - ( - Epoch, - BTreeSet, - Transaction, - ), - >, - Error, - > { + logger: &impl ProgressLogger, + last_indexed_tx: Option, + last_query_height: Option, + ) -> Result { // Query for the last produced block height let last_block_height = query_block(client) .await? .map_or_else(BlockHeight::first, |block| block.height); + let last_query_height = last_query_height.unwrap_or(last_block_height); let mut shielded_txs = BTreeMap::new(); // Fetch all the transactions we do not have yet let first_height_to_query = - last_indexed_tx.map_or_else(|| 1, |last| last.height.0); - let first_idx_to_query = - last_indexed_tx.map_or_else(|| 0, |last| last.index.0 + 1); - for height in first_height_to_query..=last_block_height.0 { + last_indexed_tx.map_or_else(|| 1, |last| last.0); + let heights = logger.fetch(first_height_to_query..=last_query_height.0); + for height in heights { + if self.unscanned.contains_height(height) { + continue; + } // Get the valid masp transactions at the specified height let epoch = query_epoch_at_height(client, height.into()) .await? @@ -733,16 +853,10 @@ impl ShieldedContext { )) })?; - let first_index_to_query = if height == first_height_to_query { - Some(TxIndex(first_idx_to_query)) - } else { - None - }; - let txs_results = match get_indexed_masp_events_at_height( client, height.into(), - first_index_to_query, + None, ) .await? { @@ -945,82 +1059,62 @@ impl ShieldedContext { epoch: Epoch, tx_changed_keys: &BTreeSet, shielded: &Transaction, + vk: &ViewingKey, native_token: Address, ) -> Result<(), Error> { // For tracking the account changes caused by this Transaction let mut transaction_delta = TransactionDelta::new(); + let mut note_pos = self.tx_note_map[&indexed_tx]; // Listen for notes sent to our viewing keys for so in shielded .sapling_bundle() .map_or(&vec![], |x| &x.shielded_outputs) { - // Create merkle tree leaf node from note commitment - let node = Node::new(so.cmu.to_repr()); - // Update each merkle tree in the witness map with the latest - // addition - for (_, witness) in self.witness_map.iter_mut() { - witness.append(node).map_err(|()| { - Error::Other("note commitment tree is full".to_string()) - })?; - } - let note_pos = self.tree.size(); - self.tree.append(node).map_err(|()| { - Error::Other("note commitment tree is full".to_string()) - })?; - // Finally, make it easier to construct merkle paths to this new - // note - let witness = IncrementalWitness::::from_tree(&self.tree); - self.witness_map.insert(note_pos, witness); - // Let's try to see if any of our viewing keys can decrypt latest + // Let's try to see if this viewing key can decrypt latest // note - let mut pos_map = HashMap::new(); - std::mem::swap(&mut pos_map, &mut self.pos_map); - for (vk, notes) in pos_map.iter_mut() { - let decres = try_sapling_note_decryption::<_, OutputDescription<<::SaplingAuth as masp_primitives::transaction::components::sapling::Authorization>::Proof>>( - &NETWORK, - 1.into(), - &PreparedIncomingViewingKey::new(&vk.ivk()), - so, + let notes = self.pos_map.entry(*vk).or_default(); + let decres = try_sapling_note_decryption::<_, OutputDescription<<::SaplingAuth as masp_primitives::transaction::components::sapling::Authorization>::Proof>>( + &NETWORK, + 1.into(), + &PreparedIncomingViewingKey::new(&vk.ivk()), + so, + ); + // So this current viewing key does decrypt this current note... + if let Some((note, pa, memo)) = decres { + // Add this note to list of notes decrypted by this viewing + // key + notes.insert(note_pos); + // Compute the nullifier now to quickly recognize when spent + let nf = note.nf( + &vk.nk, + note_pos.try_into().map_err(|_| { + Error::Other("Can not get nullifier".to_string()) + })?, ); - // So this current viewing key does decrypt this current note... - if let Some((note, pa, memo)) = decres { - // Add this note to list of notes decrypted by this viewing - // key - notes.insert(note_pos); - // Compute the nullifier now to quickly recognize when spent - let nf = note.nf( - &vk.nk, - note_pos.try_into().map_err(|_| { - Error::Other("Can not get nullifier".to_string()) - })?, - ); - self.note_map.insert(note_pos, note); - self.memo_map.insert(note_pos, memo); - // The payment address' diversifier is required to spend - // note - self.div_map.insert(note_pos, *pa.diversifier()); - self.nf_map.insert(nf, note_pos); - // Note the account changes - let balance = transaction_delta - .entry(*vk) - .or_insert_with(I128Sum::zero); - *balance += I128Sum::from_nonnegative( - note.asset_type, - note.value as i128, + self.note_map.insert(note_pos, note); + self.memo_map.insert(note_pos, memo); + // The payment address' diversifier is required to spend + // note + self.div_map.insert(note_pos, *pa.diversifier()); + self.nf_map.insert(nf, note_pos); + // Note the account changes + let balance = + transaction_delta.entry(*vk).or_insert_with(I128Sum::zero); + *balance += I128Sum::from_nonnegative( + note.asset_type, + note.value as i128, + ) + .map_err(|()| { + Error::Other( + "found note with invalid value or asset type" + .to_string(), ) - .map_err(|()| { - Error::Other( - "found note with invalid value or asset type" - .to_string(), - ) - })?; - - self.vk_map.insert(note_pos, *vk); - break; - } + })?; + self.vk_map.insert(note_pos, *vk); } - std::mem::swap(&mut pos_map, &mut self.pos_map); + note_pos += 1; } + // Cancel out those of our notes that have been spent for ss in shielded .sapling_bundle() @@ -1172,8 +1266,6 @@ impl ShieldedContext { change: -amount.change(), }, ); - self.last_indexed = Some(indexed_tx); - self.delta_map .insert(indexed_tx, (epoch, transfer_delta, transaction_delta)); Ok(()) @@ -1882,18 +1974,11 @@ impl ShieldedContext { } // We want to fund our transaction solely from supplied spending key let spending_key = spending_key.map(|x| x.into()); - let spending_keys: Vec<_> = spending_key.into_iter().collect(); { // Load the current shielded context given the spending key we // possess let mut shielded = context.shielded_mut().await; let _ = shielded.load().await; - shielded - .fetch(context.client(), &spending_keys, &[]) - .await?; - // Save the update state so that future fetches can be - // short-circuited - let _ = shielded.save().await; } // Determine epoch in which to submit potential shielded transaction let epoch = rpc::query_epoch(context.client()).await?; @@ -2110,7 +2195,7 @@ impl ShieldedContext { // We want to take at most the remaining quota for the // current denomination to the receiver let contr = std::cmp::min(*rem_amount as u128, val) as u64; - // Make transaction output tied to thee currentt token, + // Make transaction output tied to the current token, // denomination, and epoch. if let Some(pa) = payment_address { // If there is a shielded output @@ -2291,6 +2376,19 @@ impl ShieldedContext { BorshDeserialize::try_from_slice(&loaded_bytes) .map_err(|_e| Error::Other(exp_str))?; + // Cache the generated transfer + let mut shielded_ctx = context.shielded_mut().await; + shielded_ctx + .pre_cache_transaction( + context, + &loaded.masp_tx, + source, + target, + token, + epoch, + ) + .await?; + Ok(Some(loaded)) } else { // Build and return the constructed transaction @@ -2305,6 +2403,20 @@ impl ShieldedContext { .await .map_err(|e| Error::Other(e.to_string()))?; } + + // Cache the generated transfer + let mut shielded_ctx = context.shielded_mut().await; + shielded_ctx + .pre_cache_transaction( + context, + &built.masp_tx, + source, + target, + token, + epoch, + ) + .await?; + Ok(Some(built)) } } @@ -2315,17 +2427,96 @@ impl ShieldedContext { let built = build_transfer( context.shielded().await.utils.local_tx_prover(), )?; + + let mut shielded_ctx = context.shielded_mut().await; + shielded_ctx + .pre_cache_transaction( + context, + &built.masp_tx, + source, + target, + token, + epoch, + ) + .await?; + Ok(Some(built)) } } + // Updates the internal state with the data of the newly generated + // transaction + async fn pre_cache_transaction( + &mut self, + context: &impl Namada, + masp_tx: &Transaction, + source: &TransferSource, + target: &TransferTarget, + token: &Address, + epoch: Epoch, + ) -> Result<(), Error> { + // Need to mock the changed balance keys + let mut changed_balance_keys = BTreeSet::default(); + match (source.effective_address(), target.effective_address()) { + // Shielded transactions don't write balance keys + (MASP, MASP) => (), + (source, target) => { + changed_balance_keys.insert(balance_key(token, &source)); + changed_balance_keys.insert(balance_key(token, &target)); + } + } + + let native_token = query_native_token(context.client()).await?; + let vks: Vec<_> = context + .wallet() + .await + .get_viewing_keys() + .values() + .map(|evk| ExtendedFullViewingKey::from(*evk).fvk.vk) + .collect(); + let last_witnessed_tx = self.tx_note_map.keys().max(); + // This data will be discarded at the next fetch so we don't need to + // populate it accurately + let indexed_tx = last_witnessed_tx.map_or_else( + || IndexedTx { + height: BlockHeight::first(), + index: TxIndex(0), + }, + |indexed| IndexedTx { + height: indexed.height, + index: indexed.index + 1, + }, + ); + self.update_witness_map(indexed_tx, masp_tx)?; + for vk in vks { + self.vk_heights.entry(vk).or_default(); + + self.scan_tx( + indexed_tx, + epoch, + &changed_balance_keys, + masp_tx, + &vk, + native_token.clone(), + )?; + + *self.vk_heights.get_mut(&vk).unwrap() = Some(indexed_tx); + } + self.sync_status = ContextSyncStatus::Speculative; + // Save the speculative state for future usage + self.save().await.map_err(|e| Error::Other(e.to_string()))?; + + Ok(()) + } + /// Obtain the known effects of all accepted shielded and transparent /// transactions. If an owner is specified, then restrict the set to only /// transactions crediting/debiting the given owner. If token is specified, /// then restrict set to only transactions involving the given token. - pub async fn query_tx_deltas( + pub async fn query_tx_deltas( &mut self, client: &C, + io: &IO, query_owner: &Either>, query_token: &Option
, viewing_keys: &HashMap, @@ -2340,7 +2531,8 @@ impl ShieldedContext { .values() .map(|fvk| ExtendedFullViewingKey::from(*fvk).fvk.vk) .collect(); - self.fetch(client, &[], &fvks).await?; + self.fetch(client, &DefaultLogger::new(io), None, 1, &[], &fvks) + .await?; // Save the update state so that future fetches can be short-circuited let _ = self.save().await; // Required for filtering out rejected transactions from Tendermint @@ -3522,6 +3714,8 @@ pub mod fs { /// Shielded context file name const FILE_NAME: &str = "shielded.dat"; const TMP_FILE_NAME: &str = "shielded.tmp"; + const SPECULATIVE_FILE_NAME: &str = "speculative_shielded.dat"; + const SPECULATIVE_TMP_FILE_NAME: &str = "speculative_shielded.tmp"; #[derive(Debug, BorshSerialize, BorshDeserialize, Clone)] /// An implementation of ShieldedUtils for standard filesystems @@ -3552,9 +3746,21 @@ pub mod fs { ); } // Finally initialize a shielded context with the supplied directory + + let sync_status = + if std::fs::read(context_dir.join(SPECULATIVE_FILE_NAME)) + .is_ok() + { + // Load speculative state + ContextSyncStatus::Speculative + } else { + ContextSyncStatus::Confirmed + }; + let utils = Self { context_dir }; ShieldedContext { utils, + sync_status, ..Default::default() } } @@ -3570,6 +3776,8 @@ pub mod fs { #[cfg_attr(feature = "async-send", async_trait::async_trait)] #[cfg_attr(not(feature = "async-send"), async_trait::async_trait(?Send))] + // FIXME: I can probably refactor everything to have the associated + // contextsSycn state on this trait impl ShieldedUtils for FsShieldedUtils { fn local_tx_prover(&self) -> LocalTxProver { if let Ok(params_dir) = env::var(ENV_VAR_MASP_PARAMS_DIR) { @@ -3602,7 +3810,28 @@ pub mod fs { Ok(()) } - /// Save this shielded context into its associated context directory + /// Try to load the last saved speculative shielded context from the + /// given context directory. If this fails, then leave the + /// current context unchanged. + async fn load_speculative( + &self, + ctx: &mut ShieldedContext, + ) -> std::io::Result<()> { + // Try to load shielded context from file + let mut ctx_file = + File::open(self.context_dir.join(SPECULATIVE_FILE_NAME))?; + let mut bytes = Vec::new(); + ctx_file.read_to_end(&mut bytes)?; + // Fill the supplied context with the deserialized object + *ctx = ShieldedContext { + utils: ctx.utils.clone(), + ..ShieldedContext::::deserialize(&mut &bytes[..])? + }; + Ok(()) + } + + /// Save this confirmed shielded context into its associated context + /// directory. At the same time, delete the speculative file if present async fn save( &self, ctx: &ShieldedContext, @@ -3628,14 +3857,111 @@ pub mod fs { // Atomically update the old shielded context file with new data. // Atomicity is required to prevent other client instances from // reading corrupt data. + std::fs::rename(tmp_path, self.context_dir.join(FILE_NAME))?; + + // Remove the speculative file if present since it's state is + // overruled by the confirmed one we just saved + let _ = std::fs::remove_file( + self.context_dir.join(SPECULATIVE_FILE_NAME), + ); + + Ok(()) + } + + // FIXME: refactor, the functions are exactly the same, just pass the + // enum and modify the filename based on that + /// Save this speculative shielded context into its associated context + /// directory + async fn save_speculative( + &self, + ctx: &ShieldedContext, + ) -> std::io::Result<()> { + // TODO: use mktemp crate? + let tmp_path = self.context_dir.join(SPECULATIVE_TMP_FILE_NAME); + { + // First serialize the shielded context into a temporary file. + // Inability to create this file implies a simultaneuous write + // is in progress. In this case, immediately + // fail. This is unproblematic because the data + // intended to be stored can always be re-fetched + // from the blockchain. + let mut ctx_file = OpenOptions::new() + .write(true) + .create_new(true) + .open(tmp_path.clone())?; + let mut bytes = Vec::new(); + ctx.serialize(&mut bytes) + .expect("cannot serialize shielded context"); + ctx_file.write_all(&bytes[..])?; + } + // Atomically update the old shielded context file with new data. + // Atomicity is required to prevent other client instances from + // reading corrupt data. std::fs::rename( - tmp_path.clone(), - self.context_dir.join(FILE_NAME), + tmp_path, + self.context_dir.join(SPECULATIVE_FILE_NAME), )?; - // Finally, remove our temporary file to allow future saving of - // shielded contexts. - std::fs::remove_file(tmp_path)?; Ok(()) } } } + +/// A enum to indicate how to log sync progress depending on +/// whether sync is currently fetch or scanning blocks. +#[derive(Debug, Copy, Clone)] +pub enum ProgressType { + Fetch, + Scan, +} + +pub trait ProgressLogger { + type Fetch: Iterator; + type Scan: Iterator; + + fn io(&self) -> &IO; + + fn fetch(&self, items: I) -> Self::Fetch + where + I: IntoIterator; + + fn scan(&self, items: I) -> Self::Scan + where + I: IntoIterator; +} + +/// The default type for logging sync progress. +#[derive(Debug, Clone)] +pub struct DefaultLogger<'io, IO: Io> { + io: &'io IO, +} + +impl<'io, IO: Io> DefaultLogger<'io, IO> { + pub fn new(io: &'io IO) -> Self { + Self { io } + } +} + +impl<'io, IO: Io> ProgressLogger for DefaultLogger<'io, IO> { + type Fetch = as IntoIterator>::IntoIter; + type Scan = as IntoIterator>::IntoIter; + + fn io(&self) -> &IO { + self.io + } + + fn fetch(&self, items: I) -> Self::Fetch + where + I: IntoIterator, + { + let items: Vec<_> = items.into_iter().collect(); + items.into_iter() + } + + fn scan(&self, items: I) -> Self::Scan + where + I: IntoIterator, + { + let items: Vec<_> = items.into_iter().collect(); + items.into_iter() + } +} diff --git a/crates/sdk/src/signing.rs b/crates/sdk/src/signing.rs index 1519b1ae49..7bc127c68d 100644 --- a/crates/sdk/src/signing.rs +++ b/crates/sdk/src/signing.rs @@ -10,6 +10,7 @@ use masp_primitives::asset_type::AssetType; use masp_primitives::transaction::components::sapling::fees::{ InputView, OutputView, }; +use masp_primitives::transaction::Transaction; use namada_account::{AccountPublicKeysMap, InitAccount, UpdateAccount}; use namada_core::types::address::{ Address, ImplicitAddress, InternalAddress, MASP, @@ -403,8 +404,8 @@ pub async fn init_validator_signing_data( }) } -/// Information about the post-tx balance of the tx's source. Used to correctly -/// handle fee validation in the wrapper tx +/// Information about the post-fee balance of the tx's source. Used to correctly +/// handle balance validation in the inner tx pub struct TxSourcePostBalance { /// The balance of the tx source after the tx has been applied pub post_balance: Amount, @@ -414,19 +415,15 @@ pub struct TxSourcePostBalance { pub token: Address, } -/// 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. -#[allow(clippy::too_many_arguments)] -pub async fn wrap_tx( +/// Validate the fee of the transaction and generate the fee unshielding +/// transaction if needed +pub async fn validate_fee_and_gen_unshield( context: &N, - tx: &mut Tx, args: &args::Tx, - tx_source_balance: Option, - epoch: Epoch, - fee_payer: common::PublicKey, -) -> Result<(), Error> { - let fee_payer_address = Address::from(&fee_payer); + fee_payer: &common::PublicKey, +) -> Result<(DenominatedAmount, TxSourcePostBalance, Option), Error> +{ + let fee_payer_address = Address::from(fee_payer); // Validate fee amount and token let gas_cost_key = parameter_storage::get_gas_cost_key(); let minimum_fee = match rpc::query_storage_value::< @@ -482,27 +479,22 @@ pub async fn wrap_tx( None => validated_minimum_fee, }; - let mut updated_balance = match tx_source_balance { - Some(TxSourcePostBalance { - post_balance: balance, - source, - token, - }) if token == args.fee_token && source == fee_payer_address => balance, - _ => { - let balance_key = balance_key(&args.fee_token, &fee_payer_address); - - rpc::query_storage_value::<_, token::Amount>( - context.client(), - &balance_key, - ) - .await - .unwrap_or_default() - } - }; + let balance_key = balance_key(&args.fee_token, &fee_payer_address); + let balance = rpc::query_storage_value::<_, token::Amount>( + context.client(), + &balance_key, + ) + .await + .unwrap_or_default(); let total_fee = fee_amount.amount() * u64::from(args.gas_limit); + let mut updated_balance = TxSourcePostBalance { + post_balance: balance, + source: fee_payer_address.clone(), + token: args.fee_token.clone(), + }; - let unshield = match total_fee.checked_sub(updated_balance) { + let unshield = match total_fee.checked_sub(balance) { Some(diff) if !diff.is_zero() => { if let Some(spending_key) = args.fee_unshield.clone() { // Unshield funds for fee payment @@ -574,8 +566,7 @@ pub async fn wrap_tx( )); } - updated_balance += total_fee; - Some(transaction) + Some(transaction) } Ok(None) => { if !args.force { @@ -587,6 +578,7 @@ pub async fn wrap_tx( )); } + updated_balance.post_balance = Amount::zero(); None } Err(e) => { @@ -595,7 +587,7 @@ pub async fn wrap_tx( TxSubmitError::FeeUnshieldingError(e.to_string()), )); } - + updated_balance.post_balance = Amount::zero(); None } } @@ -605,9 +597,8 @@ pub async fn wrap_tx( let fee_amount = context.format_amount(&token_addr, total_fee).await; - let balance = context - .format_amount(&token_addr, updated_balance) - .await; + let balance = + context.format_amount(&token_addr, balance).await; return Err(Error::from( TxSubmitError::BalanceTooLowForFees( fee_payer_address, @@ -618,21 +609,38 @@ pub async fn wrap_tx( )); } + updated_balance.post_balance = Amount::zero(); None } } _ => { if args.fee_unshield.is_some() { - display_line!( - context.io(), - "Enough transparent balance to pay fees: the fee \ - unshielding spending key will be ignored" - ); + return Err(Error::Other( + "Enough transparent balance to pay fees: please remove \ + the --gas-spending-key or select a different --gas-payer" + .to_string(), + )); } + updated_balance.post_balance -= total_fee; None } }; + Ok((fee_amount, updated_balance, unshield)) +} + +/// 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. +#[allow(clippy::too_many_arguments)] +pub async fn wrap_tx( + tx: &mut Tx, + args: &args::Tx, + epoch: Epoch, + unshield: Option, + fee_amount: DenominatedAmount, + fee_payer: common::PublicKey, +) -> Result<(), Error> { let unshield_section_hash = unshield.map(|masp_tx| { let section = Section::MaspTx(masp_tx); let mut hasher = sha2::Sha256::new(); diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index a5ff9da0da..a95aa5cc75 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -47,6 +47,7 @@ use namada_ibc::storage::channel_key; use namada_proof_of_stake::parameters::PosParams; use namada_proof_of_stake::types::{CommissionPair, ValidatorState}; use namada_token::storage_key::balance_key; +use namada_token::DenominatedAmount; use namada_tx::data::pgf::UpdateStewardCommission; use namada_tx::data::{pos, ResultCode, TxResult}; pub use namada_tx::{Signature, *}; @@ -62,7 +63,7 @@ use crate::rpc::{ self, query_wasm_code_hash, validate_amount, InnerTxResult, TxBroadcastData, TxResponse, }; -use crate::signing::{self, SigningTxData, TxSourcePostBalance}; +use crate::signing::{self, validate_fee_and_gen_unshield, SigningTxData}; use crate::tendermint_rpc::endpoint::broadcast::tx_sync::Response; use crate::tendermint_rpc::error::Error as RpcError; use crate::wallet::WalletIo; @@ -181,18 +182,18 @@ pub fn dump_tx(io: &IO, args: &args::Tx, tx: Tx) { /// Prepare a transaction for signing and submission by adding a wrapper header /// to it. #[allow(clippy::too_many_arguments)] -pub async fn prepare_tx( - context: &impl Namada, +pub async fn prepare_tx( + client: &C, args: &args::Tx, tx: &mut Tx, + unshield: Option, + fee_amount: DenominatedAmount, fee_payer: common::PublicKey, - tx_source_balance: Option, ) -> Result<()> { if !args.dry_run { - let epoch = rpc::query_epoch(context.client()).await?; + let epoch = rpc::query_epoch(client).await?; - signing::wrap_tx(context, tx, args, tx_source_balance, epoch, fee_payer) - .await + signing::wrap_tx(tx, args, epoch, unshield, fee_amount, fee_payer).await } else { Ok(()) } @@ -286,6 +287,9 @@ pub async fn build_reveal_pk( let signing_data = signing::aux_signing_data(context, args, None, Some(public_key.into())) .await?; + let (fee_amount, _, unshield) = + validate_fee_and_gen_unshield(context, args, &signing_data.fee_payer) + .await?; build( context, @@ -293,8 +297,9 @@ pub async fn build_reveal_pk( args.tx_reveal_code_path.clone(), public_key, do_nothing, + unshield, + fee_amount, &signing_data.fee_payer, - None, ) .await .map(|tx| (tx, signing_data)) @@ -575,6 +580,12 @@ pub async fn build_validator_commission_change( default_signer, ) .await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + context, + tx_args, + &signing_data.fee_payer, + ) + .await?; let epoch = rpc::query_epoch(context.client()).await?; @@ -664,8 +675,9 @@ pub async fn build_validator_commission_change( tx_code_path.clone(), data, do_nothing, + unshield, + fee_amount, &signing_data.fee_payer, - None, ) .await .map(|tx| (tx, signing_data)) @@ -694,6 +706,12 @@ pub async fn build_validator_metadata_change( default_signer, ) .await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + context, + tx_args, + &signing_data.fee_payer, + ) + .await?; let epoch = rpc::query_epoch(context.client()).await?; @@ -797,8 +815,9 @@ pub async fn build_validator_metadata_change( tx_code_path.clone(), data, do_nothing, + unshield, + fee_amount, &signing_data.fee_payer, - None, ) .await .map(|tx| (tx, signing_data)) @@ -822,6 +841,12 @@ pub async fn build_update_steward_commission( default_signer, ) .await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + context, + tx_args, + &signing_data.fee_payer, + ) + .await?; if !rpc::is_steward(context.client(), steward).await && !tx_args.force { edisplay_line!( @@ -858,8 +883,9 @@ pub async fn build_update_steward_commission( tx_code_path.clone(), data, do_nothing, + unshield, + fee_amount, &signing_data.fee_payer, - None, ) .await .map(|tx| (tx, signing_data)) @@ -882,6 +908,12 @@ pub async fn build_resign_steward( default_signer, ) .await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + context, + tx_args, + &signing_data.fee_payer, + ) + .await?; if !rpc::is_steward(context.client(), steward).await && !tx_args.force { edisplay_line!( @@ -900,8 +932,9 @@ pub async fn build_resign_steward( tx_code_path.clone(), steward.clone(), do_nothing, + unshield, + fee_amount, &signing_data.fee_payer, - None, ) .await .map(|tx| (tx, signing_data)) @@ -924,6 +957,12 @@ pub async fn build_unjail_validator( default_signer, ) .await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + context, + tx_args, + &signing_data.fee_payer, + ) + .await?; if !rpc::is_validator(context.client(), validator).await? { edisplay_line!( @@ -1002,8 +1041,9 @@ pub async fn build_unjail_validator( tx_code_path.clone(), validator.clone(), do_nothing, + unshield, + fee_amount, &signing_data.fee_payer, - None, ) .await .map(|tx| (tx, signing_data)) @@ -1026,6 +1066,12 @@ pub async fn build_deactivate_validator( default_signer, ) .await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + context, + tx_args, + &signing_data.fee_payer, + ) + .await?; // Check if the validator address is actually a validator if !rpc::is_validator(context.client(), validator).await? { @@ -1073,8 +1119,9 @@ pub async fn build_deactivate_validator( tx_code_path.clone(), validator.clone(), do_nothing, + unshield, + fee_amount, &signing_data.fee_payer, - None, ) .await .map(|tx| (tx, signing_data)) @@ -1097,6 +1144,12 @@ pub async fn build_reactivate_validator( default_signer, ) .await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + context, + tx_args, + &signing_data.fee_payer, + ) + .await?; // Check if the validator address is actually a validator if !rpc::is_validator(context.client(), validator).await? { @@ -1143,8 +1196,9 @@ pub async fn build_reactivate_validator( tx_code_path.clone(), validator.clone(), do_nothing, + unshield, + fee_amount, &signing_data.fee_payer, - None, ) .await .map(|tx| (tx, signing_data)) @@ -1313,6 +1367,12 @@ pub async fn build_redelegation( default_signer, ) .await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + context, + tx_args, + &signing_data.fee_payer, + ) + .await?; let data = pos::Redelegation { src_validator, @@ -1327,8 +1387,9 @@ pub async fn build_redelegation( tx_code_path.clone(), data, do_nothing, + unshield, + fee_amount, &signing_data.fee_payer, - None, ) .await .map(|tx| (tx, signing_data)) @@ -1353,6 +1414,12 @@ pub async fn build_withdraw( default_signer, ) .await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + context, + tx_args, + &signing_data.fee_payer, + ) + .await?; let epoch = rpc::query_epoch(context.client()).await?; @@ -1410,8 +1477,9 @@ pub async fn build_withdraw( tx_code_path.clone(), data, do_nothing, + unshield, + fee_amount, &signing_data.fee_payer, - None, ) .await .map(|tx| (tx, signing_data)) @@ -1436,6 +1504,12 @@ pub async fn build_claim_rewards( default_signer, ) .await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + context, + tx_args, + &signing_data.fee_payer, + ) + .await?; // Check that the validator address is actually a validator let validator = @@ -1458,8 +1532,9 @@ pub async fn build_claim_rewards( tx_code_path.clone(), data, do_nothing, + unshield, + fee_amount, &signing_data.fee_payer, - None, ) .await .map(|tx| (tx, signing_data)) @@ -1510,6 +1585,12 @@ pub async fn build_unbond( default_signer, ) .await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + context, + tx_args, + &signing_data.fee_payer, + ) + .await?; // Check the source's current bond amount let bond_source = source.clone().unwrap_or_else(|| validator.clone()); @@ -1560,8 +1641,9 @@ pub async fn build_unbond( tx_code_path.clone(), data, do_nothing, + unshield, + fee_amount, &signing_data.fee_payer, - None, ) .await?; Ok((tx, signing_data, latest_withdrawal_pre)) @@ -1710,28 +1792,61 @@ pub async fn build_bond( default_signer, ) .await?; + let (fee_amount, updated_balance, unshield) = + validate_fee_and_gen_unshield( + context, + tx_args, + &signing_data.fee_payer, + ) + .await?; // Check bond's source (source for delegation or validator for self-bonds) // balance let bond_source = source.as_ref().unwrap_or(&validator); let native_token = context.native_token(); - let balance_key = balance_key(&native_token, bond_source); - - // TODO Should we state the same error message for the native token? - let post_balance = check_balance_too_low_err( - &native_token, - bond_source, - *amount, - balance_key, - tx_args.force, - context, - ) - .await?; - let tx_source_balance = Some(TxSourcePostBalance { - post_balance, - source: bond_source.clone(), - token: native_token, - }); + if &updated_balance.source == bond_source + && updated_balance.token == native_token + { + if *amount > updated_balance.post_balance { + if tx_args.force { + edisplay_line!( + context.io(), + "The balance of the source {} of token {} is lower than \ + the amount to be transferred. Amount to transfer is {} \ + and the balance is {}.", + bond_source, + native_token, + context.format_amount(&native_token, *amount).await, + context + .format_amount( + &native_token, + updated_balance.post_balance + ) + .await, + ); + } else { + return Err(Error::from(TxSubmitError::BalanceTooLow( + bond_source.clone(), + native_token, + amount.to_string_native(), + updated_balance.post_balance.to_string_native(), + ))); + } + } + } else { + let balance_key = balance_key(&native_token, bond_source); + + // TODO Should we state the same error message for the native token? + check_balance_too_low_err( + &native_token, + bond_source, + *amount, + balance_key, + tx_args.force, + context, + ) + .await?; + } let data = pos::Bond { validator, @@ -1745,8 +1860,9 @@ pub async fn build_bond( tx_code_path.clone(), data, do_nothing, + unshield, + fee_amount, &signing_data.fee_payer, - tx_source_balance, ) .await .map(|tx| (tx, signing_data)) @@ -1773,6 +1889,9 @@ pub async fn build_default_proposal( default_signer, ) .await?; + let (fee_amount, _updated_balance, unshield) = + validate_fee_and_gen_unshield(context, tx, &signing_data.fee_payer) + .await?; let init_proposal_data = InitProposalData::try_from(proposal.clone()) .map_err(|e| TxSubmitError::InvalidProposal(e.to_string()))?; @@ -1791,14 +1910,16 @@ pub async fn build_default_proposal( }; Ok(()) }; + // TODO: need to pay the fee to submit a proposal, check enough balance build( context, tx, tx_code_path.clone(), init_proposal_data, push_data, + unshield, + fee_amount, &signing_data.fee_payer, - None, // TODO: need to pay the fee to submit a proposal ) .await .map(|tx| (tx, signing_data)) @@ -1826,6 +1947,9 @@ pub async fn build_vote_proposal( default_signer.clone(), ) .await?; + let (fee_amount, _, unshield) = + validate_fee_and_gen_unshield(context, tx, &signing_data.fee_payer) + .await?; let proposal_vote = ProposalVote::try_from(vote.clone()) .map_err(|_| TxSubmitError::InvalidProposalVote)?; @@ -1884,8 +2008,9 @@ pub async fn build_vote_proposal( tx_code_path.clone(), data, do_nothing, + unshield, + fee_amount, &signing_data.fee_payer, - None, ) .await .map(|tx| (tx, signing_data)) @@ -1912,7 +2037,11 @@ pub async fn build_pgf_funding_proposal( default_signer, ) .await?; + let (fee_amount, _updated_balance, unshield) = + validate_fee_and_gen_unshield(context, tx, &signing_data.fee_payer) + .await?; + // TODO: need to pay the fee to submit a proposal, check enough balance let init_proposal_data = InitProposalData::try_from(proposal.clone()) .map_err(|e| TxSubmitError::InvalidProposal(e.to_string()))?; @@ -1928,8 +2057,9 @@ pub async fn build_pgf_funding_proposal( tx_code_path.clone(), init_proposal_data, add_section, + unshield, + fee_amount, &signing_data.fee_payer, - None, // TODO: need to pay the fee to submit a proposal ) .await .map(|tx| (tx, signing_data)) @@ -1956,7 +2086,11 @@ pub async fn build_pgf_stewards_proposal( default_signer, ) .await?; + let (fee_amount, _updated_balance, unshield) = + validate_fee_and_gen_unshield(context, tx, &signing_data.fee_payer) + .await?; + // TODO: need to pay the fee to submit a proposal, check enough balance let init_proposal_data = InitProposalData::try_from(proposal.clone()) .map_err(|e| TxSubmitError::InvalidProposal(e.to_string()))?; @@ -1973,8 +2107,9 @@ pub async fn build_pgf_stewards_proposal( tx_code_path.clone(), init_proposal_data, add_section, + unshield, + fee_amount, &signing_data.fee_payer, - None, // TODO: need to pay the fee to submit a proposal ) .await .map(|tx| (tx, signing_data)) @@ -1993,6 +2128,14 @@ pub async fn build_ibc_transfer( Some(source.clone()), ) .await?; + let (fee_amount, updated_balance, unshield) = + validate_fee_and_gen_unshield( + context, + &args.tx, + &signing_data.fee_payer, + ) + .await?; + // Check that the source address exists on chain let source = source_exists_or_err(source.clone(), args.tx.force, context).await?; @@ -2010,23 +2153,49 @@ pub async fn build_ibc_transfer( ))); } - // Check source balance - let balance_key = balance_key(&args.token, &source); - - let post_balance = check_balance_too_low_err( - &args.token, - &source, - validated_amount.amount(), - balance_key, - args.tx.force, - context, - ) - .await?; - let tx_source_balance = Some(TxSourcePostBalance { - post_balance, - source: source.clone(), - token: args.token.clone(), - }); + if updated_balance.source == source && updated_balance.token == args.token { + if validated_amount.amount() > updated_balance.post_balance { + if args.tx.force { + edisplay_line!( + context.io(), + "The balance of the source {} of token {} is lower than \ + the amount to be transferred. Amount to transfer is {} \ + and the balance is {}.", + source, + args.token, + context + .format_amount(&args.token, validated_amount.amount()) + .await, + context + .format_amount( + &args.token, + updated_balance.post_balance + ) + .await, + ); + } else { + return Err(Error::from(TxSubmitError::BalanceTooLow( + source.clone(), + args.token.clone(), + validated_amount.amount().to_string_native(), + updated_balance.post_balance.to_string_native(), + ))); + } + } + } else { + // Check source balance + let balance_key = balance_key(&args.token, &source); + + check_balance_too_low_err( + &args.token, + &source, + validated_amount.amount(), + balance_key, + args.tx.force, + context, + ) + .await?; + } let tx_code_hash = query_wasm_code_hash(context, args.tx_code_path.to_str().unwrap()) @@ -2149,11 +2318,12 @@ pub async fn build_ibc_transfer( .add_serialized_data(data); prepare_tx( - context, + context.client(), &args.tx, &mut tx, + unshield, + fee_amount, signing_data.fee_payer.clone(), - tx_source_balance, ) .await?; @@ -2168,21 +2338,16 @@ pub async fn build( path: PathBuf, data: D, on_tx: F, + unshield: Option, + fee_amount: DenominatedAmount, gas_payer: &common::PublicKey, - tx_source_balance: Option, ) -> Result where F: FnOnce(&mut Tx, &mut D) -> Result<()>, D: BorshSerialize, { build_pow_flag( - context, - tx_args, - path, - data, - on_tx, - gas_payer, - tx_source_balance, + context, tx_args, path, data, on_tx, unshield, fee_amount, gas_payer, ) .await } @@ -2194,8 +2359,9 @@ async fn build_pow_flag( path: PathBuf, mut data: D, on_tx: F, + unshield: Option, + fee_amount: DenominatedAmount, gas_payer: &common::PublicKey, - tx_source_balance: Option, ) -> Result where F: FnOnce(&mut Tx, &mut D) -> Result<()>, @@ -2222,11 +2388,12 @@ where .add_data(data); prepare_tx( - context, + context.client(), tx_args, &mut tx_builder, + unshield, + fee_amount, gas_payer.clone(), - tx_source_balance, ) .await?; Ok(tx_builder) @@ -2301,6 +2468,14 @@ pub async fn build_transfer( ) .await?; + let (fee_amount, updated_balance, unshield) = + validate_fee_and_gen_unshield( + context, + &args.tx, + &signing_data.fee_payer, + ) + .await?; + let source = args.source.effective_address(); let target = args.target.effective_address(); @@ -2308,8 +2483,6 @@ pub async fn build_transfer( source_exists_or_err(source.clone(), args.tx.force, context).await?; // Check that the target address exists on chain target_exists_or_err(target.clone(), args.tx.force, context).await?; - // Check source balance - let balance_key = balance_key(&args.token, &source); // validate the amount given let validated_amount = @@ -2317,20 +2490,49 @@ pub async fn build_transfer( .await?; args.amount = InputAmount::Validated(validated_amount); - let post_balance = check_balance_too_low_err( - &args.token, - &source, - validated_amount.amount(), - balance_key, - args.tx.force, - context, - ) - .await?; - let tx_source_balance = Some(TxSourcePostBalance { - post_balance, - source: source.clone(), - token: args.token.clone(), - }); + + // Check source balance + if updated_balance.source == source && updated_balance.token == args.token { + if validated_amount.amount() > updated_balance.post_balance { + if args.tx.force { + edisplay_line!( + context.io(), + "The balance of the source {} of token {} is lower than \ + the amount to be transferred. Amount to transfer is {} \ + and the balance is {}.", + source, + args.token, + context + .format_amount(&args.token, validated_amount.amount()) + .await, + context + .format_amount( + &args.token, + updated_balance.post_balance + ) + .await, + ); + } else { + return Err(Error::from(TxSubmitError::BalanceTooLow( + source.clone(), + args.token.clone(), + validated_amount.amount().to_string_native(), + updated_balance.post_balance.to_string_native(), + ))); + } + } + } else { + let balance_key = balance_key(&args.token, &source); + check_balance_too_low_err( + &args.token, + &source, + validated_amount.amount(), + balance_key, + args.tx.force, + context, + ) + .await?; + } let masp_addr = MASP; @@ -2407,8 +2609,9 @@ pub async fn build_transfer( args.tx_code_path.clone(), transfer, add_shielded, + unshield, + fee_amount, &signing_data.fee_payer, - tx_source_balance, ) .await?; Ok((tx, signing_data, shielded_tx_epoch)) @@ -2472,6 +2675,12 @@ pub async fn build_init_account( ) -> Result<(Tx, SigningTxData)> { let signing_data = signing::aux_signing_data(context, tx_args, None, None).await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + context, + tx_args, + &signing_data.fee_payer, + ) + .await?; let vp_code_hash = query_wasm_code_hash_buf(context, vp_code_path).await?; @@ -2509,8 +2718,9 @@ pub async fn build_init_account( tx_code_path.clone(), data, add_code_hash, + unshield, + fee_amount, &signing_data.fee_payer, - None, ) .await .map(|tx| (tx, signing_data)) @@ -2536,6 +2746,12 @@ pub async fn build_update_account( default_signer, ) .await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + context, + tx_args, + &signing_data.fee_payer, + ) + .await?; let addr = if let Some(account) = rpc::get_account_info(context.client(), addr).await? @@ -2596,8 +2812,9 @@ pub async fn build_update_account( tx_code_path.clone(), data, add_code_hash, + unshield, + fee_amount, &signing_data.fee_payer, - None, ) .await .map(|tx| (tx, signing_data)) @@ -2622,6 +2839,12 @@ pub async fn build_custom( default_signer, ) .await?; + let (fee_amount, _, unshield) = validate_fee_and_gen_unshield( + context, + tx_args, + &signing_data.fee_payer, + ) + .await?; let mut tx = if let Some(serialized_tx) = serialized_tx { Tx::deserialize(serialized_tx.as_ref()).map_err(|_| { @@ -2646,11 +2869,12 @@ pub async fn build_custom( }; prepare_tx( - context, + context.client(), tx_args, &mut tx, + unshield, + fee_amount, signing_data.fee_payer.clone(), - None, ) .await?; @@ -2878,7 +3102,7 @@ async fn target_exists_or_err( /// Checks the balance at the given address is enough to transfer the /// given amount, along with the balance even existing. Force -/// overrides this. Returns the updated balance for fee check if necessary +/// overrides this. async fn check_balance_too_low_err( token: &Address, source: &Address, @@ -2886,7 +3110,7 @@ async fn check_balance_too_low_err( balance_key: storage::Key, force: bool, context: &N, -) -> Result { +) -> Result<()> { match rpc::query_storage_value::( context.client(), &balance_key, @@ -2894,7 +3118,7 @@ async fn check_balance_too_low_err( .await { Ok(balance) => match balance.checked_sub(amount) { - Some(diff) => Ok(diff), + Some(_) => Ok(()), None => { if force { edisplay_line!( @@ -2907,7 +3131,7 @@ async fn check_balance_too_low_err( context.format_amount(token, amount).await, context.format_amount(token, balance).await, ); - Ok(token::Amount::zero()) + Ok(()) } else { Err(Error::from(TxSubmitError::BalanceTooLow( source.clone(), @@ -2928,7 +3152,7 @@ async fn check_balance_too_low_err( source, token ); - Ok(token::Amount::zero()) + Ok(()) } else { Err(Error::from(TxSubmitError::NoBalanceForToken( source.clone(), diff --git a/crates/sdk/src/wallet/mod.rs b/crates/sdk/src/wallet/mod.rs index 5fae3db2ad..dc2fd5aafd 100644 --- a/crates/sdk/src/wallet/mod.rs +++ b/crates/sdk/src/wallet/mod.rs @@ -381,7 +381,7 @@ impl Wallet { /// Find the viewing key with the given alias in the wallet and return it pub fn find_viewing_key( - &mut self, + &self, alias: impl AsRef, ) -> Result<&ExtendedViewingKey, FindKeyError> { self.store.find_viewing_key(alias.as_ref()).ok_or_else(|| { diff --git a/crates/shielded_token/src/storage_key.rs b/crates/shielded_token/src/storage_key.rs index e58ffe93a0..51917bae5f 100644 --- a/crates/shielded_token/src/storage_key.rs +++ b/crates/shielded_token/src/storage_key.rs @@ -85,8 +85,13 @@ pub fn is_masp_allowed_key(key: &storage::Key) -> bool { [ DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key), - DbKeySeg::StringSeg(_nullifier), - ] if *addr == address::MASP && key == MASP_NULLIFIERS_KEY => true, + DbKeySeg::StringSeg(_value), + ] if *addr == address::MASP + && (key == MASP_NULLIFIERS_KEY + || key == MASP_NOTE_COMMITMENT_ANCHOR_PREFIX) => + { + true + } _ => false, } } @@ -104,10 +109,19 @@ pub fn is_masp_nullifier_key(key: &storage::Key) -> bool { matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), - .. + DbKeySeg::StringSeg(_nullifier) ] if *addr == address::MASP && prefix == MASP_NULLIFIERS_KEY) } +/// Check if the given storage key is a masp commtimentr tree anchor key +pub fn is_masp_commitment_anchor_key(key: &storage::Key) -> bool { + matches!(&key.segments[..], + [DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::StringSeg(_anchor) + ] if *addr == address::MASP && prefix == MASP_NOTE_COMMITMENT_ANCHOR_PREFIX) +} + /// Get a key for a masp pin pub fn masp_pin_tx_key(key: &str) -> storage::Key { storage::Key::from(address::MASP.to_db_key()) diff --git a/crates/shielded_token/src/utils.rs b/crates/shielded_token/src/utils.rs index 4dfbaa9e89..dc2d0816be 100644 --- a/crates/shielded_token/src/utils.rs +++ b/crates/shielded_token/src/utils.rs @@ -7,7 +7,8 @@ use namada_core::types::storage::IndexedTx; use namada_storage::{Error, Result, StorageRead, StorageWrite}; use crate::storage_key::{ - masp_commitment_tree_key, masp_nullifier_key, masp_pin_tx_key, + masp_commitment_anchor_key, masp_commitment_tree_key, masp_nullifier_key, + masp_pin_tx_key, }; // Writes the nullifiers of the provided masp transaction to storage @@ -50,7 +51,9 @@ pub fn update_note_commitment_tree( })?; } + let anchor_key = masp_commitment_anchor_key(commitment_tree.root()); ctx.write(&tree_key, commitment_tree)?; + ctx.write(&anchor_key, ())?; } } diff --git a/crates/tests/src/e2e/ibc_tests.rs b/crates/tests/src/e2e/ibc_tests.rs index 4cd825b8c8..21bac52f3d 100644 --- a/crates/tests/src/e2e/ibc_tests.rs +++ b/crates/tests/src/e2e/ibc_tests.rs @@ -1130,7 +1130,7 @@ fn transfer_on_chain( "--node", &rpc, ]; - let mut client = run!(test, Bin::Client, tx_args, Some(40))?; + let mut client = run!(test, Bin::Client, tx_args, Some(120))?; client.exp_string(TX_ACCEPTED)?; client.exp_string(TX_APPLIED_SUCCESS)?; client.assert_success(); @@ -1297,6 +1297,16 @@ fn shielded_transfer( // Send a token to the shielded address on Chain A transfer_on_chain(test_a, ALBERT, AA_PAYMENT_ADDRESS, BTC, 10, ALBERT_KEY)?; + let rpc = get_actor_rpc(test_a, Who::Validator(0)); + let tx_args = vec![ + "shielded-sync", + "--viewing-keys", + AA_VIEWING_KEY, + "--node", + &rpc, + ]; + let mut client = run!(test_a, Bin::Client, tx_args, Some(120))?; + client.assert_success(); // Send a token from SP(A) on Chain A to PA(B) on Chain B let amount = Amount::native_whole(10).to_string_native(); @@ -1899,6 +1909,15 @@ fn check_shielded_balances( let token_addr = find_address(test_a, BTC)?.to_string(); std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); let rpc_b = get_actor_rpc(test_b, Who::Validator(0)); + let tx_args = vec![ + "shielded-sync", + "--viewing-keys", + AB_VIEWING_KEY, + "--node", + &rpc_b, + ]; + let mut client = run!(test_b, Bin::Client, tx_args, Some(120))?; + client.assert_success(); let ibc_denom = format!("{dest_port_id}/{dest_channel_id}/btc"); let query_args = vec![ "balance", diff --git a/crates/tests/src/e2e/ledger_tests.rs b/crates/tests/src/e2e/ledger_tests.rs index 9c904e095c..2f9e7d4da5 100644 --- a/crates/tests/src/e2e/ledger_tests.rs +++ b/crates/tests/src/e2e/ledger_tests.rs @@ -694,6 +694,8 @@ fn ledger_txs_and_queries() -> Result<()> { /// operation is successful /// 2. Test that a tx requesting a disposable signer /// providing an insufficient unshielding fails +/// 3. Submit another transaction with valid fee unshielding and an inner +/// shielded transfer with the same source #[test] fn wrapper_disposable_signer() -> Result<()> { // Download the shielded pool parameters before starting node @@ -739,6 +741,16 @@ fn wrapper_disposable_signer() -> Result<()> { client.exp_string(TX_APPLIED_SUCCESS)?; let _ep1 = epoch_sleep(&test, &validator_one_rpc, 720)?; + let tx_args = vec![ + "shielded-sync", + "--viewing-keys", + AA_VIEWING_KEY, + "--node", + &validator_one_rpc, + ]; + let mut client = run!(test, Bin::Client, tx_args, Some(120))?; + client.assert_success(); + let tx_args = vec![ "transfer", "--source", @@ -749,6 +761,8 @@ fn wrapper_disposable_signer() -> Result<()> { NAM, "--amount", "1", + "--gas-limit", + "20000", "--gas-spending-key", A_SPENDING_KEY, "--disposable-gas-payer", @@ -760,6 +774,9 @@ fn wrapper_disposable_signer() -> Result<()> { client.exp_string(TX_ACCEPTED)?; client.exp_string(TX_APPLIED_SUCCESS)?; let _ep1 = epoch_sleep(&test, &validator_one_rpc, 720)?; + let tx_args = vec!["shielded-sync", "--node", &validator_one_rpc]; + let mut client = run!(test, Bin::Client, tx_args, Some(120))?; + client.assert_success(); let tx_args = vec![ "transfer", "--source", @@ -770,6 +787,8 @@ fn wrapper_disposable_signer() -> Result<()> { NAM, "--amount", "1", + "--gas-limit", + "20000", "--gas-price", "90000000", "--gas-spending-key", @@ -785,6 +804,32 @@ fn wrapper_disposable_signer() -> Result<()> { let mut client = run!(test, Bin::Client, tx_args, Some(720))?; client.exp_string("Error while processing transaction's fees")?; + // Try another valid fee unshielding and masp transaction in the same tx, + // with the same source. This tests that the client can properly fetch data + // and construct these kind of transactions + let _ep1 = epoch_sleep(&test, &validator_one_rpc, 720)?; + let tx_args = vec![ + "transfer", + "--source", + A_SPENDING_KEY, + "--target", + AB_PAYMENT_ADDRESS, + "--token", + NAM, + "--amount", + "1", + "--gas-limit", + "20000", + "--gas-spending-key", + A_SPENDING_KEY, + "--disposable-gas-payer", + "--ledger-address", + &validator_one_rpc, + ]; + let mut client = run!(test, Bin::Client, tx_args, Some(720))?; + + client.exp_string(TX_ACCEPTED)?; + client.exp_string(TX_APPLIED_SUCCESS)?; Ok(()) } diff --git a/crates/tests/src/integration/masp.rs b/crates/tests/src/integration/masp.rs index deef3c1061..26949373c2 100644 --- a/crates/tests/src/integration/masp.rs +++ b/crates/tests/src/integration/masp.rs @@ -4,9 +4,11 @@ use std::str::FromStr; use color_eyre::eyre::Result; use color_eyre::owo_colors::OwoColorize; use namada::state::StorageWrite; -use namada::token; +use namada::token::{self, DenominatedAmount}; use namada_apps::node::ledger::shell::testing::client::run; +use namada_apps::node::ledger::shell::testing::node::NodeResults; use namada_apps::node::ledger::shell::testing::utils::{Bin, CapturedOutput}; +use namada_apps::wallet::defaults::christel_keypair; use namada_core::types::dec::Dec; use namada_sdk::masp::fs::FsShieldedUtils; use test_log::test; @@ -54,6 +56,21 @@ fn masp_incentives() -> Result<()> { )?; node.assert_success(); + // sync the shielded context + run( + &node, + Bin::Client, + vec![ + "shielded-sync", + "--viewing-keys", + AA_VIEWING_KEY, + AB_VIEWING_KEY, + "--node", + validator_one_rpc, + ], + )?; + node.assert_success(); + // Assert BTC balance at VK(A) is 1 let captured = CapturedOutput::of(|| { run( @@ -95,6 +112,14 @@ fn masp_incentives() -> Result<()> { // Wait till epoch boundary node.next_epoch(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); + // Assert BTC balance at VK(A) is still 1 let captured = CapturedOutput::of(|| { run( @@ -157,6 +182,14 @@ fn masp_incentives() -> Result<()> { // Wait till epoch boundary node.next_epoch(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); + // Assert BTC balance at VK(A) is still 1 let captured = CapturedOutput::of(|| { run( @@ -239,6 +272,14 @@ fn masp_incentives() -> Result<()> { )?; node.assert_success(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); + // Assert ETH balance at VK(B) is 0.001 let captured = CapturedOutput::of(|| { run( @@ -280,6 +321,14 @@ fn masp_incentives() -> Result<()> { // Wait till epoch boundary node.next_epoch(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); + // Assert ETH balance at VK(B) is still 0.001 let captured = CapturedOutput::of(|| { run( @@ -341,7 +390,6 @@ fn masp_incentives() -> Result<()> { // Wait till epoch boundary node.next_epoch(); - // Send 0.001 ETH from SK(B) to Christel run( &node, @@ -364,6 +412,14 @@ fn masp_incentives() -> Result<()> { )?; node.assert_success(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); + // Assert ETH balance at VK(B) is 0 let captured = CapturedOutput::of(|| { run( @@ -384,6 +440,13 @@ fn masp_incentives() -> Result<()> { assert!(captured.contains("No shielded eth balance found")); node.next_epoch(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); // Assert VK(B) retains the NAM rewards dispensed in the correct // amount. @@ -406,6 +469,13 @@ fn masp_incentives() -> Result<()> { assert!(captured.contains("nam: 0.719514")); node.next_epoch(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); // Assert NAM balance at MASP pool is // the accumulation of rewards from the shielded assets (BTC and ETH) @@ -452,6 +522,14 @@ fn masp_incentives() -> Result<()> { )?; node.assert_success(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); + // Assert BTC balance at VK(A) is 0 let captured = CapturedOutput::of(|| { run( @@ -512,6 +590,13 @@ fn masp_incentives() -> Result<()> { // Wait till epoch boundary node.next_epoch(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); // Assert NAM balance at VK(A) is the rewards dispensed earlier // (since VK(A) has no shielded assets, no further rewards should @@ -578,7 +663,13 @@ fn masp_incentives() -> Result<()> { // Wait till epoch boundary to prevent conversion expiry during transaction // construction node.next_epoch(); - + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); // Send all NAM rewards from SK(B) to Christel run( &node, @@ -603,7 +694,13 @@ fn masp_incentives() -> Result<()> { // Wait till epoch boundary node.next_epoch(); - + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); // Send all NAM rewards from SK(A) to Bertha run( &node, @@ -626,6 +723,14 @@ fn masp_incentives() -> Result<()> { )?; node.assert_success(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); + // Assert NAM balance at VK(A) is 0 let captured = CapturedOutput::of(|| { run( @@ -645,6 +750,13 @@ fn masp_incentives() -> Result<()> { assert!(captured.result.is_ok()); assert!(captured.contains("No shielded nam balance found")); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); // Assert NAM balance at VK(B) is 0 let captured = CapturedOutput::of(|| { run( @@ -750,7 +862,20 @@ fn spend_unconverted_asset_type() -> Result<()> { for _ in 0..5 { node.next_epoch(); } - + // sync the shielded context + run( + &node, + Bin::Client, + vec![ + "shielded-sync", + "--viewing-keys", + AA_VIEWING_KEY, + AB_VIEWING_KEY, + "--node", + validator_one_rpc, + ], + )?; + node.assert_success(); // 4. Check the shielded balance let captured = CapturedOutput::of(|| { run( @@ -812,6 +937,20 @@ fn masp_pinned_txs() -> Result<()> { // Wait till epoch boundary let _ep0 = node.next_epoch(); + // sync shielded context + run( + &node, + Bin::Client, + vec![ + "shielded-sync", + "--viewing-keys", + AC_VIEWING_KEY, + "--node", + validator_one_rpc, + ], + )?; + node.assert_success(); + // Assert PPA(C) cannot be recognized by incorrect viewing key let captured = CapturedOutput::with_input(AB_VIEWING_KEY.into()).run(|| { @@ -881,6 +1020,14 @@ fn masp_pinned_txs() -> Result<()> { // This makes it more consistent for some reason? let _ep2 = node.next_epoch(); + // sync shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); + // Assert PPA(C) has the 20 BTC transaction pinned to it let captured = CapturedOutput::with_input(AC_VIEWING_KEY.into()).run(|| { @@ -924,6 +1071,14 @@ fn masp_pinned_txs() -> Result<()> { // Wait till epoch boundary let _ep1 = node.next_epoch(); + // sync shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); + // Assert PPA(C) does not NAM pinned to it on epoch boundary let captured = CapturedOutput::with_input(AC_VIEWING_KEY.into()).run(|| { @@ -978,6 +1133,22 @@ fn masp_txs_and_queries() -> Result<()> { let (mut node, _services) = setup::setup()?; _ = node.next_epoch(); + + // add necessary viewing keys to shielded context + run( + &node, + Bin::Client, + vec![ + "shielded-sync", + "--viewing-keys", + AA_VIEWING_KEY, + AB_VIEWING_KEY, + "--node", + validator_one_rpc, + ], + )?; + node.assert_success(); + let txs_args = vec![ // 0. Attempt to spend 10 BTC at SK(A) to PA(B) ( @@ -1188,11 +1359,22 @@ fn masp_txs_and_queries() -> Result<()> { ]; for (tx_args, tx_result) in &txs_args { - // We ensure transfers don't cross epoch boundaries. - if tx_args[0] == "transfer" { + node.assert_success(); + // there is no need to dry run balance queries + let dry_run_args = if tx_args[0] == "transfer" { + // We ensure transfers don't cross epoch boundaries. node.next_epoch(); - } - for &dry_run in &[true, false] { + vec![true, false] + } else { + vec![false] + }; + for &dry_run in &dry_run_args { + // sync shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; let tx_args = if dry_run && tx_args[0] == "transfer" { vec![tx_args.clone(), vec!["--dry-run"]].concat() } else { @@ -1268,7 +1450,9 @@ fn masp_txs_and_queries() -> Result<()> { /// 1. Shield some tokens to reduce the unshielded balance /// 2. Submit a new wrapper with a valid unshielding tx and assert /// success -/// 3. Submit a new wrapper with an invalid unshielding tx and assert the +/// 3. Submit another transaction with valid fee unshielding and an inner +/// shielded transfer with the same source +/// 4. Submit a new wrapper with an invalid unshielding tx and assert the /// failure #[test] fn wrapper_fee_unshielding() -> Result<()> { @@ -1279,6 +1463,35 @@ fn wrapper_fee_unshielding() -> Result<()> { let (mut node, _services) = setup::setup()?; _ = node.next_epoch(); + // Add the relevant viewing keys to the wallet otherwise the shielded + // context won't precache the masp data + run( + &node, + Bin::Wallet, + vec![ + "add", + "--alias", + "alias_a", + "--value", + AA_VIEWING_KEY, + "--unsafe-dont-encrypt", + ], + )?; + node.assert_success(); + run( + &node, + Bin::Wallet, + vec![ + "add", + "--alias", + "alias_b", + "--value", + AB_VIEWING_KEY, + "--unsafe-dont-encrypt", + ], + )?; + node.assert_success(); + // 1. Shield some tokens run( &node, @@ -1286,15 +1499,15 @@ fn wrapper_fee_unshielding() -> Result<()> { vec![ "transfer", "--source", - ALBERT, + ALBERT_KEY, "--target", AA_PAYMENT_ADDRESS, "--token", NAM, "--amount", - "500000", + "1979999", // Reduce the balance of the fee payer artificially "--gas-price", - "30", // Reduce the balance of the fee payer artificially + "1", "--gas-limit", "20000", "--ledger-address", @@ -1302,6 +1515,53 @@ fn wrapper_fee_unshielding() -> Result<()> { ], )?; node.assert_success(); + // sync shielded context + run( + &node, + Bin::Client, + vec![ + "shielded-sync", + "--viewing-keys", + AA_VIEWING_KEY, + "--node", + validator_one_rpc, + ], + )?; + node.assert_success(); + let captured = CapturedOutput::of(|| { + run( + &node, + Bin::Client, + vec![ + "balance", + "--owner", + ALBERT_KEY, + "--token", + NAM, + "--node", + validator_one_rpc, + ], + ) + }); + assert!(captured.result.is_ok()); + assert!(captured.contains("nam: 1")); + let captured = CapturedOutput::of(|| { + run( + &node, + Bin::Client, + vec![ + "balance", + "--owner", + AA_VIEWING_KEY, + "--token", + NAM, + "--node", + validator_one_rpc, + ], + ) + }); + assert!(captured.result.is_ok()); + assert!(captured.contains("nam: 1979999")); _ = node.next_epoch(); // 2. Valid unshielding @@ -1311,13 +1571,15 @@ fn wrapper_fee_unshielding() -> Result<()> { vec![ "transfer", "--source", - ALBERT, + ALBERT_KEY, "--target", BERTHA, "--token", NAM, "--amount", "1", + "--gas-price", + "1", "--gas-limit", "20000", "--gas-spending-key", @@ -1327,78 +1589,487 @@ fn wrapper_fee_unshielding() -> Result<()> { ], )?; node.assert_success(); - - // 3. Invalid unshielding - let tx_args = vec![ - "transfer", - "--source", - ALBERT, - "--target", - BERTHA, - "--token", - NAM, - "--amount", - "1", - "--gas-price", - "1000", - "--gas-spending-key", - B_SPENDING_KEY, - "--ledger-address", - validator_one_rpc, - // NOTE: Forcing the transaction will make the client produce a - // transfer without a masp object attached to it, so don't expect a - // failure from the masp vp here but from the check_fees function - "--force", - ]; - - let captured = - CapturedOutput::of(|| run(&node, Bin::Client, tx_args.clone())); - assert!( - captured.result.is_err(), - "{:?} unexpectedly succeeded", - tx_args - ); - - Ok(()) -} - -// Test that a masp unshield transaction can be succesfully executed even across -// an epoch boundary. -#[test] -fn cross_epoch_tx() -> Result<()> { - // This address doesn't matter for tests. But an argument is required. - let validator_one_rpc = "127.0.0.1:26567"; - // Download the shielded pool parameters before starting node - let _ = FsShieldedUtils::new(PathBuf::new()); - let (mut node, _services) = setup::setup()?; - _ = node.next_epoch(); - - // 1. Shield some tokens + // sync shielded context run( &node, Bin::Client, - vec![ - "transfer", - "--source", - ALBERT, - "--target", - AA_PAYMENT_ADDRESS, - "--token", - NAM, - "--amount", - "1000", - "--ledger-address", - validator_one_rpc, - ], + vec!["shielded-sync", "--node", validator_one_rpc], )?; node.assert_success(); - - // 2. Generate the tx in the current epoch - let tempdir = tempfile::tempdir().unwrap(); - run( - &node, - Bin::Client, - vec![ + let captured = CapturedOutput::of(|| { + run( + &node, + Bin::Client, + vec![ + "balance", + "--owner", + ALBERT_KEY, + "--token", + NAM, + "--node", + validator_one_rpc, + ], + ) + }); + assert!(captured.result.is_ok()); + assert!(captured.contains("nam: 0")); + let captured = CapturedOutput::of(|| { + run( + &node, + Bin::Client, + vec![ + "balance", + "--owner", + AA_VIEWING_KEY, + "--token", + NAM, + "--node", + validator_one_rpc, + ], + ) + }); + assert!(captured.result.is_ok()); + assert!(captured.contains("nam: 1959999")); + + // 3. Try another valid fee unshielding and masp transaction in the same tx, + // with the same source. This tests that the client can properly fetch data + // and construct these kind of transactions + run( + &node, + Bin::Client, + vec![ + "transfer", + "--source", + A_SPENDING_KEY, + "--target", + AB_PAYMENT_ADDRESS, + "--token", + NAM, + "--amount", + "1", + "--gas-price", + "1", + "--gas-limit", + "20000", + "--gas-payer", + ALBERT_KEY, + "--gas-spending-key", + A_SPENDING_KEY, + "--ledger-address", + validator_one_rpc, + ], + )?; + node.assert_success(); + let captured = CapturedOutput::of(|| { + run( + &node, + Bin::Client, + vec![ + "balance", + "--owner", + AA_VIEWING_KEY, + "--token", + NAM, + "--node", + validator_one_rpc, + ], + ) + }); + assert!(captured.result.is_ok()); + assert!(captured.contains("nam: 1939998")); + + // 4. Invalid unshielding + let tx_args = vec![ + "transfer", + "--source", + ALBERT_KEY, + "--target", + BERTHA, + "--token", + NAM, + "--amount", + "1", + "--gas-price", + "1000", + "--gas-spending-key", + B_SPENDING_KEY, + "--ledger-address", + validator_one_rpc, + // NOTE: Forcing the transaction will make the client produce a + // transfer without a masp object attached to it, so don't expect a + // failure from the masp vp here but from the check_fees function + "--force", + ]; + + let captured = + CapturedOutput::of(|| run(&node, Bin::Client, tx_args.clone())); + assert!( + captured.result.is_err(), + "{:?} unexpectedly succeeded", + tx_args + ); + + Ok(()) +} + +/// Tests that multiple chained transactions can be constructed from the +/// shielded context and executed in the same block +#[test] +fn chained_txs_same_block() -> Result<()> { + // This address doesn't matter for tests. But an argument is required. + let validator_one_rpc = "127.0.0.1:26567"; + // Download the shielded pool parameters before starting node + let _ = FsShieldedUtils::new(PathBuf::new()); + let (mut node, _services) = setup::setup()?; + _ = node.next_epoch(); + + // Add the relevant viewing keys to the wallet otherwise the shielded + // context won't precache the masp data + run( + &node, + Bin::Wallet, + vec![ + "add", + "--alias", + "alias_a", + "--value", + AA_VIEWING_KEY, + "--unsafe-dont-encrypt", + ], + )?; + node.assert_success(); + run( + &node, + Bin::Wallet, + vec![ + "add", + "--alias", + "alias_b", + "--value", + AB_VIEWING_KEY, + "--unsafe-dont-encrypt", + ], + )?; + node.assert_success(); + + // 1. Shield tokens + _ = node.next_epoch(); + run( + &node, + Bin::Client, + vec![ + "transfer", + "--source", + ALBERT_KEY, + "--target", + AA_PAYMENT_ADDRESS, + "--token", + NAM, + "--amount", + "100", + "--ledger-address", + validator_one_rpc, + ], + )?; + node.assert_success(); + + // 2. Shielded consecutive operations without fetching. The second and third + // operations are constructed by the speculative context based on the notes + // produces by the previous one. Dump the txs to than reload and submit in + // the same block + let tempdir = tempfile::tempdir().unwrap(); + let mut txs_bytes = vec![]; + + _ = node.next_epoch(); + run( + &node, + Bin::Client, + vec![ + "transfer", + "--source", + A_SPENDING_KEY, + "--target", + AB_PAYMENT_ADDRESS, + "--token", + NAM, + "--amount", + "50", + "--gas-payer", + ALBERT_KEY, + "--output-folder-path", + tempdir.path().to_str().unwrap(), + "--dump-tx", + "--ledger-address", + validator_one_rpc, + ], + )?; + node.assert_success(); + let file_path = tempdir + .path() + .read_dir() + .unwrap() + .next() + .unwrap() + .unwrap() + .path(); + txs_bytes.push(std::fs::read(&file_path).unwrap()); + std::fs::remove_file(&file_path).unwrap(); + + run( + &node, + Bin::Client, + vec![ + "transfer", + "--source", + A_SPENDING_KEY, + "--target", + AC_PAYMENT_ADDRESS, + "--token", + NAM, + "--amount", + "50", + "--gas-payer", + CHRISTEL_KEY, + "--output-folder-path", + tempdir.path().to_str().unwrap(), + "--dump-tx", + "--ledger-address", + validator_one_rpc, + ], + )?; + node.assert_success(); + let file_path = tempdir + .path() + .read_dir() + .unwrap() + .next() + .unwrap() + .unwrap() + .path(); + txs_bytes.push(std::fs::read(&file_path).unwrap()); + std::fs::remove_file(&file_path).unwrap(); + + run( + &node, + Bin::Client, + vec![ + "transfer", + "--source", + B_SPENDING_KEY, + "--target", + AC_PAYMENT_ADDRESS, + "--token", + NAM, + "--amount", + "50", + "--gas-payer", + CHRISTEL_KEY, + "--output-folder-path", + tempdir.path().to_str().unwrap(), + "--dump-tx", + "--ledger-address", + validator_one_rpc, + ], + )?; + node.assert_success(); + let file_path = tempdir + .path() + .read_dir() + .unwrap() + .next() + .unwrap() + .unwrap() + .path(); + txs_bytes.push(std::fs::read(&file_path).unwrap()); + std::fs::remove_file(&file_path).unwrap(); + + let sk = christel_keypair(); + let pk = sk.to_public(); + + let native_token = node + .shell + .lock() + .unwrap() + .wl_storage + .storage + .native_token + .clone(); + let mut txs = vec![]; + for bytes in txs_bytes { + let mut tx = namada::tx::Tx::deserialize(&bytes).unwrap(); + tx.add_wrapper( + namada::tx::data::wrapper_tx::Fee { + amount_per_gas_unit: DenominatedAmount::native(1.into()), + token: native_token.clone(), + }, + pk.clone(), + Default::default(), + 20000.into(), + None, + ); + tx.sign_wrapper(sk.clone()); + + txs.push(tx.to_bytes()); + } + + node.clear_results(); + node.submit_txs(txs); + { + let results = node.results.lock().unwrap(); + // If empty than failed in process proposal + assert!(!results.is_empty()); + + for result in results.iter() { + assert!(matches!(result, NodeResults::Ok)); + } + } + // Finalize the next block to actually execute the decrypted txs + node.clear_results(); + node.finalize_and_commit(); + { + let results = node.results.lock().unwrap(); + for result in results.iter() { + assert!(matches!(result, NodeResults::Ok)); + } + } + + Ok(()) +} + +// Test that the shielded context can spend some notes produced in a previous tx +// without fetching and in a different epoch (the second requirement to test +// that conversions are correctly queried) +#[test] +fn unfetched_cross_epoch_shielded() -> Result<()> { + // This address doesn't matter for tests. But an argument is required. + let validator_one_rpc = "127.0.0.1:26567"; + // Download the shielded pool parameters before starting node + let _ = FsShieldedUtils::new(PathBuf::new()); + let (mut node, _services) = setup::setup()?; + _ = node.next_epoch(); + + // Add the relevant viewing key to the wallet otherwise the shielded context + // won't precache the masp data + run( + &node, + Bin::Wallet, + vec![ + "add", + "--alias", + "alias", + "--value", + AA_VIEWING_KEY, + "--unsafe-dont-encrypt", + ], + )?; + node.assert_success(); + + // 1. Shield tokens + _ = node.next_epoch(); + run( + &node, + Bin::Client, + vec![ + "transfer", + "--source", + ALBERT_KEY, + "--target", + AA_PAYMENT_ADDRESS, + "--token", + NAM, + "--amount", + "1980000", // Reduce the balance of the fee payer artificially to 0 + "--gas-price", + "1", + "--gas-limit", + "20000", + "--ledger-address", + validator_one_rpc, + ], + )?; + node.assert_success(); + + _ = node.next_epoch(); + // 2. Shielded operation in the next epoch without fetching + run( + &node, + Bin::Client, + vec![ + "transfer", + "--source", + A_SPENDING_KEY, + "--target", + AB_PAYMENT_ADDRESS, + "--token", + NAM, + "--amount", + "1", + "--gas-payer", + ALBERT_KEY, + // fee unshielding with the same source key to add more stress to + // the test + "--gas-spending-key", + A_SPENDING_KEY, + "--ledger-address", + validator_one_rpc, + ], + )?; + node.assert_success(); + + Ok(()) +} + +// Test that a masp unshield transaction can be succesfully executed even across +// an epoch boundary. +#[test] +fn cross_epoch_unshield() -> Result<()> { + // This address doesn't matter for tests. But an argument is required. + let validator_one_rpc = "127.0.0.1:26567"; + // Download the shielded pool parameters before starting node + let _ = FsShieldedUtils::new(PathBuf::new()); + let (mut node, _services) = setup::setup()?; + _ = node.next_epoch(); + + // 1. Shield some tokens + run( + &node, + Bin::Client, + vec![ + "transfer", + "--source", + ALBERT, + "--target", + AA_PAYMENT_ADDRESS, + "--token", + NAM, + "--amount", + "1000", + "--ledger-address", + validator_one_rpc, + ], + )?; + node.assert_success(); + + // sync the shielded context + run( + &node, + Bin::Client, + vec![ + "shielded-sync", + "--viewing-keys", + AA_VIEWING_KEY, + "--node", + validator_one_rpc, + ], + )?; + node.assert_success(); + + // 2. Generate the tx in the current epoch + let tempdir = tempfile::tempdir().unwrap(); + run( + &node, + Bin::Client, + vec![ "transfer", "--source", A_SPENDING_KEY, @@ -1471,6 +2142,19 @@ fn dynamic_assets() -> Result<()> { storage.conversion_state.tokens.retain(|k, _v| *k == nam); tokens }; + // add necessary viewing keys to shielded context + run( + &node, + Bin::Client, + vec![ + "shielded-sync", + "--viewing-keys", + AA_VIEWING_KEY, + "--node", + validator_one_rpc, + ], + )?; + node.assert_success(); // Wait till epoch boundary node.next_epoch(); // Send 1 BTC from Albert to PA @@ -1492,6 +2176,13 @@ fn dynamic_assets() -> Result<()> { ], )?; node.assert_success(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); // Assert BTC balance at VK(A) is 1 let captured = CapturedOutput::of(|| { @@ -1543,6 +2234,13 @@ fn dynamic_assets() -> Result<()> { // Wait till epoch boundary node.next_epoch(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); // Assert BTC balance at VK(A) is still 1 let captured = CapturedOutput::of(|| { @@ -1602,6 +2300,13 @@ fn dynamic_assets() -> Result<()> { ], )?; node.assert_success(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); // Assert BTC balance at VK(A) is now 2 let captured = CapturedOutput::of(|| { @@ -1643,6 +2348,13 @@ fn dynamic_assets() -> Result<()> { // Wait till epoch boundary node.next_epoch(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); // Assert that VK(A) has now received a NAM rewward for second deposit let captured = CapturedOutput::of(|| { @@ -1695,6 +2407,13 @@ fn dynamic_assets() -> Result<()> { // Wait till epoch boundary node.next_epoch(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); // Assert BTC balance at VK(A) is still 2 let captured = CapturedOutput::of(|| { @@ -1742,6 +2461,13 @@ fn dynamic_assets() -> Result<()> { // Wait till epoch boundary node.next_epoch(); + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); // Assert BTC balance at VK(A) is still 2 let captured = CapturedOutput::of(|| { @@ -1783,7 +2509,13 @@ fn dynamic_assets() -> Result<()> { // Wait till epoch boundary node.next_epoch(); - + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); // Assert BTC balance at VK(A) is still 2 let captured = CapturedOutput::of(|| { run( @@ -1835,7 +2567,13 @@ fn dynamic_assets() -> Result<()> { // Wait till epoch boundary node.next_epoch(); - + // sync the shielded context + run( + &node, + Bin::Client, + vec!["shielded-sync", "--node", validator_one_rpc], + )?; + node.assert_success(); // Assert BTC balance at VK(A) is still 2 let captured = CapturedOutput::of(|| { run( diff --git a/test_fixtures/masp_proofs/0E64D319134C2890884AB8CC8856EE712BC279DFB681673A7513CE688FD54D62.bin b/test_fixtures/masp_proofs/0E64D319134C2890884AB8CC8856EE712BC279DFB681673A7513CE688FD54D62.bin new file mode 100644 index 0000000000..c97e831aee Binary files /dev/null and b/test_fixtures/masp_proofs/0E64D319134C2890884AB8CC8856EE712BC279DFB681673A7513CE688FD54D62.bin differ diff --git a/test_fixtures/masp_proofs/0F1E22C136B5E0A08948FF20DDEF1861C5C360B54F42D041F181BC79077AF7D5.bin b/test_fixtures/masp_proofs/0F1E22C136B5E0A08948FF20DDEF1861C5C360B54F42D041F181BC79077AF7D5.bin new file mode 100644 index 0000000000..eed08a5aa3 Binary files /dev/null and b/test_fixtures/masp_proofs/0F1E22C136B5E0A08948FF20DDEF1861C5C360B54F42D041F181BC79077AF7D5.bin differ diff --git a/test_fixtures/masp_proofs/11BBC6FA2A8493A11DD5E488B2582DAD9FBD3ACA2E0F8314642810FC958594E7.bin b/test_fixtures/masp_proofs/11BBC6FA2A8493A11DD5E488B2582DAD9FBD3ACA2E0F8314642810FC958594E7.bin index d8d0752e4d..33dc097696 100644 Binary files a/test_fixtures/masp_proofs/11BBC6FA2A8493A11DD5E488B2582DAD9FBD3ACA2E0F8314642810FC958594E7.bin and b/test_fixtures/masp_proofs/11BBC6FA2A8493A11DD5E488B2582DAD9FBD3ACA2E0F8314642810FC958594E7.bin differ diff --git a/test_fixtures/masp_proofs/14AA8F00D776FA8BFE458925D5A016B823DA3B4C354320DE314EAF403C85E300.bin b/test_fixtures/masp_proofs/14AA8F00D776FA8BFE458925D5A016B823DA3B4C354320DE314EAF403C85E300.bin new file mode 100644 index 0000000000..8a4d1335d6 Binary files /dev/null and b/test_fixtures/masp_proofs/14AA8F00D776FA8BFE458925D5A016B823DA3B4C354320DE314EAF403C85E300.bin differ diff --git a/test_fixtures/masp_proofs/2B6C3CEF478B4971AF997B08C8D4D70154B89179E7DA6F14C700AAE1EC531E80.bin b/test_fixtures/masp_proofs/2B6C3CEF478B4971AF997B08C8D4D70154B89179E7DA6F14C700AAE1EC531E80.bin index 896302b0a0..5179ac037f 100644 Binary files a/test_fixtures/masp_proofs/2B6C3CEF478B4971AF997B08C8D4D70154B89179E7DA6F14C700AAE1EC531E80.bin and b/test_fixtures/masp_proofs/2B6C3CEF478B4971AF997B08C8D4D70154B89179E7DA6F14C700AAE1EC531E80.bin differ diff --git a/test_fixtures/masp_proofs/3403F505B8B74E42E29C9EA054A0D10D24963CD60561FA8ADF3118E192AED9F8.bin b/test_fixtures/masp_proofs/3403F505B8B74E42E29C9EA054A0D10D24963CD60561FA8ADF3118E192AED9F8.bin new file mode 100644 index 0000000000..b9b6fc109e Binary files /dev/null and b/test_fixtures/masp_proofs/3403F505B8B74E42E29C9EA054A0D10D24963CD60561FA8ADF3118E192AED9F8.bin differ diff --git a/test_fixtures/masp_proofs/3E8EFC1EFCFDF6F01065414ADAB30049BA56A2A99E9EA9B09BE15B931CB7D476.bin b/test_fixtures/masp_proofs/3E8EFC1EFCFDF6F01065414ADAB30049BA56A2A99E9EA9B09BE15B931CB7D476.bin new file mode 100644 index 0000000000..88b9bb8301 Binary files /dev/null and b/test_fixtures/masp_proofs/3E8EFC1EFCFDF6F01065414ADAB30049BA56A2A99E9EA9B09BE15B931CB7D476.bin differ diff --git a/test_fixtures/masp_proofs/3E93E8F4FC3498BA19EF4D4D70FD0A13DAD049EAA1ECA95234C8AB02601FC0F0.bin b/test_fixtures/masp_proofs/3E93E8F4FC3498BA19EF4D4D70FD0A13DAD049EAA1ECA95234C8AB02601FC0F0.bin index 5747b31c01..8425882840 100644 Binary files a/test_fixtures/masp_proofs/3E93E8F4FC3498BA19EF4D4D70FD0A13DAD049EAA1ECA95234C8AB02601FC0F0.bin and b/test_fixtures/masp_proofs/3E93E8F4FC3498BA19EF4D4D70FD0A13DAD049EAA1ECA95234C8AB02601FC0F0.bin differ diff --git a/test_fixtures/masp_proofs/3FDD781F73CEEE5426EF9EF03811DFBCF2AAA7876CE5FACD48AD18CB552E7349.bin b/test_fixtures/masp_proofs/3FDD781F73CEEE5426EF9EF03811DFBCF2AAA7876CE5FACD48AD18CB552E7349.bin index 7ab7add9ba..82fe852cf6 100644 Binary files a/test_fixtures/masp_proofs/3FDD781F73CEEE5426EF9EF03811DFBCF2AAA7876CE5FACD48AD18CB552E7349.bin and b/test_fixtures/masp_proofs/3FDD781F73CEEE5426EF9EF03811DFBCF2AAA7876CE5FACD48AD18CB552E7349.bin differ diff --git a/test_fixtures/masp_proofs/5355FAACD2BC8B1D4E226738EADB8CE4F984EEA5DE6991947DC46912C5EEEC72.bin b/test_fixtures/masp_proofs/5355FAACD2BC8B1D4E226738EADB8CE4F984EEA5DE6991947DC46912C5EEEC72.bin index c3af4cfd0c..01a199b680 100644 Binary files a/test_fixtures/masp_proofs/5355FAACD2BC8B1D4E226738EADB8CE4F984EEA5DE6991947DC46912C5EEEC72.bin and b/test_fixtures/masp_proofs/5355FAACD2BC8B1D4E226738EADB8CE4F984EEA5DE6991947DC46912C5EEEC72.bin differ diff --git a/test_fixtures/masp_proofs/59CC89A38C2D211F8765F502B6753FB3BE606A5729D43A6EA8F79007D34F6C60.bin b/test_fixtures/masp_proofs/59CC89A38C2D211F8765F502B6753FB3BE606A5729D43A6EA8F79007D34F6C60.bin index 0e4ff13fd5..31fc8824ae 100644 Binary files a/test_fixtures/masp_proofs/59CC89A38C2D211F8765F502B6753FB3BE606A5729D43A6EA8F79007D34F6C60.bin and b/test_fixtures/masp_proofs/59CC89A38C2D211F8765F502B6753FB3BE606A5729D43A6EA8F79007D34F6C60.bin differ diff --git a/test_fixtures/masp_proofs/6A12E11EEAA83A0D00BF7470DD0F3AD4FA4AB61D0B965BB9ED02971491A2B7FC.bin b/test_fixtures/masp_proofs/6A12E11EEAA83A0D00BF7470DD0F3AD4FA4AB61D0B965BB9ED02971491A2B7FC.bin index 492c021d12..d5c9cc3829 100644 Binary files a/test_fixtures/masp_proofs/6A12E11EEAA83A0D00BF7470DD0F3AD4FA4AB61D0B965BB9ED02971491A2B7FC.bin and b/test_fixtures/masp_proofs/6A12E11EEAA83A0D00BF7470DD0F3AD4FA4AB61D0B965BB9ED02971491A2B7FC.bin differ diff --git a/test_fixtures/masp_proofs/7A240D86DB78C8E1478FB0857D16BE737B7C747CD18D3B832DEC3C9DDCF3DBF8.bin b/test_fixtures/masp_proofs/7A240D86DB78C8E1478FB0857D16BE737B7C747CD18D3B832DEC3C9DDCF3DBF8.bin new file mode 100644 index 0000000000..dc1aee3d37 Binary files /dev/null and b/test_fixtures/masp_proofs/7A240D86DB78C8E1478FB0857D16BE737B7C747CD18D3B832DEC3C9DDCF3DBF8.bin differ diff --git a/test_fixtures/masp_proofs/7CF9F01C0531594091F642C6EB72B0D2BB1E2337D71D21CD8DB40EBC58CFAD73.bin b/test_fixtures/masp_proofs/7CF9F01C0531594091F642C6EB72B0D2BB1E2337D71D21CD8DB40EBC58CFAD73.bin new file mode 100644 index 0000000000..82594f87c9 Binary files /dev/null and b/test_fixtures/masp_proofs/7CF9F01C0531594091F642C6EB72B0D2BB1E2337D71D21CD8DB40EBC58CFAD73.bin differ diff --git a/test_fixtures/masp_proofs/849D9E73D858B17BBB5856FAC2DBE3DA9201B7C90E74A7B6C2B9B7320F006872.bin b/test_fixtures/masp_proofs/849D9E73D858B17BBB5856FAC2DBE3DA9201B7C90E74A7B6C2B9B7320F006872.bin new file mode 100644 index 0000000000..678a20c50b Binary files /dev/null and b/test_fixtures/masp_proofs/849D9E73D858B17BBB5856FAC2DBE3DA9201B7C90E74A7B6C2B9B7320F006872.bin differ diff --git a/test_fixtures/masp_proofs/AF6B3A470013460A05E8A522EC0DF6893C1C09CF3101C402BFE1465005639F14.bin b/test_fixtures/masp_proofs/AF6B3A470013460A05E8A522EC0DF6893C1C09CF3101C402BFE1465005639F14.bin index 9526b74cec..65a58651bf 100644 Binary files a/test_fixtures/masp_proofs/AF6B3A470013460A05E8A522EC0DF6893C1C09CF3101C402BFE1465005639F14.bin and b/test_fixtures/masp_proofs/AF6B3A470013460A05E8A522EC0DF6893C1C09CF3101C402BFE1465005639F14.bin differ diff --git a/test_fixtures/masp_proofs/B0876E15DE7B2C742912E048F583E61BED82F25C4452CA41B769C3BABEB62D2B.bin b/test_fixtures/masp_proofs/B0876E15DE7B2C742912E048F583E61BED82F25C4452CA41B769C3BABEB62D2B.bin index dd3f26ba0b..7e0a015943 100644 Binary files a/test_fixtures/masp_proofs/B0876E15DE7B2C742912E048F583E61BED82F25C4452CA41B769C3BABEB62D2B.bin and b/test_fixtures/masp_proofs/B0876E15DE7B2C742912E048F583E61BED82F25C4452CA41B769C3BABEB62D2B.bin differ diff --git a/test_fixtures/masp_proofs/55CC69CC552A02161C1C9EED1AF2741B2A24802E41AF7F56081564BF2A160464.bin b/test_fixtures/masp_proofs/B13164F18807523FAB9840F34106FB77DB730FFFE562F2565B0431F7A901CC9A.bin similarity index 62% rename from test_fixtures/masp_proofs/55CC69CC552A02161C1C9EED1AF2741B2A24802E41AF7F56081564BF2A160464.bin rename to test_fixtures/masp_proofs/B13164F18807523FAB9840F34106FB77DB730FFFE562F2565B0431F7A901CC9A.bin index 180c2c668f..10f12f61d1 100644 Binary files a/test_fixtures/masp_proofs/55CC69CC552A02161C1C9EED1AF2741B2A24802E41AF7F56081564BF2A160464.bin and b/test_fixtures/masp_proofs/B13164F18807523FAB9840F34106FB77DB730FFFE562F2565B0431F7A901CC9A.bin differ diff --git a/test_fixtures/masp_proofs/B6BF8444CE901DD0D10F1C8BA46B143F55D67FD17124ECAE16FAD6286D804362.bin b/test_fixtures/masp_proofs/B6BF8444CE901DD0D10F1C8BA46B143F55D67FD17124ECAE16FAD6286D804362.bin new file mode 100644 index 0000000000..da509d1cad Binary files /dev/null and b/test_fixtures/masp_proofs/B6BF8444CE901DD0D10F1C8BA46B143F55D67FD17124ECAE16FAD6286D804362.bin differ diff --git a/test_fixtures/masp_proofs/BE09EB8FF98C0CAF7BA8278C4ADC5356710DE7B6F6FA2B23C19412005669DC2B.bin b/test_fixtures/masp_proofs/BE09EB8FF98C0CAF7BA8278C4ADC5356710DE7B6F6FA2B23C19412005669DC2B.bin index a356306775..f42e03bb6f 100644 Binary files a/test_fixtures/masp_proofs/BE09EB8FF98C0CAF7BA8278C4ADC5356710DE7B6F6FA2B23C19412005669DC2B.bin and b/test_fixtures/masp_proofs/BE09EB8FF98C0CAF7BA8278C4ADC5356710DE7B6F6FA2B23C19412005669DC2B.bin differ diff --git a/test_fixtures/masp_proofs/BEDD60F57C8F73C0266B1DFF869384EF0421F46F7732EE62196DEB73ECB4C225.bin b/test_fixtures/masp_proofs/BEDD60F57C8F73C0266B1DFF869384EF0421F46F7732EE62196DEB73ECB4C225.bin index 5d5f606f6a..c3b3c0a571 100644 Binary files a/test_fixtures/masp_proofs/BEDD60F57C8F73C0266B1DFF869384EF0421F46F7732EE62196DEB73ECB4C225.bin and b/test_fixtures/masp_proofs/BEDD60F57C8F73C0266B1DFF869384EF0421F46F7732EE62196DEB73ECB4C225.bin differ diff --git a/test_fixtures/masp_proofs/C5EF7E4D76C16E75B400531D982A4E4AA763188BD9D5DDAC6E217228B95AD57F.bin b/test_fixtures/masp_proofs/C5EF7E4D76C16E75B400531D982A4E4AA763188BD9D5DDAC6E217228B95AD57F.bin new file mode 100644 index 0000000000..3b881fe6b6 Binary files /dev/null and b/test_fixtures/masp_proofs/C5EF7E4D76C16E75B400531D982A4E4AA763188BD9D5DDAC6E217228B95AD57F.bin differ diff --git a/test_fixtures/masp_proofs/C614F20521BADE7CE91F2399B81E2DCE33145F68A217FD50B065966646F59116.bin b/test_fixtures/masp_proofs/C614F20521BADE7CE91F2399B81E2DCE33145F68A217FD50B065966646F59116.bin index f30214b8c8..526753fb9b 100644 Binary files a/test_fixtures/masp_proofs/C614F20521BADE7CE91F2399B81E2DCE33145F68A217FD50B065966646F59116.bin and b/test_fixtures/masp_proofs/C614F20521BADE7CE91F2399B81E2DCE33145F68A217FD50B065966646F59116.bin differ diff --git a/test_fixtures/masp_proofs/CCDB111327E4189EC4C6FE2D429AFB3B6AE2AA3ADCACF75E6520A66B2F7D184C.bin b/test_fixtures/masp_proofs/CCDB111327E4189EC4C6FE2D429AFB3B6AE2AA3ADCACF75E6520A66B2F7D184C.bin index db2f1b978d..090496b8cc 100644 Binary files a/test_fixtures/masp_proofs/CCDB111327E4189EC4C6FE2D429AFB3B6AE2AA3ADCACF75E6520A66B2F7D184C.bin and b/test_fixtures/masp_proofs/CCDB111327E4189EC4C6FE2D429AFB3B6AE2AA3ADCACF75E6520A66B2F7D184C.bin differ diff --git a/test_fixtures/masp_proofs/D1DF6FEDD8C02C1506E1DCA61CA81A7DF315190329C7C2F708876D51F60D2D60.bin b/test_fixtures/masp_proofs/D1DF6FEDD8C02C1506E1DCA61CA81A7DF315190329C7C2F708876D51F60D2D60.bin index 8357cbe5ed..64f5f54787 100644 Binary files a/test_fixtures/masp_proofs/D1DF6FEDD8C02C1506E1DCA61CA81A7DF315190329C7C2F708876D51F60D2D60.bin and b/test_fixtures/masp_proofs/D1DF6FEDD8C02C1506E1DCA61CA81A7DF315190329C7C2F708876D51F60D2D60.bin differ diff --git a/test_fixtures/masp_proofs/D39FEE41A59DA80C4DBCAFC36E7B62D6F3F03E1332D2B25890911CE8375D1280.bin b/test_fixtures/masp_proofs/D39FEE41A59DA80C4DBCAFC36E7B62D6F3F03E1332D2B25890911CE8375D1280.bin new file mode 100644 index 0000000000..225fcb8ab7 Binary files /dev/null and b/test_fixtures/masp_proofs/D39FEE41A59DA80C4DBCAFC36E7B62D6F3F03E1332D2B25890911CE8375D1280.bin differ diff --git a/test_fixtures/masp_proofs/E1D883F9C9E7FD5C51D39AE1809BC3BBACE79DF98801EB8154EA1904597854A1.bin b/test_fixtures/masp_proofs/E1D883F9C9E7FD5C51D39AE1809BC3BBACE79DF98801EB8154EA1904597854A1.bin index a25fa32cf0..88df400e5e 100644 Binary files a/test_fixtures/masp_proofs/E1D883F9C9E7FD5C51D39AE1809BC3BBACE79DF98801EB8154EA1904597854A1.bin and b/test_fixtures/masp_proofs/E1D883F9C9E7FD5C51D39AE1809BC3BBACE79DF98801EB8154EA1904597854A1.bin differ diff --git a/test_fixtures/masp_proofs/E750A2B670666F8788E71B3F685E298FEBBEA9975D764A6A9E2F98E10810859D.bin b/test_fixtures/masp_proofs/E750A2B670666F8788E71B3F685E298FEBBEA9975D764A6A9E2F98E10810859D.bin new file mode 100644 index 0000000000..f3e3c17005 Binary files /dev/null and b/test_fixtures/masp_proofs/E750A2B670666F8788E71B3F685E298FEBBEA9975D764A6A9E2F98E10810859D.bin differ diff --git a/test_fixtures/masp_proofs/EA7A88B1429EFFF0AE7167F6FBEF52D368B771AD525817E722EA3E12B0A96971.bin b/test_fixtures/masp_proofs/EA7A88B1429EFFF0AE7167F6FBEF52D368B771AD525817E722EA3E12B0A96971.bin index b1fcfa75c2..4ff441e5c6 100644 Binary files a/test_fixtures/masp_proofs/EA7A88B1429EFFF0AE7167F6FBEF52D368B771AD525817E722EA3E12B0A96971.bin and b/test_fixtures/masp_proofs/EA7A88B1429EFFF0AE7167F6FBEF52D368B771AD525817E722EA3E12B0A96971.bin differ diff --git a/test_fixtures/masp_proofs/EF7D15FBFB9CC557CC8F9D4D18F7858007C4DA521F8E3BF250A46E9342178EEB.bin b/test_fixtures/masp_proofs/EF7D15FBFB9CC557CC8F9D4D18F7858007C4DA521F8E3BF250A46E9342178EEB.bin index e23e43bbb9..477c4a73d3 100644 Binary files a/test_fixtures/masp_proofs/EF7D15FBFB9CC557CC8F9D4D18F7858007C4DA521F8E3BF250A46E9342178EEB.bin and b/test_fixtures/masp_proofs/EF7D15FBFB9CC557CC8F9D4D18F7858007C4DA521F8E3BF250A46E9342178EEB.bin differ diff --git a/test_fixtures/masp_proofs/F4F0D829373DDA78336CF39EBA3356453E412F862D949ABE9671C24A3903909E.bin b/test_fixtures/masp_proofs/F4F0D829373DDA78336CF39EBA3356453E412F862D949ABE9671C24A3903909E.bin index 2c8eec82a4..9b234f0f76 100644 Binary files a/test_fixtures/masp_proofs/F4F0D829373DDA78336CF39EBA3356453E412F862D949ABE9671C24A3903909E.bin and b/test_fixtures/masp_proofs/F4F0D829373DDA78336CF39EBA3356453E412F862D949ABE9671C24A3903909E.bin differ diff --git a/test_fixtures/masp_proofs/F7D91D1E43AA598631284A69E9861C7F98EA4325D94989648024831E350D8F98.bin b/test_fixtures/masp_proofs/F7D91D1E43AA598631284A69E9861C7F98EA4325D94989648024831E350D8F98.bin index c3214e3d34..ef6b46ba90 100644 Binary files a/test_fixtures/masp_proofs/F7D91D1E43AA598631284A69E9861C7F98EA4325D94989648024831E350D8F98.bin and b/test_fixtures/masp_proofs/F7D91D1E43AA598631284A69E9861C7F98EA4325D94989648024831E350D8F98.bin differ diff --git a/wasm/checksums.json b/wasm/checksums.json index 2aff2c2fde..bcf852dfc2 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,26 +1,26 @@ { - "tx_become_validator.wasm": "tx_become_validator.8df88a97bf4c610f136ddc7508863595447aa2bc5690695913a3b99ce7915db4.wasm", - "tx_bond.wasm": "tx_bond.b0f895d92ebb56296bcd2331de9f50525bdc018062faa80be003565096c98b0e.wasm", - "tx_bridge_pool.wasm": "tx_bridge_pool.d2e23c4efb08f95a969abbe66ace29ee147b446910f774a91ca411c4f7c33839.wasm", - "tx_change_consensus_key.wasm": "tx_change_consensus_key.f3a73802a8aa3b0bd3df227223e7d518ab2eeaba5ed22ddb22fff37eee0bac7b.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.a6b4fe747a500468a008298c173a6f6bb10823ea48b2e7e387601aafb97a411e.wasm", - "tx_change_validator_metadata.wasm": "tx_change_validator_metadata.a3b9483e570552b7188d9c474d6c853ba71793b965eb82df630c2240fd36d52c.wasm", - "tx_claim_rewards.wasm": "tx_claim_rewards.9a23fc15dcfbd64f7ae2c0a8877256ee7bac8e5ec85add66dac0bb04341336cd.wasm", - "tx_deactivate_validator.wasm": "tx_deactivate_validator.d8b6dcc070b87874c88381387545ee081a29adcce370d129dbfd5afb0acf6369.wasm", - "tx_ibc.wasm": "tx_ibc.3a54f5086273d9056981bf1437c102027bb2dd93e01049aad0bc1446914a0922.wasm", - "tx_init_account.wasm": "tx_init_account.66195469a7e80a7c019900241f127fe4b944dc43a5b22c817e8d9664d9f8bff7.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.f6406b2e30579d8d8752837702bd7bdb7dfc9adb92a1d3700f72d81f20f25f96.wasm", - "tx_reactivate_validator.wasm": "tx_reactivate_validator.893adb2b8f59c132ae60a1ba2b850c0fe06703d63bd537a2ba42f6a7d785a83a.wasm", - "tx_redelegate.wasm": "tx_redelegate.14975e2d1d631025b86af4c69d94c6b05e65f3c5dcaa78c258a8321b47fd2c28.wasm", - "tx_resign_steward.wasm": "tx_resign_steward.4223fd4bbddb65402ac3a856a2a26a3135505f6dce75e6cc9366b91dd0813a19.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.309baf2cf49bb57790f0da7dc0fa8bb463aff5f2deef91f72e95b7bac5a4223f.wasm", - "tx_transfer.wasm": "tx_transfer.eec37f37129766e8a9bd8908c4ee9f637723def0b53b337dd044e676e303b82a.wasm", - "tx_unbond.wasm": "tx_unbond.5b4cdd37cd3d676df308e91db9680046099e2c59b35fd8f60182c2f1b4352ed9.wasm", - "tx_unjail_validator.wasm": "tx_unjail_validator.3641d61e260eaf0570502f5ae196fdab9b658bf4ca17283517ed8f5a6b20b144.wasm", - "tx_update_account.wasm": "tx_update_account.67e3fe173b9ec85df136caac1099cae3ce7227628b6e2186fd95368ebc8c3581.wasm", - "tx_update_steward_commission.wasm": "tx_update_steward_commission.aab49609ce1ddf8be7e9c85ea0dc62586296a0d6ca083c35bbd8138583f5771c.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.3de7f3bd6063bd068c696c53698a40e1b9086c9dc45c05b02003c27ae26080f5.wasm", - "tx_withdraw.wasm": "tx_withdraw.66f99861369b419968bb1f8a3e04ea9421072ea4f6b173ed1e2f2b41c4940330.wasm", - "vp_implicit.wasm": "vp_implicit.fb99a9e1d8d8ca23e29c2668bf16dcb378fb847a0b089d033ab1eee2901cdb3a.wasm", - "vp_user.wasm": "vp_user.454337f9e0cd292f12297f248872413d4df4ce44b6ea4c9a60365c6ee7b5655e.wasm" + "tx_become_validator.wasm": "tx_become_validator.342d13f80c7db9462b9614b169303ea768f27f2ffa8d3f152eb9fa1e53fbfa05.wasm", + "tx_bond.wasm": "tx_bond.d231d3dd21613198245be912ccc72631730c0052eb58762b771d3014513a63a5.wasm", + "tx_bridge_pool.wasm": "tx_bridge_pool.174239af9711946856652bd641c92b50d64bc3d421efa9ff55e8c2fe5cfd15eb.wasm", + "tx_change_consensus_key.wasm": "tx_change_consensus_key.d4ea4959fb2ee837e22e5e7a40233211560a9fb9514cbc1908baff90371ad873.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.47409552a74537abbe95b150213fb0bf96ff336f2a614f7098b50cdc5be1c75b.wasm", + "tx_change_validator_metadata.wasm": "tx_change_validator_metadata.5b6d5f26a2e7e496d2400ea0b7822c160712978f2028e95f95a84a4cecb161f5.wasm", + "tx_claim_rewards.wasm": "tx_claim_rewards.4bc11106b3c1f1c5a83b6f434d5c561e441c1414cd0cb1f3456377e19b5ee41c.wasm", + "tx_deactivate_validator.wasm": "tx_deactivate_validator.551aa9604c4493af85fe4135bae091e80a111dff3cac78c134c6332fd8f99b33.wasm", + "tx_ibc.wasm": "tx_ibc.64c7f2f96d18dcd37d5e2f965a55db3150313870521e3f8153920146fe45dded.wasm", + "tx_init_account.wasm": "tx_init_account.183d63f2a1756d94d05c34c492b800cf64469f96fa89c5827ecf9ca76872e7a5.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.f593e476685555d759bf17c8a00b99eb23a395030c1d91860f8533351acac3fb.wasm", + "tx_reactivate_validator.wasm": "tx_reactivate_validator.199726342ab6115ad331fde68cce609868a7260c2e66e825d5834ad8336f93db.wasm", + "tx_redelegate.wasm": "tx_redelegate.ba0412eeecf16911d146eae01cdf3c73c496772a951d11c56f640381af15d68d.wasm", + "tx_resign_steward.wasm": "tx_resign_steward.52930818f481983c1dafff33d7d54fb262b1e817e3f53d631e57d1c148e9d9ca.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.573d68c7c0b42eba243c6902aae4002043e95bfc7d9fc2bda7334bf1b9a79ab3.wasm", + "tx_transfer.wasm": "tx_transfer.64672e859ca7a21eed446713857d439af9d3fea465075700611801a6a8d1d69d.wasm", + "tx_unbond.wasm": "tx_unbond.39ba9396dddfdbba866907b8c0c97b2fd0bd01f901dfd22426e53392bd5ee876.wasm", + "tx_unjail_validator.wasm": "tx_unjail_validator.10f93e3d9074e1d2fde280041a2e7b255cc27a974f074d958c532b57e009f1aa.wasm", + "tx_update_account.wasm": "tx_update_account.8a1f1b6a8e0d810e4a29be07331ee07cd8ade875025f0e680f2702aa23e30e9b.wasm", + "tx_update_steward_commission.wasm": "tx_update_steward_commission.dbe2f06f39f42748d45494318308abbc50b037b5d2dcd9663b14e23eb80c4c07.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.897e9cee7fd524e16be3bffc37f03cedc188f8a615ea98f6af4478ca309b3bb8.wasm", + "tx_withdraw.wasm": "tx_withdraw.0d8ca0609d9a69149a1414aa6b13d4ebc0e7f8aa1e0946c7e74089d1fc15bb95.wasm", + "vp_implicit.wasm": "vp_implicit.14c5208d7e6b17475b9535e046275b4f448f42d519340a02ff74e4c86c2995e7.wasm", + "vp_user.wasm": "vp_user.e36a0040e31cd98c920399589ea532f068fb56a6d63aa5a27395995c810edd67.wasm" } \ No newline at end of file