From 53af6ee9219196fea974283e01a4f49ed33fdaf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 31 Jan 2023 18:09:14 +0100 Subject: [PATCH 01/11] test/ledger/shell: test that `finalize_block` doesn't commit storage --- .../lib/node/ledger/shell/finalize_block.rs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 2f2ed4d1e91..71927ca2312 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -436,7 +436,12 @@ where /// are covered by the e2e tests. #[cfg(test)] mod test_finalize_block { + use std::collections::BTreeMap; + use std::str::FromStr; + + use namada::ledger::parameters::EpochDuration; use namada::types::storage::Epoch; + use namada::types::time::DurationSecs; use namada::types::transaction::{EncryptionKey, Fee, WrapperTx, MIN_FEE}; use super::*; @@ -789,4 +794,61 @@ mod test_finalize_block { } assert_eq!(counter, 2); } + + /// Test that the finalize block handler never commits changes directly to + /// the DB. + #[test] + fn test_finalize_doesnt_commit_db() { + let (mut shell, _) = setup(); + + // Update epoch duration to make sure we go through couple epochs + let epoch_duration = EpochDuration { + min_num_of_blocks: 5, + min_duration: DurationSecs(0), + }; + namada::ledger::parameters::update_epoch_parameter( + &mut shell.wl_storage.storage, + &epoch_duration, + ) + .unwrap(); + shell.wl_storage.storage.next_epoch_min_start_height = BlockHeight(5); + shell.wl_storage.storage.next_epoch_min_start_time = DateTimeUtc::now(); + shell.wl_storage.commit_genesis().unwrap(); + shell.commit(); + + // Collect all storage key-vals into a sorted map + let store_block_state = |shell: &TestShell| -> BTreeMap<_, _> { + let prefix: Key = FromStr::from_str("").unwrap(); + shell + .wl_storage + .storage + .db + .iter_prefix(&prefix) + .map(|(key, val, _gas)| (key, val)) + .collect() + }; + + // Store the full state in sorted map + let mut last_storage_state: std::collections::BTreeMap< + String, + Vec, + > = store_block_state(&shell); + + // Keep applying finalize block + for _ in 0..20 { + let req = FinalizeBlock::default(); + let _events = shell.finalize_block(req).unwrap(); + let new_state = store_block_state(&shell); + // The new state must be unchanged + itertools::assert_equal( + last_storage_state.iter(), + new_state.iter(), + ); + // Commit the block to move on to the next one + shell.wl_storage.commit_block().unwrap(); + + // Store the state after commit for the next iteration + last_storage_state = store_block_state(&shell); + } + } } From 517040d4947a9d8dcf5f09138a68e7613b96ab7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 31 Jan 2023 18:15:03 +0100 Subject: [PATCH 02/11] wl_storage: apply protocol write log changes at block level --- core/src/ledger/storage/wl_storage.rs | 16 ++--- core/src/ledger/storage/write_log.rs | 98 ++++++++++++++------------- 2 files changed, 59 insertions(+), 55 deletions(-) diff --git a/core/src/ledger/storage/wl_storage.rs b/core/src/ledger/storage/wl_storage.rs index a5c559936c3..8c89d3e6c41 100644 --- a/core/src/ledger/storage/wl_storage.rs +++ b/core/src/ledger/storage/wl_storage.rs @@ -36,8 +36,11 @@ where /// Commit the genesis state to DB. This should only be used before any /// blocks are produced. pub fn commit_genesis(&mut self) -> storage_api::Result<()> { + // Because the `impl StorageWrite for WlStorage` writes into block-level + // write log, we just commit the `block_write_log`, but without + // committing an actual block in storage self.write_log - .commit_genesis(&mut self.storage) + .commit_block(&mut self.storage) .into_storage_result() } @@ -310,15 +313,12 @@ where key: &storage::Key, val: impl AsRef<[u8]>, ) -> storage_api::Result<()> { - let _ = self - .write_log - .write(key, val.as_ref().to_vec()) - .into_storage_result(); - Ok(()) + self.write_log + .protocol_write(key, val.as_ref().to_vec()) + .into_storage_result() } fn delete(&mut self, key: &storage::Key) -> storage_api::Result<()> { - let _ = self.write_log.delete(key).into_storage_result(); - Ok(()) + self.write_log.protocol_delete(key).into_storage_result() } } diff --git a/core/src/ledger/storage/write_log.rs b/core/src/ledger/storage/write_log.rs index a754d876165..f51ef738d5a 100644 --- a/core/src/ledger/storage/write_log.rs +++ b/core/src/ledger/storage/write_log.rs @@ -192,6 +192,34 @@ impl WriteLog { Ok((gas as _, size_diff)) } + /// Write a key and a value. + /// Fails with [`Error::UpdateVpOfNewAccount`] when attempting to update a + /// validity predicate of a new account that's not yet committed to storage. + /// Fails with [`Error::UpdateTemporaryValue`] when attempting to update a + /// temporary value. + pub fn protocol_write( + &mut self, + key: &storage::Key, + value: Vec, + ) -> Result<()> { + if let Some(prev) = self + .block_write_log + .insert(key.clone(), StorageModification::Write { value }) + { + match prev { + StorageModification::InitAccount { .. } => { + return Err(Error::UpdateVpOfNewAccount); + } + StorageModification::Temp { .. } => { + return Err(Error::UpdateTemporaryValue); + } + StorageModification::Write { .. } + | StorageModification::Delete => {} + } + } + Ok(()) + } + /// Write a key and a value and return the gas cost and the size difference /// Fails with [`Error::UpdateVpOfNewAccount`] when attempting to update a /// validity predicate of a new account that's not yet committed to storage. @@ -257,6 +285,29 @@ impl WriteLog { Ok((gas as _, -size_diff)) } + /// Delete a key and its value. + /// Fails with [`Error::DeleteVp`] for a validity predicate key, which are + /// not possible to delete. + pub fn protocol_delete(&mut self, key: &storage::Key) -> Result<()> { + if key.is_validity_predicate().is_some() { + return Err(Error::DeleteVp); + } + if let Some(prev) = self + .block_write_log + .insert(key.clone(), StorageModification::Delete) + { + match prev { + StorageModification::InitAccount { .. } => { + return Err(Error::DeleteVp); + } + StorageModification::Write { .. } + | StorageModification::Delete + | StorageModification::Temp { .. } => {} + } + }; + Ok(()) + } + /// Initialize a new account and return the gas cost. pub fn init_account( &mut self, @@ -338,53 +389,6 @@ impl WriteLog { self.ibc_event.as_ref() } - /// Commit the current genesis tx's write log to the storage. - pub fn commit_genesis( - &mut self, - storage: &mut Storage, - ) -> Result<()> - where - DB: 'static - + ledger::storage::DB - + for<'iter> ledger::storage::DBIter<'iter>, - H: StorageHasher, - { - // This whole function is almost the same as `commit_block`, except that - // we commit the state directly from `tx_write_log` - let mut batch = Storage::::batch(); - for (key, entry) in self.tx_write_log.iter() { - match entry { - StorageModification::Write { value } => { - storage - .batch_write_subspace_val( - &mut batch, - key, - value.clone(), - ) - .map_err(Error::StorageError)?; - } - StorageModification::Delete => { - storage - .batch_delete_subspace_val(&mut batch, key) - .map_err(Error::StorageError)?; - } - StorageModification::InitAccount { vp } => { - storage - .batch_write_subspace_val(&mut batch, key, vp.clone()) - .map_err(Error::StorageError)?; - } - // temporary value isn't persisted - StorageModification::Temp { .. } => {} - } - } - storage.exec_batch(batch).map_err(Error::StorageError)?; - if let Some(address_gen) = self.address_gen.take() { - storage.address_gen = address_gen - } - self.tx_write_log.clear(); - Ok(()) - } - /// Commit the current transaction's write log to the block when it's /// accepted by all the triggered validity predicates. Starts a new /// transaction write log. From e344cdfff0ef2bc56ff40c7c541b2eadd007a3ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 31 Jan 2023 19:09:34 +0100 Subject: [PATCH 03/11] ledger: apply `update_allowed_conversions` storage changes via WlStorage --- .../lib/node/ledger/shell/finalize_block.rs | 7 + core/src/ledger/storage/masp_conversions.rs | 204 ++++++++++++++++++ core/src/ledger/storage/mod.rs | 194 +---------------- 3 files changed, 215 insertions(+), 190 deletions(-) create mode 100644 core/src/ledger/storage/masp_conversions.rs diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 71927ca2312..c55479dd0b5 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -50,6 +50,13 @@ where self.update_state(req.header, req.hash, req.byzantine_validators); if new_epoch { + if let Err(e) = namada::ledger::storage::update_allowed_conversions( + &mut self.wl_storage, + ) { + tracing::error!( + "Failed to update allowed conversions with {e}" + ); + } let _proposals_result = execute_governance_proposals(self, &mut response)?; } diff --git a/core/src/ledger/storage/masp_conversions.rs b/core/src/ledger/storage/masp_conversions.rs new file mode 100644 index 00000000000..0834d7bb53d --- /dev/null +++ b/core/src/ledger/storage/masp_conversions.rs @@ -0,0 +1,204 @@ +//! MASP rewards conversions + +use std::collections::BTreeMap; + +use borsh::{BorshDeserialize, BorshSerialize}; +use masp_primitives::asset_type::AssetType; +use masp_primitives::convert::AllowedConversion; +use masp_primitives::merkle_tree::FrozenCommitmentTree; +use masp_primitives::sapling::Node; + +use crate::types::address::Address; +use crate::types::storage::Epoch; + +/// A representation of the conversion state +#[derive(Debug, Default, BorshSerialize, BorshDeserialize)] +pub struct ConversionState { + /// The merkle root from the previous epoch + pub prev_root: Node, + /// The tree currently containing all the conversions + pub tree: FrozenCommitmentTree, + /// Map assets to their latest conversion and position in Merkle tree + pub assets: BTreeMap, +} + +// This is only enabled when "wasm-runtime" is on, because we're using rayon +#[cfg(feature = "wasm-runtime")] +/// Update the MASP's allowed conversions +pub fn update_allowed_conversions( + wl_storage: &mut super::WlStorage, +) -> crate::ledger::storage_api::Result<()> +where + D: super::DB + for<'iter> super::DBIter<'iter>, + H: super::StorageHasher, +{ + use masp_primitives::ff::PrimeField; + use masp_primitives::transaction::components::Amount as MaspAmount; + use rayon::iter::{ + IndexedParallelIterator, IntoParallelIterator, ParallelIterator, + }; + use rayon::prelude::ParallelSlice; + + use crate::ledger::storage_api::{ResultExt, StorageRead, StorageWrite}; + use crate::types::storage::{self, KeySeg}; + use crate::types::{address, token}; + + // The derived conversions will be placed in MASP address space + let masp_addr = address::masp(); + let key_prefix: storage::Key = masp_addr.to_db_key().into(); + + let masp_rewards = address::masp_rewards(); + // The total transparent value of the rewards being distributed + let mut total_reward = token::Amount::from(0); + + // Construct MASP asset type for rewards. Always timestamp reward tokens + // with the zeroth epoch to minimize the number of convert notes clients + // have to use. This trick works under the assumption that reward tokens + // from different epochs are exactly equivalent. + let reward_asset_bytes = (address::nam(), 0u64) + .try_to_vec() + .expect("unable to serialize address and epoch"); + let reward_asset = AssetType::new(reward_asset_bytes.as_ref()) + .expect("unable to derive asset identifier"); + // Conversions from the previous to current asset for each address + let mut current_convs = BTreeMap::::new(); + // Reward all tokens according to above reward rates + for (addr, reward) in &masp_rewards { + // Dispence a transparent reward in parallel to the shielded rewards + let addr_bal: token::Amount = wl_storage + .read(&token::balance_key(addr, &masp_addr))? + .unwrap_or_default(); + // The reward for each reward.1 units of the current asset is + // reward.0 units of the reward token + // Since floor(a) + floor(b) <= floor(a+b), there will always be + // enough rewards to reimburse users + total_reward += (addr_bal * *reward).0; + // Provide an allowed conversion from previous timestamp. The + // negative sign allows each instance of the old asset to be + // cancelled out/replaced with the new asset + let old_asset = + encode_asset_type(addr.clone(), wl_storage.storage.last_epoch); + let new_asset = + encode_asset_type(addr.clone(), wl_storage.storage.block.epoch); + current_convs.insert( + addr.clone(), + (MaspAmount::from_pair(old_asset, -(reward.1 as i64)).unwrap() + + MaspAmount::from_pair(new_asset, reward.1).unwrap() + + MaspAmount::from_pair(reward_asset, reward.0).unwrap()) + .into(), + ); + // Add a conversion from the previous asset type + wl_storage.storage.conversion_state.assets.insert( + old_asset, + ( + addr.clone(), + wl_storage.storage.last_epoch, + MaspAmount::zero().into(), + 0, + ), + ); + } + + // Try to distribute Merkle leaf updating as evenly as possible across + // multiple cores + let num_threads = rayon::current_num_threads(); + // Put assets into vector to enable computation batching + let assets: Vec<_> = wl_storage + .storage + .conversion_state + .assets + .values_mut() + .enumerate() + .collect(); + // ceil(assets.len() / num_threads) + let notes_per_thread_max = (assets.len() - 1) / num_threads + 1; + // floor(assets.len() / num_threads) + let notes_per_thread_min = assets.len() / num_threads; + // Now on each core, add the latest conversion to each conversion + let conv_notes: Vec = assets + .into_par_iter() + .with_min_len(notes_per_thread_min) + .with_max_len(notes_per_thread_max) + .map(|(idx, (addr, _epoch, conv, pos))| { + // Use transitivity to update conversion + *conv += current_convs[addr].clone(); + // Update conversion position to leaf we are about to create + *pos = idx; + // The merkle tree need only provide the conversion commitment, + // the remaining information is provided through the storage API + Node::new(conv.cmu().to_repr()) + }) + .collect(); + + // Update the MASP's transparent reward token balance to ensure that it + // is sufficiently backed to redeem rewards + let reward_key = token::balance_key(&address::nam(), &masp_addr); + let addr_bal: token::Amount = + wl_storage.read(&reward_key)?.unwrap_or_default(); + let new_bal = addr_bal + total_reward; + wl_storage.write(&reward_key, new_bal)?; + // Try to distribute Merkle tree construction as evenly as possible + // across multiple cores + // Merkle trees must have exactly 2^n leaves to be mergeable + let mut notes_per_thread_rounded = 1; + while notes_per_thread_max > notes_per_thread_rounded * 4 { + notes_per_thread_rounded *= 2; + } + // Make the sub-Merkle trees in parallel + let tree_parts: Vec<_> = conv_notes + .par_chunks(notes_per_thread_rounded) + .map(FrozenCommitmentTree::new) + .collect(); + + // Keep the merkle root from the old tree for transactions constructed + // close to the epoch boundary + wl_storage.storage.conversion_state.prev_root = + wl_storage.storage.conversion_state.tree.root(); + + // Convert conversion vector into tree so that Merkle paths can be + // obtained + wl_storage.storage.conversion_state.tree = + FrozenCommitmentTree::merge(&tree_parts); + + // Add purely decoding entries to the assets map. These will be + // overwritten before the creation of the next commitment tree + for addr in masp_rewards.keys() { + // Add the decoding entry for the new asset type. An uncommited + // node position is used since this is not a conversion. + let new_asset = + encode_asset_type(addr.clone(), wl_storage.storage.block.epoch); + wl_storage.storage.conversion_state.assets.insert( + new_asset, + ( + addr.clone(), + wl_storage.storage.block.epoch, + MaspAmount::zero().into(), + wl_storage.storage.conversion_state.tree.size(), + ), + ); + } + + // Save the current conversion state in order to avoid computing + // conversion commitments from scratch in the next epoch + let state_key = key_prefix + .push(&(token::CONVERSION_KEY_PREFIX.to_owned())) + .into_storage_result()?; + // We cannot borrow `conversion_state` at the same time as when we call + // `wl_storage.write`, so we encode it manually first + let conv_bytes = wl_storage + .storage + .conversion_state + .try_to_vec() + .into_storage_result()?; + wl_storage.write_bytes(&state_key, conv_bytes)?; + Ok(()) +} + +/// Construct MASP asset type with given epoch for given token +pub fn encode_asset_type(addr: Address, epoch: Epoch) -> AssetType { + let new_asset_bytes = (addr, epoch.0) + .try_to_vec() + .expect("unable to serialize address and epoch"); + AssetType::new(new_asset_bytes.as_ref()) + .expect("unable to derive asset identifier") +} diff --git a/core/src/ledger/storage/mod.rs b/core/src/ledger/storage/mod.rs index 66049b40148..e2ac4da2351 100644 --- a/core/src/ledger/storage/mod.rs +++ b/core/src/ledger/storage/mod.rs @@ -1,6 +1,7 @@ //! Ledger's state storage with key-value backed store and a merkle tree pub mod ics23_specs; +mod masp_conversions; pub mod merkle_tree; #[cfg(any(test, feature = "testing"))] pub mod mockdb; @@ -10,30 +11,21 @@ mod wl_storage; pub mod write_log; use core::fmt::Debug; -use std::collections::BTreeMap; -use borsh::{BorshDeserialize, BorshSerialize}; -use masp_primitives::asset_type::AssetType; -use masp_primitives::convert::AllowedConversion; -use masp_primitives::merkle_tree::FrozenCommitmentTree; -use masp_primitives::sapling::Node; use merkle_tree::StorageBytes; pub use merkle_tree::{ MembershipProof, MerkleTree, MerkleTreeStoresRead, MerkleTreeStoresWrite, StoreType, }; -#[cfg(feature = "wasm-runtime")] -use rayon::iter::{ - IndexedParallelIterator, IntoParallelIterator, ParallelIterator, -}; -#[cfg(feature = "wasm-runtime")] -use rayon::prelude::ParallelSlice; use thiserror::Error; pub use traits::{Sha256Hasher, StorageHasher}; pub use wl_storage::{ iter_prefix_post, iter_prefix_pre, PrefixIter, WlStorage, }; +#[cfg(feature = "wasm-runtime")] +pub use self::masp_conversions::update_allowed_conversions; +pub use self::masp_conversions::{encode_asset_type, ConversionState}; use crate::ledger::gas::MIN_STORAGE_GAS; use crate::ledger::parameters::{self, EpochDuration, Parameters}; use crate::ledger::storage::merkle_tree::{ @@ -57,16 +49,6 @@ use crate::types::token; /// A result of a function that may fail pub type Result = std::result::Result; -/// A representation of the conversion state -#[derive(Debug, Default, BorshSerialize, BorshDeserialize)] -pub struct ConversionState { - /// The merkle root from the previous epoch - pub prev_root: Node, - /// The tree currently containing all the conversions - pub tree: FrozenCommitmentTree, - /// Map assets to their latest conversion and position in Merkle tree - pub assets: BTreeMap, -} /// The storage data #[derive(Debug)] @@ -730,7 +712,6 @@ where /// Initialize a new epoch when the current epoch is finished. Returns /// `true` on a new epoch. - #[cfg(feature = "wasm-runtime")] pub fn update_epoch( &mut self, height: BlockHeight, @@ -758,7 +739,6 @@ where .pred_epochs .new_epoch(height, evidence_max_age_num_blocks); tracing::info!("Began a new epoch {}", self.block.epoch); - self.update_allowed_conversions()?; } self.update_epoch_in_merkle_tree()?; Ok(new_epoch) @@ -769,172 +749,6 @@ where &self.conversion_state } - // Construct MASP asset type with given timestamp for given token - #[cfg(feature = "wasm-runtime")] - fn encode_asset_type(addr: Address, epoch: Epoch) -> AssetType { - let new_asset_bytes = (addr, epoch.0) - .try_to_vec() - .expect("unable to serialize address and epoch"); - AssetType::new(new_asset_bytes.as_ref()) - .expect("unable to derive asset identifier") - } - - #[cfg(feature = "wasm-runtime")] - /// Update the MASP's allowed conversions - fn update_allowed_conversions(&mut self) -> Result<()> { - use masp_primitives::ff::PrimeField; - use masp_primitives::transaction::components::Amount as MaspAmount; - - use crate::types::address::{masp_rewards, nam}; - - // The derived conversions will be placed in MASP address space - let masp_addr = masp(); - let key_prefix: Key = masp_addr.to_db_key().into(); - - let masp_rewards = masp_rewards(); - // The total transparent value of the rewards being distributed - let mut total_reward = token::Amount::from(0); - - // Construct MASP asset type for rewards. Always timestamp reward tokens - // with the zeroth epoch to minimize the number of convert notes clients - // have to use. This trick works under the assumption that reward tokens - // from different epochs are exactly equivalent. - let reward_asset_bytes = (nam(), 0u64) - .try_to_vec() - .expect("unable to serialize address and epoch"); - let reward_asset = AssetType::new(reward_asset_bytes.as_ref()) - .expect("unable to derive asset identifier"); - // Conversions from the previous to current asset for each address - let mut current_convs = BTreeMap::::new(); - // Reward all tokens according to above reward rates - for (addr, reward) in &masp_rewards { - // Dispence a transparent reward in parallel to the shielded rewards - let token_key = self.read(&token::balance_key(addr, &masp_addr)); - if let Ok((Some(addr_balance), _)) = token_key { - // The reward for each reward.1 units of the current asset is - // reward.0 units of the reward token - let addr_bal: token::Amount = - types::decode(addr_balance).expect("invalid balance"); - // Since floor(a) + floor(b) <= floor(a+b), there will always be - // enough rewards to reimburse users - total_reward += (addr_bal * *reward).0; - } - // Provide an allowed conversion from previous timestamp. The - // negative sign allows each instance of the old asset to be - // cancelled out/replaced with the new asset - let old_asset = - Self::encode_asset_type(addr.clone(), self.last_epoch); - let new_asset = - Self::encode_asset_type(addr.clone(), self.block.epoch); - current_convs.insert( - addr.clone(), - (MaspAmount::from_pair(old_asset, -(reward.1 as i64)).unwrap() - + MaspAmount::from_pair(new_asset, reward.1).unwrap() - + MaspAmount::from_pair(reward_asset, reward.0).unwrap()) - .into(), - ); - // Add a conversion from the previous asset type - self.conversion_state.assets.insert( - old_asset, - (addr.clone(), self.last_epoch, MaspAmount::zero().into(), 0), - ); - } - - // Try to distribute Merkle leaf updating as evenly as possible across - // multiple cores - let num_threads = rayon::current_num_threads(); - // Put assets into vector to enable computation batching - let assets: Vec<_> = self - .conversion_state - .assets - .values_mut() - .enumerate() - .collect(); - // ceil(assets.len() / num_threads) - let notes_per_thread_max = (assets.len() - 1) / num_threads + 1; - // floor(assets.len() / num_threads) - let notes_per_thread_min = assets.len() / num_threads; - // Now on each core, add the latest conversion to each conversion - let conv_notes: Vec = assets - .into_par_iter() - .with_min_len(notes_per_thread_min) - .with_max_len(notes_per_thread_max) - .map(|(idx, (addr, _epoch, conv, pos))| { - // Use transitivity to update conversion - *conv += current_convs[addr].clone(); - // Update conversion position to leaf we are about to create - *pos = idx; - // The merkle tree need only provide the conversion commitment, - // the remaining information is provided through the storage API - Node::new(conv.cmu().to_repr()) - }) - .collect(); - - // Update the MASP's transparent reward token balance to ensure that it - // is sufficiently backed to redeem rewards - let reward_key = token::balance_key(&nam(), &masp_addr); - if let Ok((Some(addr_bal), _)) = self.read(&reward_key) { - // If there is already a balance, then add to it - let addr_bal: token::Amount = - types::decode(addr_bal).expect("invalid balance"); - let new_bal = types::encode(&(addr_bal + total_reward)); - self.write(&reward_key, new_bal) - .expect("unable to update MASP transparent balance"); - } else { - // Otherwise the rewards form the entirity of the reward token - // balance - self.write(&reward_key, types::encode(&total_reward)) - .expect("unable to update MASP transparent balance"); - } - // Try to distribute Merkle tree construction as evenly as possible - // across multiple cores - // Merkle trees must have exactly 2^n leaves to be mergeable - let mut notes_per_thread_rounded = 1; - while notes_per_thread_max > notes_per_thread_rounded * 4 { - notes_per_thread_rounded *= 2; - } - // Make the sub-Merkle trees in parallel - let tree_parts: Vec<_> = conv_notes - .par_chunks(notes_per_thread_rounded) - .map(FrozenCommitmentTree::new) - .collect(); - - // Keep the merkle root from the old tree for transactions constructed - // close to the epoch boundary - self.conversion_state.prev_root = self.conversion_state.tree.root(); - - // Convert conversion vector into tree so that Merkle paths can be - // obtained - self.conversion_state.tree = FrozenCommitmentTree::merge(&tree_parts); - - // Add purely decoding entries to the assets map. These will be - // overwritten before the creation of the next commitment tree - for addr in masp_rewards.keys() { - // Add the decoding entry for the new asset type. An uncommited - // node position is used since this is not a conversion. - let new_asset = - Self::encode_asset_type(addr.clone(), self.block.epoch); - self.conversion_state.assets.insert( - new_asset, - ( - addr.clone(), - self.block.epoch, - MaspAmount::zero().into(), - self.conversion_state.tree.size(), - ), - ); - } - - // Save the current conversion state in order to avoid computing - // conversion commitments from scratch in the next epoch - let state_key = key_prefix - .push(&(token::CONVERSION_KEY_PREFIX.to_owned())) - .map_err(Error::KeyError)?; - self.write(&state_key, types::encode(&self.conversion_state)) - .expect("unable to save current conversion state"); - Ok(()) - } - /// Update the merkle tree with epoch data fn update_epoch_in_merkle_tree(&mut self) -> Result<()> { let key_prefix: Key = From 8d15f5a0975452099fa94636cc90bf234fb0e783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 1 Feb 2023 13:10:52 +0100 Subject: [PATCH 04/11] test/test_vp_host_env: update to write via tx --- tests/src/vm_host_env/mod.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index 3f4adf6df5b..04a545e8b10 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -267,14 +267,13 @@ mod tests { /// An example how to write a VP host environment integration test #[test] fn test_vp_host_env() { - // The environment must be initialized first - vp_host_env::init(); - - // We can add some data to the environment - let key_raw = "key"; - let key = storage::Key::parse(key_raw).unwrap(); let value = "test".to_string(); - vp_host_env::with(|env| env.wl_storage.write(&key, &value).unwrap()); + let addr = address::testing::established_address_1(); + let key = storage::Key::from(addr.to_db_key()); + // We can write some data from a transaction + vp_host_env::init_from_tx(addr, TestTxEnv::default(), |_addr| { + tx::ctx().write(&key, &value).unwrap(); + }); let read_pre_value: Option = vp::CTX.read_pre(&key).unwrap(); assert_eq!(None, read_pre_value); From 5f8f43bcdc0fa0a1ed98295919f04b91d8b87bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 2 Feb 2023 11:43:57 +0100 Subject: [PATCH 05/11] move gov mod from tx_prelude to core storage_api --- {tx_prelude/src => core/src/ledger/storage_api}/governance.rs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {tx_prelude/src => core/src/ledger/storage_api}/governance.rs (100%) diff --git a/tx_prelude/src/governance.rs b/core/src/ledger/storage_api/governance.rs similarity index 100% rename from tx_prelude/src/governance.rs rename to core/src/ledger/storage_api/governance.rs From 1d94a9bb80a5c74e798678fa931b705f310bec28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 2 Feb 2023 13:50:01 +0100 Subject: [PATCH 06/11] gov + token: refactor using storage_api --- core/src/ledger/storage_api/governance.rs | 61 ++++++++++--------- core/src/ledger/storage_api/mod.rs | 2 + core/src/ledger/storage_api/token.rs | 72 +++++++++++++++++++++++ core/src/types/token.rs | 9 ++- tx_prelude/src/lib.rs | 5 +- 5 files changed, 118 insertions(+), 31 deletions(-) create mode 100644 core/src/ledger/storage_api/token.rs diff --git a/core/src/ledger/storage_api/governance.rs b/core/src/ledger/storage_api/governance.rs index 3e1afb6a68b..c6197ebbb3f 100644 --- a/core/src/ledger/storage_api/governance.rs +++ b/core/src/ledger/storage_api/governance.rs @@ -1,79 +1,86 @@ //! Governance -use namada_core::ledger::governance::{storage, ADDRESS as governance_address}; -use namada_core::types::token::Amount; -use namada_core::types::transaction::governance::{ +use super::token; +use crate::ledger::governance::{storage, ADDRESS as governance_address}; +use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; +use crate::types::transaction::governance::{ InitProposalData, VoteProposalData, }; -use super::*; -use crate::token::transfer; - /// A proposal creation transaction. -pub fn init_proposal(ctx: &mut Ctx, data: InitProposalData) -> TxResult { +pub fn init_proposal( + storage: &mut S, + data: InitProposalData, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ let counter_key = storage::get_counter_key(); let proposal_id = if let Some(id) = data.id { id } else { - ctx.read(&counter_key)?.unwrap() + storage.read(&counter_key)?.unwrap() }; let content_key = storage::get_content_key(proposal_id); - ctx.write_bytes(&content_key, data.content)?; + storage.write_bytes(&content_key, data.content)?; let author_key = storage::get_author_key(proposal_id); - ctx.write(&author_key, data.author.clone())?; + storage.write(&author_key, data.author.clone())?; let voting_start_epoch_key = storage::get_voting_start_epoch_key(proposal_id); - ctx.write(&voting_start_epoch_key, data.voting_start_epoch)?; + storage.write(&voting_start_epoch_key, data.voting_start_epoch)?; let voting_end_epoch_key = storage::get_voting_end_epoch_key(proposal_id); - ctx.write(&voting_end_epoch_key, data.voting_end_epoch)?; + storage.write(&voting_end_epoch_key, data.voting_end_epoch)?; let grace_epoch_key = storage::get_grace_epoch_key(proposal_id); - ctx.write(&grace_epoch_key, data.grace_epoch)?; + storage.write(&grace_epoch_key, data.grace_epoch)?; if let Some(proposal_code) = data.proposal_code { let proposal_code_key = storage::get_proposal_code_key(proposal_id); - ctx.write_bytes(&proposal_code_key, proposal_code)?; + storage.write_bytes(&proposal_code_key, proposal_code)?; } - ctx.write(&counter_key, proposal_id + 1)?; + storage.write(&counter_key, proposal_id + 1)?; let min_proposal_funds_key = storage::get_min_proposal_fund_key(); - let min_proposal_funds: Amount = - ctx.read(&min_proposal_funds_key)?.unwrap(); + let min_proposal_funds: token::Amount = + storage.read(&min_proposal_funds_key)?.unwrap(); let funds_key = storage::get_funds_key(proposal_id); - ctx.write(&funds_key, min_proposal_funds)?; + storage.write(&funds_key, min_proposal_funds)?; // this key must always be written for each proposal let committing_proposals_key = storage::get_committing_proposals_key(proposal_id, data.grace_epoch.0); - ctx.write(&committing_proposals_key, ())?; + storage.write(&committing_proposals_key, ())?; - transfer( - ctx, + token::transfer( + storage, + &storage.get_native_token()?, &data.author, &governance_address, - &ctx.get_native_token()?, - None, min_proposal_funds, - &None, - &None, ) } /// A proposal vote transaction. -pub fn vote_proposal(ctx: &mut Ctx, data: VoteProposalData) -> TxResult { +pub fn vote_proposal( + storage: &mut S, + data: VoteProposalData, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ for delegation in data.delegations { let vote_key = storage::get_vote_proposal_key( data.id, data.voter.clone(), delegation, ); - ctx.write(&vote_key, data.vote.clone())?; + storage.write(&vote_key, data.vote.clone())?; } Ok(()) } diff --git a/core/src/ledger/storage_api/mod.rs b/core/src/ledger/storage_api/mod.rs index c929aec03b9..1a4bcd13da6 100644 --- a/core/src/ledger/storage_api/mod.rs +++ b/core/src/ledger/storage_api/mod.rs @@ -3,7 +3,9 @@ pub mod collections; mod error; +pub mod governance; pub mod key; +pub mod token; pub mod validation; use borsh::{BorshDeserialize, BorshSerialize}; diff --git a/core/src/ledger/storage_api/token.rs b/core/src/ledger/storage_api/token.rs new file mode 100644 index 00000000000..3ee3b84f352 --- /dev/null +++ b/core/src/ledger/storage_api/token.rs @@ -0,0 +1,72 @@ +//! Token storage_api functions + +use super::{StorageRead, StorageWrite}; +use crate::ledger::storage_api; +use crate::types::address::Address; +use crate::types::token; +pub use crate::types::token::Amount; + +/// Read the balance of a given token and owner. +pub fn read_balance( + storage: &S, + token: &Address, + owner: &Address, +) -> storage_api::Result +where + S: StorageRead, +{ + let key = token::balance_key(token, owner); + let balance = storage.read::(&key)?.unwrap_or_default(); + Ok(balance) +} + +/// Transfer `token` from `src` to `dest`. Returns an `Err` if `src` has +/// insufficient balance or if the transfer the `dest` would overflow (This can +/// only happen if the total supply does't fit in `token::Amount`). +pub fn transfer( + storage: &mut S, + token: &Address, + src: &Address, + dest: &Address, + amount: token::Amount, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let src_key = token::balance_key(token, src); + let src_balance = read_balance(storage, token, src)?; + match src_balance.checked_sub(amount) { + Some(new_src_balance) => { + let dest_key = token::balance_key(token, dest); + let dest_balance = read_balance(storage, token, dest)?; + match dest_balance.checked_add(amount) { + Some(new_dest_balance) => { + storage.write(&src_key, new_src_balance)?; + storage.write(&dest_key, new_dest_balance) + } + None => Err(storage_api::Error::new_const( + "The transfer would overflow destination balance", + )), + } + } + None => { + Err(storage_api::Error::new_const("Insufficient source balance")) + } + } +} + +/// Credit tokens to an account, to be used only by protocol. In transactions, +/// this would get rejected by the default `vp_token`. +pub fn credit_tokens( + storage: &mut S, + token: &Address, + dest: &Address, + amount: token::Amount, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = token::balance_key(token, dest); + let new_balance = read_balance(storage, token, dest)? + amount; + storage.write(&key, new_balance) +} diff --git a/core/src/types/token.rs b/core/src/types/token.rs index becbe323a31..82ff045096d 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -71,7 +71,14 @@ impl Amount { Self { micro: u64::MAX } } - /// Checked subtraction + /// Checked addition. Returns `None` on overflow. + pub fn checked_add(&self, amount: Amount) -> Option { + self.micro + .checked_add(amount.micro) + .map(|result| Self { micro: result }) + } + + /// Checked subtraction. Returns `None` on underflow pub fn checked_sub(&self, amount: Amount) -> Option { self.micro .checked_sub(amount.micro) diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index 9d66dcb73e2..6e4893cbfc7 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -6,7 +6,6 @@ #![deny(rustdoc::broken_intra_doc_links)] #![deny(rustdoc::private_intra_doc_links)] -pub mod governance; pub mod ibc; pub mod key; pub mod proof_of_stake; @@ -21,8 +20,8 @@ pub use namada_core::ledger::parameters::storage as parameters_storage; pub use namada_core::ledger::slash_fund::storage as slash_fund_storage; pub use namada_core::ledger::storage::types::encode; pub use namada_core::ledger::storage_api::{ - self, iter_prefix, iter_prefix_bytes, Error, OptionExt, ResultExt, - StorageRead, StorageWrite, + self, governance, iter_prefix, iter_prefix_bytes, Error, OptionExt, + ResultExt, StorageRead, StorageWrite, }; pub use namada_core::ledger::tx_env::TxEnv; pub use namada_core::proto::{Signed, SignedTxData}; From 386f3e524650279b7f5c0e32c459de6e644c4d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 2 Feb 2023 14:00:32 +0100 Subject: [PATCH 07/11] test/finalize_block: ensure that proposal doesn't commit to DB --- .../lib/node/ledger/shell/finalize_block.rs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index c55479dd0b5..ed774520f6b 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -447,8 +447,13 @@ mod test_finalize_block { use std::str::FromStr; use namada::ledger::parameters::EpochDuration; + use namada::ledger::storage_api; + use namada::types::governance::ProposalVote; use namada::types::storage::Epoch; use namada::types::time::DurationSecs; + use namada::types::transaction::governance::{ + InitProposalData, VoteProposalData, + }; use namada::types::transaction::{EncryptionKey, Fee, WrapperTx, MIN_FEE}; use super::*; @@ -820,6 +825,41 @@ mod test_finalize_block { .unwrap(); shell.wl_storage.storage.next_epoch_min_start_height = BlockHeight(5); shell.wl_storage.storage.next_epoch_min_start_time = DateTimeUtc::now(); + + // Add a proposal to be executed on next epoch change. + let mut add_proposal = |proposal_id, vote| { + let validator = shell.mode.get_validator_address().unwrap().clone(); + shell.proposal_data.insert(proposal_id); + let proposal = InitProposalData { + id: Some(proposal_id), + content: vec![], + author: validator.clone(), + voting_start_epoch: Epoch::default(), + voting_end_epoch: Epoch::default().next(), + grace_epoch: Epoch::default().next(), + proposal_code: None, + }; + storage_api::governance::init_proposal( + &mut shell.wl_storage, + proposal, + ) + .unwrap(); + let vote = VoteProposalData { + id: proposal_id, + vote, + voter: validator, + delegations: vec![], + }; + // Vote to accept the proposal (there's only one validator, so its + // vote decides) + storage_api::governance::vote_proposal(&mut shell.wl_storage, vote) + .unwrap(); + }; + // Add a proposal to be accepted and one to be rejected. + add_proposal(0, ProposalVote::Yay); + add_proposal(1, ProposalVote::Nay); + + // Commit the genesis state shell.wl_storage.commit_genesis().unwrap(); shell.commit(); From 0c304cd22c367592546f95f5ed50e46ec08fd150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 2 Feb 2023 14:01:46 +0100 Subject: [PATCH 08/11] shell/gov: apply changes via `WlStorage` write log --- apps/src/lib/node/ledger/shell/governance.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index ce16a638db3..a27814029d0 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -9,10 +9,10 @@ use namada::ledger::native_vp::governance::utils::{ use namada::ledger::protocol; use namada::ledger::storage::types::encode; use namada::ledger::storage::{DBIter, StorageHasher, DB}; +use namada::ledger::storage_api::{token, StorageWrite}; use namada::types::address::Address; use namada::types::governance::TallyResult; use namada::types::storage::Epoch; -use namada::types::token; use super::*; @@ -84,8 +84,7 @@ where gov_storage::get_proposal_execution_key(id); shell .wl_storage - .storage - .write(&pending_execution_key, "") + .write(&pending_execution_key, ()) .expect("Should be able to write to storage."); let tx_result = protocol::apply_tx( tx_type, @@ -101,7 +100,6 @@ where ); shell .wl_storage - .storage .delete(&pending_execution_key) .expect("Should be able to delete the storage."); match tx_result { @@ -206,11 +204,16 @@ where let native_token = shell.wl_storage.storage.native_token.clone(); // transfer proposal locked funds - shell.wl_storage.transfer( + token::transfer( + &mut shell.wl_storage, &native_token, - funds, &gov_address, &transfer_address, + funds, + ) + .expect( + "Must be able to transfer governance locked funds after proposal \ + has been tallied", ); } From 93e4374e7be97f052b6dfea22345dff241840feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 2 Feb 2023 14:02:14 +0100 Subject: [PATCH 09/11] shell/queries: refactor token query using storage_api token mod --- apps/src/lib/node/ledger/shell/queries.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/queries.rs b/apps/src/lib/node/ledger/shell/queries.rs index 4f63924b0f4..c04c87408a8 100644 --- a/apps/src/lib/node/ledger/shell/queries.rs +++ b/apps/src/lib/node/ledger/shell/queries.rs @@ -4,10 +4,10 @@ use borsh::BorshSerialize; use ferveo_common::TendermintValidator; use namada::ledger::pos::into_tm_voting_power; use namada::ledger::queries::{RequestCtx, ResponseQuery}; -use namada::ledger::storage_api; +use namada::ledger::storage_api::token; use namada::types::address::Address; +use namada::types::key; use namada::types::key::dkg_session_keys::DkgPublicKey; -use namada::types::{key, token}; use super::*; use crate::node::ledger::response; @@ -69,15 +69,10 @@ where token: &Address, owner: &Address, ) -> token::Amount { - let balance = storage_api::StorageRead::read( - &self.wl_storage, - &token::balance_key(token, owner), - ); // Storage read must not fail, but there might be no value, in which // case default (0) is returned - balance - .expect("Storage read in the protocol must not fail") - .unwrap_or_default() + token::read_balance(&self.wl_storage, token, owner) + .expect("Token balance read in the protocol must not fail") } /// Lookup data about a validator from their protocol signing key From 17427faebafbd08b3ddd1938aff418d7dbecb215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 1 Feb 2023 17:28:58 +0100 Subject: [PATCH 10/11] changelog: add #1108 --- .changelog/unreleased/improvements/1108-protocol-write-log.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changelog/unreleased/improvements/1108-protocol-write-log.md diff --git a/.changelog/unreleased/improvements/1108-protocol-write-log.md b/.changelog/unreleased/improvements/1108-protocol-write-log.md new file mode 100644 index 00000000000..34e395b7e04 --- /dev/null +++ b/.changelog/unreleased/improvements/1108-protocol-write-log.md @@ -0,0 +1,4 @@ +- Improved the `WlStorage` to write protocol changes via block-level write log. + This is then used to make sure that no storage changes are committed in ABCI + `FinalizeBlock` request handler and only in the `Commit` handler. + ([#1108](https://github.com/anoma/namada/pull/1108)) From c5d77bb09feb47a30698745b6b4a5c32dcb9d58f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 2 Feb 2023 14:42:45 +0000 Subject: [PATCH 11/11] [ci] wasm checksums update --- wasm/checksums.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index 425b24bc202..97a81cd714b 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,20 +1,20 @@ { - "tx_bond.wasm": "tx_bond.dd3099e3d4408d5cfbc2d5a4f8b7e8784d30fafe25653852cd314e00a0ab3a90.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.f3cd1434d0e0651d7a916e0eedad7d006e893d735485ca71e43b56e305cb929a.wasm", - "tx_ibc.wasm": "tx_ibc.3f4db4d3270a00fb03107c82fac975101c213c7a26dce4222311f43e0d1f56a9.wasm", - "tx_init_account.wasm": "tx_init_account.a325187897761411205170dad5d6e62b3f23843358178045b11b5bc855d91021.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.e5ecc4d33efbfdfe4f09f716fa9c115cadfb540c546910e16c1edc3d8a201b9f.wasm", - "tx_init_validator.wasm": "tx_init_validator.5d7bc76bb3fbba0758e6ac7d0737782370c34e5e0de5f4e04a97a35dff6033ab.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.9a351c937882e1bf5dc6a45740f47d18b82d89d626785d0795bba53f1940c18a.wasm", - "tx_transfer.wasm": "tx_transfer.c5e6a611da195185fe1002146601f8194076b6a6df2fcfaa5dabd054c11ba6ef.wasm", - "tx_unbond.wasm": "tx_unbond.122252b1faf2c6cd0c4138c1c28e5f1a4c25a8a32d9cae113b9a34796e276ca1.wasm", - "tx_update_vp.wasm": "tx_update_vp.6f3acf7e2e77589187815367eca221e5496bb6f6da5453a58de953dd4eca00cf.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.b2c6a0b4a46013587b682c9089af6ff869ec1bd4a6855e011caf5775a07a44a1.wasm", - "tx_withdraw.wasm": "tx_withdraw.d515807101fa226f1688054e72c102b48d95a17fc59cd8f5a226d99b404ef288.wasm", - "vp_implicit.wasm": "vp_implicit.f1428855df5d67bb915a8081d45cd4b70a3fd1e6f1de88a5dc20d7c0c56b5cd1.wasm", - "vp_masp.wasm": "vp_masp.6f5676f0544badb01c7097e2f74c689473fcb684d732f2745f9c347465d06b3f.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.24f8abc18751fd40cbc40c92346011bb4db8d17bc9cf7b5a416f17cd707cbb13.wasm", - "vp_token.wasm": "vp_token.7522b17d69915cfdbb80fd28be7fc9c16551969b8dece33d95e8cc1c564b5f24.wasm", - "vp_user.wasm": "vp_user.2bf9f9ad541d5c3554812d71e703b8fc1a994b7f3a5fd7226ea1936002094ffe.wasm", - "vp_validator.wasm": "vp_validator.71369ffbb9d12e8696fb6ea69e16c2cc9b40cdd79d93f4289c5799397eb8f175.wasm" + "tx_bond.wasm": "tx_bond.21b31932c21c1fa6cf798d8fd54641afa624fe0bfa845ffeed693c9fa56d589b.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.5429653ce3e95ae5f55d3598026656954f31f3821d53fc0bfd6fd5ca6a24910c.wasm", + "tx_ibc.wasm": "tx_ibc.73e94eeb0835b843a4f8d7c32ca06748dc9390c458c9e120678fe366a8f57816.wasm", + "tx_init_account.wasm": "tx_init_account.d5b0232d9e73a2d7ce547ea268a93b60438ffb4ca73c69fafc4d5fc02ce0bfdd.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.98cd31193dc8b0cd979601fed9613decb296bc66011da58e1684723e60e2fb36.wasm", + "tx_init_validator.wasm": "tx_init_validator.3e9afb24e734407f6666f1729b8883fd12eac63465e344de183fe892cbabdf9a.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.490b4db0219ba6132578caa0d195027ffb219b4674d0aa728040eebc832bd2b6.wasm", + "tx_transfer.wasm": "tx_transfer.284961e600b55894d1d88a7e59595df10d1fcbabbca8ab347ba13a0093d3720b.wasm", + "tx_unbond.wasm": "tx_unbond.84156558ecd703f17d5ddca101277fb12ae7a8d4030656f0851a85b75a0803d9.wasm", + "tx_update_vp.wasm": "tx_update_vp.32b3f5182b92d5def486a219623601f6bff44bc75720a9409544adf50d72efb8.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.410725465cb19b3e1e7edef4955b649786c9f489d4ef693e038fadc8cdc39bf7.wasm", + "tx_withdraw.wasm": "tx_withdraw.7cbead21973cb29f021bddc051f806d94c7ef05b739df8ed181813c46c9891e0.wasm", + "vp_implicit.wasm": "vp_implicit.e22961d1a709fa031291c6fe2376d2eda9615cd0464bc81bbe1dba698cdde511.wasm", + "vp_masp.wasm": "vp_masp.e777a6b69dd8a3c539075d1ffb8f98912af7c7a26d7eb0c24ec0bb3787fe218e.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.339763c59b73e04d4ac3da88dc9879a9466b4f3a24dad1684b668300ff358b6b.wasm", + "vp_token.wasm": "vp_token.0841186c46fe272c782c7c40f488f05d0ab751955ae6559be4764d41ad9925b5.wasm", + "vp_user.wasm": "vp_user.ee73368dd90d22bc2fabdbe449e438b504519b8710ea994c51937acce3e0eb53.wasm", + "vp_validator.wasm": "vp_validator.785090e9611968ba4e4fe74f69ce78c932dd8844ef64be45e5c023d9f50c574b.wasm" } \ No newline at end of file