From 1a57908bfb608145daef30c6110ea67713cfc0c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 20 Jul 2022 20:54:52 +0200 Subject: [PATCH 01/81] ledger: add tx and VP traits with host env functions --- shared/src/ledger/mod.rs | 1 + shared/src/ledger/tx_env.rs | 129 +++++++++++++++++++++++++++++ shared/src/ledger/vp_env.rs | 160 ++++++++++++++++++++++++++++++++---- 3 files changed, 275 insertions(+), 15 deletions(-) create mode 100644 shared/src/ledger/tx_env.rs diff --git a/shared/src/ledger/mod.rs b/shared/src/ledger/mod.rs index 2d545f96f2..7dd7bc2d46 100644 --- a/shared/src/ledger/mod.rs +++ b/shared/src/ledger/mod.rs @@ -9,4 +9,5 @@ pub mod parameters; pub mod pos; pub mod storage; pub mod treasury; +pub mod tx_env; pub mod vp_env; diff --git a/shared/src/ledger/tx_env.rs b/shared/src/ledger/tx_env.rs new file mode 100644 index 0000000000..578357b733 --- /dev/null +++ b/shared/src/ledger/tx_env.rs @@ -0,0 +1,129 @@ +//! Transaction environment contains functions that can be called from +//! inside a tx. + +use borsh::{BorshDeserialize, BorshSerialize}; + +use crate::types::address::Address; +use crate::types::ibc::IbcEvent; +use crate::types::storage::{self, BlockHash, BlockHeight, Epoch}; +use crate::types::time::Rfc3339String; + +/// Transaction host functions +pub trait TxEnv { + /// Storage read prefix iterator + type PrefixIter; + + /// Host functions possible errors, extensible with custom user errors. + type Error; + + /// Storage read Borsh encoded value. It will try to read from the write log + /// first and if no entry found then from the storage and then decode it if + /// found. + fn read( + &self, + key: &storage::Key, + ) -> Result, Self::Error>; + + /// Storage read raw bytes. It will try to read from the write log first and + /// if no entry found then from the storage. + fn read_bytes( + &self, + key: &storage::Key, + ) -> Result>, Self::Error>; + + /// Check if the storage contains the given key. It will try + /// to check the write log first and if no entry found then the storage. + fn has_key(&self, key: &storage::Key) -> Result; + + /// Getting the chain ID. + fn get_chain_id(&self) -> Result; + + /// Getting the block height. The height is that of the block to which the + /// current transaction is being applied. + fn get_block_height(&self) -> Result; + + /// Getting the block hash. The height is that of the block to which the + /// current transaction is being applied. + fn get_block_hash(&self) -> Result; + + /// Getting the block epoch. The epoch is that of the block to which the + /// current transaction is being applied. + fn get_block_epoch(&self) -> Result; + + /// Get time of the current block header as rfc 3339 string + fn get_block_time(&self) -> Result; + + /// Storage prefix iterator. It will try to get an iterator from the + /// storage. + fn iter_prefix( + &self, + prefix: &storage::Key, + ) -> Result; + + /// Storage prefix iterator next. It will try to read from the write log + /// first and if no entry found then from the storage. + fn iter_next( + &self, + iter: &mut Self::PrefixIter, + ) -> Result)>, Self::Error>; + + // --- MUTABLE ---- + + /// Write a value to be encoded with Borsh at the given key to storage. + fn write( + &mut self, + key: &storage::Key, + val: T, + ) -> Result<(), Self::Error>; + + /// Write a value as bytes at the given key to storage. + fn write_bytes( + &mut self, + key: &storage::Key, + val: impl AsRef<[u8]>, + ) -> Result<(), Self::Error>; + + /// Write a temporary value to be encoded with Borsh at the given key to + /// storage. + fn write_temp( + &mut self, + key: &storage::Key, + val: T, + ) -> Result<(), Self::Error>; + + /// Write a temporary value as bytes at the given key to storage. + fn write_bytes_temp( + &mut self, + key: &storage::Key, + val: impl AsRef<[u8]>, + ) -> Result<(), Self::Error>; + + /// Delete a value at the given key from storage. + fn delete(&mut self, key: &storage::Key) -> Result<(), Self::Error>; + + /// Insert a verifier address. This address must exist on chain, otherwise + /// the transaction will be rejected. + /// + /// Validity predicates of each verifier addresses inserted in the + /// transaction will validate the transaction and will receive all the + /// changed storage keys and initialized accounts in their inputs. + fn insert_verifier(&mut self, addr: &Address) -> Result<(), Self::Error>; + + /// Initialize a new account generates a new established address and + /// writes the given code as its validity predicate into the storage. + fn init_account( + &mut self, + code: impl AsRef<[u8]>, + ) -> Result; + + /// Update a validity predicate + fn update_validity_predicate( + &mut self, + addr: &Address, + code: impl AsRef<[u8]>, + ) -> Result<(), Self::Error>; + + /// Emit an IBC event. There can be only one event per transaction. On + /// multiple calls, only the last emitted event will be used. + fn emit_ibc_event(&mut self, event: &IbcEvent) -> Result<(), Self::Error>; +} diff --git a/shared/src/ledger/vp_env.rs b/shared/src/ledger/vp_env.rs index 1f59613e54..1a77c38312 100644 --- a/shared/src/ledger/vp_env.rs +++ b/shared/src/ledger/vp_env.rs @@ -3,6 +3,7 @@ use std::num::TryFromIntError; +use borsh::BorshDeserialize; use thiserror::Error; use super::gas::MIN_STORAGE_GAS; @@ -12,8 +13,134 @@ use crate::ledger::storage::write_log::WriteLog; use crate::ledger::storage::{self, write_log, Storage, StorageHasher}; use crate::proto::Tx; use crate::types::hash::Hash; +use crate::types::key::common; use crate::types::storage::{BlockHash, BlockHeight, Epoch, Key}; +/// Validity predicate's environment is available for native VPs and WASM VPs +pub trait VpEnv { + /// Storage read prefix iterator + type PrefixIter; + + /// Host functions possible error. + /// + /// In a native VP this may be out-of-gas error, however, because WASM VP is + /// sandboxed and gas accounting is injected by and handled in the host, + /// in WASM VP this error is [`std::convert::Infallible`]. + type Error; + + /// Storage read prior state Borsh encoded value (before tx execution). It + /// will try to read from the storage and decode it if found. + fn read_pre( + &self, + key: &Key, + ) -> Result, Self::Error>; + + /// Storage read prior state raw bytes (before tx execution). It + /// will try to read from the storage. + fn read_bytes_pre(&self, key: &Key) + -> Result>, Self::Error>; + + /// Storage read posterior state Borsh encoded value (after tx execution). + /// It will try to read from the write log first and if no entry found + /// then from the storage and then decode it if found. + fn read_post( + &self, + key: &Key, + ) -> Result, Self::Error>; + + /// Storage read posterior state raw bytes (after tx execution). It will try + /// to read from the write log first and if no entry found then from the + /// storage. + fn read_bytes_post( + &self, + key: &Key, + ) -> Result>, Self::Error>; + + /// Storage read temporary state Borsh encoded value (after tx execution). + /// It will try to read from only the write log and then decode it if + /// found. + fn read_temp( + &self, + key: &Key, + ) -> Result, Self::Error>; + + /// Storage read temporary state raw bytes (after tx execution). It will try + /// to read from only the write log. + fn read_bytes_temp( + &self, + key: &Key, + ) -> Result>, Self::Error>; + + /// Storage `has_key` in prior state (before tx execution). It will try to + /// read from the storage. + fn has_key_pre(&self, key: &Key) -> Result; + + /// Storage `has_key` in posterior state (after tx execution). It will try + /// to check the write log first and if no entry found then the storage. + fn has_key_post(&self, key: &Key) -> Result; + + /// Getting the chain ID. + fn get_chain_id(&self) -> Result; + + /// Getting the block height. The height is that of the block to which the + /// current transaction is being applied. + fn get_block_height(&self) -> Result; + + /// Getting the block hash. The height is that of the block to which the + /// current transaction is being applied. + fn get_block_hash(&self) -> Result; + + /// Getting the block epoch. The epoch is that of the block to which the + /// current transaction is being applied. + fn get_block_epoch(&self) -> Result; + + /// Storage prefix iterator. It will try to get an iterator from the + /// storage. + fn iter_prefix( + &self, + prefix: &Key, + ) -> Result; + + /// Storage prefix iterator for prior state (before tx execution). It will + /// try to read from the storage. + fn iter_pre_next( + &self, + iter: &mut Self::PrefixIter, + ) -> Result)>, Self::Error>; + + /// Storage prefix iterator next for posterior state (after tx execution). + /// It will try to read from the write log first and if no entry found + /// then from the storage. + fn iter_post_next( + &self, + iter: &mut Self::PrefixIter, + ) -> Result)>, Self::Error>; + + /// Evaluate a validity predicate with given data. The address, changed + /// storage keys and verifiers will have the same values as the input to + /// caller's validity predicate. + /// + /// If the execution fails for whatever reason, this will return `false`. + /// Otherwise returns the result of evaluation. + fn eval( + &self, + vp_code: Vec, + input_data: Vec, + ) -> Result; + + /// Verify a transaction signature. The signature is expected to have been + /// produced on the encoded transaction [`crate::proto::Tx`] + /// using [`crate::proto::Tx::sign`]. + fn verify_tx_signature( + &self, + pk: &common::PublicKey, + sig: &common::Signature, + ) -> Result; + + /// Get a tx hash + fn get_tx_code_hash(&self) -> Result; +} + /// These runtime errors will abort VP execution immediately #[allow(missing_docs)] #[derive(Error, Debug)] @@ -37,10 +164,10 @@ pub enum RuntimeError { } /// VP environment function result -pub type Result = std::result::Result; +pub type EnvResult = std::result::Result; /// Add a gas cost incured in a validity predicate -pub fn add_gas(gas_meter: &mut VpGasMeter, used_gas: u64) -> Result<()> { +pub fn add_gas(gas_meter: &mut VpGasMeter, used_gas: u64) -> EnvResult<()> { let result = gas_meter.add(used_gas).map_err(RuntimeError::OutOfGas); if let Err(err) = &result { tracing::info!("Stopping VP execution because of gas error: {}", err); @@ -55,7 +182,7 @@ pub fn read_pre( storage: &Storage, write_log: &WriteLog, key: &Key, -) -> Result>> +) -> EnvResult>> where DB: storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, @@ -96,7 +223,7 @@ pub fn read_post( storage: &Storage, write_log: &WriteLog, key: &Key, -) -> Result>> +) -> EnvResult>> where DB: storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, @@ -137,7 +264,7 @@ pub fn read_temp( gas_meter: &mut VpGasMeter, write_log: &WriteLog, key: &Key, -) -> Result>> { +) -> EnvResult>> { // Try to read from the write log first let (log_val, gas) = write_log.read(key); add_gas(gas_meter, gas)?; @@ -156,7 +283,7 @@ pub fn has_key_pre( gas_meter: &mut VpGasMeter, storage: &Storage, key: &Key, -) -> Result +) -> EnvResult where DB: storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, @@ -174,7 +301,7 @@ pub fn has_key_post( storage: &Storage, write_log: &WriteLog, key: &Key, -) -> Result +) -> EnvResult where DB: storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, @@ -204,7 +331,7 @@ where pub fn get_chain_id( gas_meter: &mut VpGasMeter, storage: &Storage, -) -> Result +) -> EnvResult where DB: storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, @@ -219,7 +346,7 @@ where pub fn get_block_height( gas_meter: &mut VpGasMeter, storage: &Storage, -) -> Result +) -> EnvResult where DB: storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, @@ -234,7 +361,7 @@ where pub fn get_block_hash( gas_meter: &mut VpGasMeter, storage: &Storage, -) -> Result +) -> EnvResult where DB: storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, @@ -246,7 +373,10 @@ where /// Getting the block hash. The height is that of the block to which the /// current transaction is being applied. -pub fn get_tx_code_hash(gas_meter: &mut VpGasMeter, tx: &Tx) -> Result { +pub fn get_tx_code_hash( + gas_meter: &mut VpGasMeter, + tx: &Tx, +) -> EnvResult { let hash = Hash(tx.code_hash()); add_gas(gas_meter, MIN_STORAGE_GAS)?; Ok(hash) @@ -257,7 +387,7 @@ pub fn get_tx_code_hash(gas_meter: &mut VpGasMeter, tx: &Tx) -> Result { pub fn get_block_epoch( gas_meter: &mut VpGasMeter, storage: &Storage, -) -> Result +) -> EnvResult where DB: storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, @@ -272,7 +402,7 @@ pub fn iter_prefix<'a, DB, H>( gas_meter: &mut VpGasMeter, storage: &'a Storage, prefix: &Key, -) -> Result<>::PrefixIter> +) -> EnvResult<>::PrefixIter> where DB: storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, @@ -287,7 +417,7 @@ where pub fn iter_pre_next( gas_meter: &mut VpGasMeter, iter: &mut >::PrefixIter, -) -> Result)>> +) -> EnvResult)>> where DB: storage::DB + for<'iter> storage::DBIter<'iter>, { @@ -305,7 +435,7 @@ pub fn iter_post_next( gas_meter: &mut VpGasMeter, write_log: &WriteLog, iter: &mut >::PrefixIter, -) -> Result)>> +) -> EnvResult)>> where DB: storage::DB + for<'iter> storage::DBIter<'iter>, { From 5d8c61a63479672cf7ac0d9583c0aa5c9ba5e31e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 20 Jul 2022 20:56:08 +0200 Subject: [PATCH 02/81] shared/vm: rename host_env TxEnv/VpEnv to TxVmEnv/VpVmEnv --- shared/src/vm/host_env.rs | 134 ++++++++++++++++----------------- shared/src/vm/wasm/host_env.rs | 10 +-- shared/src/vm/wasm/run.rs | 8 +- 3 files changed, 76 insertions(+), 76 deletions(-) diff --git a/shared/src/vm/host_env.rs b/shared/src/vm/host_env.rs index ac9f89c31e..fc7fd33e0c 100644 --- a/shared/src/vm/host_env.rs +++ b/shared/src/vm/host_env.rs @@ -68,7 +68,7 @@ pub enum TxRuntimeError { type TxResult = std::result::Result; /// A transaction's host environment -pub struct TxEnv<'a, MEM, DB, H, CA> +pub struct TxVmEnv<'a, MEM, DB, H, CA> where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -112,7 +112,7 @@ where pub cache_access: std::marker::PhantomData, } -impl<'a, MEM, DB, H, CA> TxEnv<'a, MEM, DB, H, CA> +impl<'a, MEM, DB, H, CA> TxVmEnv<'a, MEM, DB, H, CA> where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -167,7 +167,7 @@ where } } -impl Clone for TxEnv<'_, MEM, DB, H, CA> +impl Clone for TxVmEnv<'_, MEM, DB, H, CA> where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -207,7 +207,7 @@ where } /// A validity predicate's host environment -pub struct VpEnv<'a, MEM, DB, H, EVAL, CA> +pub struct VpVmEnv<'a, MEM, DB, H, EVAL, CA> where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -282,7 +282,7 @@ pub trait VpEvaluator { ) -> HostEnvResult; } -impl<'a, MEM, DB, H, EVAL, CA> VpEnv<'a, MEM, DB, H, EVAL, CA> +impl<'a, MEM, DB, H, EVAL, CA> VpVmEnv<'a, MEM, DB, H, EVAL, CA> where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -331,7 +331,7 @@ where } } -impl Clone for VpEnv<'_, MEM, DB, H, EVAL, CA> +impl Clone for VpVmEnv<'_, MEM, DB, H, EVAL, CA> where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -435,7 +435,7 @@ where /// Called from tx wasm to request to use the given gas amount pub fn tx_charge_gas( - env: &TxEnv, + env: &TxVmEnv, used_gas: i32, ) -> TxResult<()> where @@ -454,7 +454,7 @@ where /// Add a gas cost incured in a transaction pub fn tx_add_gas( - env: &TxEnv, + env: &TxVmEnv, used_gas: u64, ) -> TxResult<()> where @@ -477,9 +477,9 @@ where /// Called from VP wasm to request to use the given gas amount pub fn vp_charge_gas( - env: &VpEnv, + env: &VpVmEnv, used_gas: i32, -) -> vp_env::Result<()> +) -> vp_env::EnvResult<()> where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -499,7 +499,7 @@ where /// Storage `has_key` function exposed to the wasm VM Tx environment. It will /// try to check the write log first and if no entry found then the storage. pub fn tx_has_key( - env: &TxEnv, + env: &TxVmEnv, key_ptr: u64, key_len: u64, ) -> TxResult @@ -555,7 +555,7 @@ where /// Returns `-1` when the key is not present, or the length of the data when /// the key is present (the length may be `0`). pub fn tx_read( - env: &TxEnv, + env: &TxVmEnv, key_ptr: u64, key_len: u64, ) -> TxResult @@ -645,7 +645,7 @@ where /// any) back to the guest, the second step reads the value from cache into a /// pre-allocated buffer with the obtained size. pub fn tx_result_buffer( - env: &TxEnv, + env: &TxVmEnv, result_ptr: u64, ) -> TxResult<()> where @@ -667,7 +667,7 @@ where /// It will try to get an iterator from the storage and return the corresponding /// ID of the iterator. pub fn tx_iter_prefix( - env: &TxEnv, + env: &TxVmEnv, prefix_ptr: u64, prefix_len: u64, ) -> TxResult @@ -702,7 +702,7 @@ where /// Returns `-1` when the key is not present, or the length of the data when /// the key is present (the length may be `0`). pub fn tx_iter_next( - env: &TxEnv, + env: &TxVmEnv, iter_id: u64, ) -> TxResult where @@ -781,7 +781,7 @@ where /// Storage write function exposed to the wasm VM Tx environment. The given /// key/value will be written to the write log. pub fn tx_write( - env: &TxEnv, + env: &TxVmEnv, key_ptr: u64, key_len: u64, val_ptr: u64, @@ -822,7 +822,7 @@ where /// given key/value will be written only to the write log. It will be never /// written to the storage. pub fn tx_write_temp( - env: &TxEnv, + env: &TxVmEnv, key_ptr: u64, key_len: u64, val_ptr: u64, @@ -860,7 +860,7 @@ where } fn check_address_existence( - env: &TxEnv, + env: &TxVmEnv, key: &Key, ) -> TxResult<()> where @@ -904,7 +904,7 @@ where /// Storage delete function exposed to the wasm VM Tx environment. The given /// key/value will be written as deleted to the write log. pub fn tx_delete( - env: &TxEnv, + env: &TxVmEnv, key_ptr: u64, key_len: u64, ) -> TxResult<()> @@ -938,7 +938,7 @@ where /// Emitting an IBC event function exposed to the wasm VM Tx environment. /// The given IBC event will be set to the write log. pub fn tx_emit_ibc_event( - env: &TxEnv, + env: &TxVmEnv, event_ptr: u64, event_len: u64, ) -> TxResult<()> @@ -966,10 +966,10 @@ where /// Returns `-1` when the key is not present, or the length of the data when /// the key is present (the length may be `0`). pub fn vp_read_pre( - env: &VpEnv, + env: &VpVmEnv, key_ptr: u64, key_len: u64, -) -> vp_env::Result +) -> vp_env::EnvResult where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1017,10 +1017,10 @@ where /// Returns `-1` when the key is not present, or the length of the data when /// the key is present (the length may be `0`). pub fn vp_read_post( - env: &VpEnv, + env: &VpVmEnv, key_ptr: u64, key_len: u64, -) -> vp_env::Result +) -> vp_env::EnvResult where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1063,10 +1063,10 @@ where /// Returns `-1` when the key is not present, or the length of the data when /// the key is present (the length may be `0`). pub fn vp_read_temp( - env: &VpEnv, + env: &VpVmEnv, key_ptr: u64, key_len: u64, -) -> vp_env::Result +) -> vp_env::EnvResult where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1111,9 +1111,9 @@ where /// any) back to the guest, the second step reads the value from cache into a /// pre-allocated buffer with the obtained size. pub fn vp_result_buffer( - env: &VpEnv, + env: &VpVmEnv, result_ptr: u64, -) -> vp_env::Result<()> +) -> vp_env::EnvResult<()> where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1134,10 +1134,10 @@ where /// Storage `has_key` in prior state (before tx execution) function exposed to /// the wasm VM VP environment. It will try to read from the storage. pub fn vp_has_key_pre( - env: &VpEnv, + env: &VpVmEnv, key_ptr: u64, key_len: u64, -) -> vp_env::Result +) -> vp_env::EnvResult where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1165,10 +1165,10 @@ where /// to the wasm VM VP environment. It will try to check the write log first and /// if no entry found then the storage. pub fn vp_has_key_post( - env: &VpEnv, + env: &VpVmEnv, key_ptr: u64, key_len: u64, -) -> vp_env::Result +) -> vp_env::EnvResult where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1197,10 +1197,10 @@ where /// It will try to get an iterator from the storage and return the corresponding /// ID of the iterator. pub fn vp_iter_prefix( - env: &VpEnv, + env: &VpVmEnv, prefix_ptr: u64, prefix_len: u64, -) -> vp_env::Result +) -> vp_env::EnvResult where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1231,9 +1231,9 @@ where /// Returns `-1` when the key is not present, or the length of the data when /// the key is present (the length may be `0`). pub fn vp_iter_pre_next( - env: &VpEnv, + env: &VpVmEnv, iter_id: u64, -) -> vp_env::Result +) -> vp_env::EnvResult where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1271,9 +1271,9 @@ where /// Returns `-1` when the key is not present, or the length of the data when /// the key is present (the length may be `0`). pub fn vp_iter_post_next( - env: &VpEnv, + env: &VpVmEnv, iter_id: u64, -) -> vp_env::Result +) -> vp_env::EnvResult where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1308,7 +1308,7 @@ where /// Verifier insertion function exposed to the wasm VM Tx environment. pub fn tx_insert_verifier( - env: &TxEnv, + env: &TxVmEnv, addr_ptr: u64, addr_len: u64, ) -> TxResult<()> @@ -1335,7 +1335,7 @@ where /// Update a validity predicate function exposed to the wasm VM Tx environment pub fn tx_update_validity_predicate( - env: &TxEnv, + env: &TxVmEnv, addr_ptr: u64, addr_len: u64, code_ptr: u64, @@ -1376,7 +1376,7 @@ where /// Initialize a new account established address. pub fn tx_init_account( - env: &TxEnv, + env: &TxVmEnv, code_ptr: u64, code_len: u64, result_ptr: u64, @@ -1419,7 +1419,7 @@ where /// Getting the chain ID function exposed to the wasm VM Tx environment. pub fn tx_get_chain_id( - env: &TxEnv, + env: &TxVmEnv, result_ptr: u64, ) -> TxResult<()> where @@ -1442,7 +1442,7 @@ where /// environment. The height is that of the block to which the current /// transaction is being applied. pub fn tx_get_block_height( - env: &TxEnv, + env: &TxVmEnv, ) -> TxResult where MEM: VmMemory, @@ -1459,7 +1459,7 @@ where /// Getting the block hash function exposed to the wasm VM Tx environment. The /// hash is that of the block to which the current transaction is being applied. pub fn tx_get_block_hash( - env: &TxEnv, + env: &TxVmEnv, result_ptr: u64, ) -> TxResult<()> where @@ -1482,7 +1482,7 @@ where /// environment. The epoch is that of the block to which the current /// transaction is being applied. pub fn tx_get_block_epoch( - env: &TxEnv, + env: &TxVmEnv, ) -> TxResult where MEM: VmMemory, @@ -1498,9 +1498,9 @@ where /// Getting the chain ID function exposed to the wasm VM VP environment. pub fn vp_get_chain_id( - env: &VpEnv, + env: &VpVmEnv, result_ptr: u64, -) -> vp_env::Result<()> +) -> vp_env::EnvResult<()> where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1522,8 +1522,8 @@ where /// environment. The height is that of the block to which the current /// transaction is being applied. pub fn vp_get_block_height( - env: &VpEnv, -) -> vp_env::Result + env: &VpVmEnv, +) -> vp_env::EnvResult where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1541,7 +1541,7 @@ where /// environment. The time is that of the block header to which the current /// transaction is being applied. pub fn tx_get_block_time( - env: &TxEnv, + env: &TxVmEnv, ) -> TxResult where MEM: VmMemory, @@ -1576,9 +1576,9 @@ where /// Getting the block hash function exposed to the wasm VM VP environment. The /// hash is that of the block to which the current transaction is being applied. pub fn vp_get_block_hash( - env: &VpEnv, + env: &VpVmEnv, result_ptr: u64, -) -> vp_env::Result<()> +) -> vp_env::EnvResult<()> where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1598,9 +1598,9 @@ where /// Getting the transaction hash function exposed to the wasm VM VP environment. pub fn vp_get_tx_code_hash( - env: &VpEnv, + env: &VpVmEnv, result_ptr: u64, -) -> vp_env::Result<()> +) -> vp_env::EnvResult<()> where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1622,8 +1622,8 @@ where /// environment. The epoch is that of the block to which the current /// transaction is being applied. pub fn vp_get_block_epoch( - env: &VpEnv, -) -> vp_env::Result + env: &VpVmEnv, +) -> vp_env::EnvResult where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1639,12 +1639,12 @@ where /// Verify a transaction signature. pub fn vp_verify_tx_signature( - env: &VpEnv, + env: &VpVmEnv, pk_ptr: u64, pk_len: u64, sig_ptr: u64, sig_len: u64, -) -> vp_env::Result +) -> vp_env::EnvResult where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1678,7 +1678,7 @@ where /// printed at the [`tracing::Level::INFO`]. This function is for development /// only. pub fn tx_log_string( - env: &TxEnv, + env: &TxVmEnv, str_ptr: u64, str_len: u64, ) -> TxResult<()> @@ -1698,12 +1698,12 @@ where /// Evaluate a validity predicate with the given input data. pub fn vp_eval( - env: &VpEnv<'static, MEM, DB, H, EVAL, CA>, + env: &VpVmEnv<'static, MEM, DB, H, EVAL, CA>, vp_code_ptr: u64, vp_code_len: u64, input_data_ptr: u64, input_data_len: u64, -) -> vp_env::Result +) -> vp_env::EnvResult where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1734,10 +1734,10 @@ where /// printed at the [`tracing::Level::INFO`]. This function is for development /// only. pub fn vp_log_string( - env: &VpEnv, + env: &VpVmEnv, str_ptr: u64, str_len: u64, -) -> vp_env::Result<()> +) -> vp_env::EnvResult<()> where MEM: VmMemory, DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1773,13 +1773,13 @@ pub mod testing { result_buffer: &mut Option>, #[cfg(feature = "wasm-runtime")] vp_wasm_cache: &mut VpCache, #[cfg(feature = "wasm-runtime")] tx_wasm_cache: &mut TxCache, - ) -> TxEnv<'static, NativeMemory, DB, H, CA> + ) -> TxVmEnv<'static, NativeMemory, DB, H, CA> where DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, CA: WasmCacheAccess, { - TxEnv::new( + TxVmEnv::new( NativeMemory::default(), storage, write_log, @@ -1808,14 +1808,14 @@ pub mod testing { keys_changed: &BTreeSet, eval_runner: &EVAL, #[cfg(feature = "wasm-runtime")] vp_wasm_cache: &mut VpCache, - ) -> VpEnv<'static, NativeMemory, DB, H, EVAL, CA> + ) -> VpVmEnv<'static, NativeMemory, DB, H, EVAL, CA> where DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, EVAL: VpEvaluator, CA: WasmCacheAccess, { - VpEnv::new( + VpVmEnv::new( NativeMemory::default(), address, storage, diff --git a/shared/src/vm/wasm/host_env.rs b/shared/src/vm/wasm/host_env.rs index 3b6715f383..1a50c5533d 100644 --- a/shared/src/vm/wasm/host_env.rs +++ b/shared/src/vm/wasm/host_env.rs @@ -9,11 +9,11 @@ use wasmer::{ }; use crate::ledger::storage::{self, StorageHasher}; -use crate::vm::host_env::{TxEnv, VpEnv, VpEvaluator}; +use crate::vm::host_env::{TxVmEnv, VpEvaluator, VpVmEnv}; use crate::vm::wasm::memory::WasmMemory; use crate::vm::{host_env, WasmCacheAccess}; -impl WasmerEnv for TxEnv<'_, WasmMemory, DB, H, CA> +impl WasmerEnv for TxVmEnv<'_, WasmMemory, DB, H, CA> where DB: storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, @@ -27,7 +27,7 @@ where } } -impl WasmerEnv for VpEnv<'_, WasmMemory, DB, H, EVAL, CA> +impl WasmerEnv for VpVmEnv<'_, WasmMemory, DB, H, EVAL, CA> where DB: storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, @@ -48,7 +48,7 @@ where pub fn tx_imports( wasm_store: &Store, initial_memory: Memory, - env: TxEnv<'static, WasmMemory, DB, H, CA>, + env: TxVmEnv<'static, WasmMemory, DB, H, CA>, ) -> ImportObject where DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -87,7 +87,7 @@ where pub fn vp_imports( wasm_store: &Store, initial_memory: Memory, - env: VpEnv<'static, WasmMemory, DB, H, EVAL, CA>, + env: VpVmEnv<'static, WasmMemory, DB, H, EVAL, CA>, ) -> ImportObject where DB: storage::DB + for<'iter> storage::DBIter<'iter>, diff --git a/shared/src/vm/wasm/run.rs b/shared/src/vm/wasm/run.rs index 75fbfb4add..d9977393d8 100644 --- a/shared/src/vm/wasm/run.rs +++ b/shared/src/vm/wasm/run.rs @@ -17,7 +17,7 @@ use crate::proto::Tx; use crate::types::address::Address; use crate::types::internal::HostEnvResult; use crate::types::storage::Key; -use crate::vm::host_env::{TxEnv, VpCtx, VpEnv, VpEvaluator}; +use crate::vm::host_env::{TxVmEnv, VpCtx, VpEvaluator, VpVmEnv}; use crate::vm::prefix_iter::PrefixIterators; use crate::vm::types::VpInput; use crate::vm::wasm::host_env::{tx_imports, vp_imports}; @@ -94,7 +94,7 @@ where let mut verifiers = BTreeSet::new(); let mut result_buffer: Option> = None; - let env = TxEnv::new( + let env = TxVmEnv::new( WasmMemory::default(), storage, write_log, @@ -189,7 +189,7 @@ where cache_access: PhantomData, }; - let env = VpEnv::new( + let env = VpVmEnv::new( WasmMemory::default(), address, storage, @@ -344,7 +344,7 @@ where let keys_changed = unsafe { ctx.keys_changed.get() }; let verifiers = unsafe { ctx.verifiers.get() }; let vp_wasm_cache = unsafe { ctx.vp_wasm_cache.get() }; - let env = VpEnv { + let env = VpVmEnv { memory: WasmMemory::default(), ctx, }; From fe365d9d19f5d1f69eac3456924fb31d21881d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 20 Jul 2022 20:57:12 +0200 Subject: [PATCH 03/81] shared/ledger/native_vp: implement VpEnv --- apps/src/lib/node/ledger/protocol/mod.rs | 3 + shared/src/ledger/native_vp.rs | 203 +++++++++++++---------- 2 files changed, 120 insertions(+), 86 deletions(-) diff --git a/apps/src/lib/node/ledger/protocol/mod.rs b/apps/src/lib/node/ledger/protocol/mod.rs index e5f191d6e7..cac7cacb42 100644 --- a/apps/src/lib/node/ledger/protocol/mod.rs +++ b/apps/src/lib/node/ledger/protocol/mod.rs @@ -249,10 +249,13 @@ where } Address::Internal(internal_addr) => { let ctx = native_vp::Ctx::new( + addr, storage, write_log, tx, gas_meter, + &keys_changed, + &verifiers, vp_wasm_cache.clone(), ); let tx_data = match tx.data.as_ref() { diff --git a/shared/src/ledger/native_vp.rs b/shared/src/ledger/native_vp.rs index faad84a0f4..3893e847ed 100644 --- a/shared/src/ledger/native_vp.rs +++ b/shared/src/ledger/native_vp.rs @@ -3,27 +3,20 @@ use std::cell::RefCell; use std::collections::BTreeSet; -use thiserror::Error; - +pub use super::vp_env::VpEnv; use crate::ledger::gas::VpGasMeter; use crate::ledger::storage::write_log::WriteLog; use crate::ledger::storage::{Storage, StorageHasher}; use crate::ledger::{storage, vp_env}; use crate::proto::Tx; use crate::types::address::{Address, InternalAddress}; +use crate::types::hash::Hash; use crate::types::storage::{BlockHash, BlockHeight, Epoch, Key}; use crate::vm::prefix_iter::PrefixIterators; use crate::vm::WasmCacheAccess; -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum Error { - #[error("Host context error: {0}")] - ContextError(vp_env::RuntimeError), -} - -/// Native VP function result -pub type Result = std::result::Result; +/// Possible error in a native VP host function call +pub type Error = vp_env::RuntimeError; /// A native VP module should implement its validation logic using this trait. pub trait NativeVp { @@ -54,6 +47,8 @@ where H: StorageHasher, CA: WasmCacheAccess, { + /// The address of the account that owns the VP + pub address: &'a Address, /// Storage prefix iterators. pub iterators: RefCell>, /// VP gas meter. @@ -64,6 +59,11 @@ where pub write_log: &'a WriteLog, /// The transaction code is used for signature verification pub tx: &'a Tx, + /// The storage keys that have been changed. Used for calls to `eval`. + pub keys_changed: &'a BTreeSet, + /// The verifiers whose validity predicates should be triggered. Used for + /// calls to `eval`. + pub verifiers: &'a BTreeSet
, /// VP WASM compilation cache #[cfg(feature = "wasm-runtime")] pub vp_wasm_cache: crate::vm::wasm::VpCache, @@ -79,20 +79,27 @@ where CA: 'static + WasmCacheAccess, { /// Initialize a new context for native VP call + #[allow(clippy::too_many_arguments)] pub fn new( + address: &'a Address, storage: &'a Storage, write_log: &'a WriteLog, tx: &'a Tx, gas_meter: VpGasMeter, + keys_changed: &'a BTreeSet, + verifiers: &'a BTreeSet
, #[cfg(feature = "wasm-runtime")] vp_wasm_cache: crate::vm::wasm::VpCache, ) -> Self { Self { + address, iterators: RefCell::new(PrefixIterators::default()), gas_meter: RefCell::new(gas_meter), storage, write_log, tx, + keys_changed, + verifiers, #[cfg(feature = "wasm-runtime")] vp_wasm_cache, #[cfg(not(feature = "wasm-runtime"))] @@ -101,153 +108,163 @@ where } /// Add a gas cost incured in a validity predicate - pub fn add_gas(&self, used_gas: u64) -> Result<()> { + pub fn add_gas(&self, used_gas: u64) -> Result<(), vp_env::RuntimeError> { vp_env::add_gas(&mut *self.gas_meter.borrow_mut(), used_gas) - .map_err(Error::ContextError) + } +} + +impl<'a, DB, H, CA> VpEnv for Ctx<'a, DB, H, CA> +where + DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, + H: 'static + StorageHasher, + CA: 'static + WasmCacheAccess, +{ + type Error = Error; + type PrefixIter = >::PrefixIter; + + fn read_pre( + &self, + key: &Key, + ) -> Result, Self::Error> { + vp_env::read_pre( + &mut *self.gas_meter.borrow_mut(), + self.storage, + self.write_log, + key, + ) + .map(|data| data.and_then(|t| T::try_from_slice(&t[..]).ok())) } - /// Storage read prior state (before tx execution). It will try to read from - /// the storage. - pub fn read_pre(&self, key: &Key) -> Result>> { + fn read_bytes_pre( + &self, + key: &Key, + ) -> Result>, Self::Error> { vp_env::read_pre( &mut *self.gas_meter.borrow_mut(), self.storage, self.write_log, key, ) - .map_err(Error::ContextError) } - /// Storage read posterior state (after tx execution). It will try to read - /// from the write log first and if no entry found then from the - /// storage. - pub fn read_post(&self, key: &Key) -> Result>> { + fn read_post( + &self, + key: &Key, + ) -> Result, Self::Error> { vp_env::read_post( &mut *self.gas_meter.borrow_mut(), self.storage, self.write_log, key, ) - .map_err(Error::ContextError) + .map(|data| data.and_then(|t| T::try_from_slice(&t[..]).ok())) } - /// Storage read temporary state (after tx execution). It will try to read - /// from only the write log. - pub fn read_temp(&self, key: &Key) -> Result>> { + fn read_bytes_post( + &self, + key: &Key, + ) -> Result>, Self::Error> { + vp_env::read_post( + &mut *self.gas_meter.borrow_mut(), + self.storage, + self.write_log, + key, + ) + } + + fn read_temp( + &self, + key: &Key, + ) -> Result, Self::Error> { vp_env::read_temp( &mut *self.gas_meter.borrow_mut(), self.write_log, key, ) - .map_err(Error::ContextError) + .map(|data| data.and_then(|t| T::try_from_slice(&t[..]).ok())) } - /// Storage `has_key` in prior state (before tx execution). It will try to - /// read from the storage. - pub fn has_key_pre(&self, key: &Key) -> Result { + fn read_bytes_temp( + &self, + key: &Key, + ) -> Result>, Self::Error> { + vp_env::read_temp( + &mut *self.gas_meter.borrow_mut(), + self.write_log, + key, + ) + } + + fn has_key_pre(&self, key: &Key) -> Result { vp_env::has_key_pre( &mut *self.gas_meter.borrow_mut(), self.storage, key, ) - .map_err(Error::ContextError) } - /// Storage `has_key` in posterior state (after tx execution). It will try - /// to check the write log first and if no entry found then the storage. - pub fn has_key_post(&self, key: &Key) -> Result { + fn has_key_post(&self, key: &Key) -> Result { vp_env::has_key_post( &mut *self.gas_meter.borrow_mut(), self.storage, self.write_log, key, ) - .map_err(Error::ContextError) } - /// Getting the chain ID. - pub fn get_chain_id(&self) -> Result { + fn get_chain_id(&self) -> Result { vp_env::get_chain_id(&mut *self.gas_meter.borrow_mut(), self.storage) - .map_err(Error::ContextError) } - /// Getting the block height. The height is that of the block to which the - /// current transaction is being applied. - pub fn get_block_height(&self) -> Result { + fn get_block_height(&self) -> Result { vp_env::get_block_height( &mut *self.gas_meter.borrow_mut(), self.storage, ) - .map_err(Error::ContextError) } - /// Getting the block hash. The height is that of the block to which the - /// current transaction is being applied. - pub fn get_block_hash(&self) -> Result { + fn get_block_hash(&self) -> Result { vp_env::get_block_hash(&mut *self.gas_meter.borrow_mut(), self.storage) - .map_err(Error::ContextError) } - /// Getting the block epoch. The epoch is that of the block to which the - /// current transaction is being applied. - pub fn get_block_epoch(&self) -> Result { + fn get_block_epoch(&self) -> Result { vp_env::get_block_epoch(&mut *self.gas_meter.borrow_mut(), self.storage) - .map_err(Error::ContextError) } - /// Storage prefix iterator. It will try to get an iterator from the - /// storage. - pub fn iter_prefix( + fn iter_prefix( &self, prefix: &Key, - ) -> Result<>::PrefixIter> { + ) -> Result { vp_env::iter_prefix( &mut *self.gas_meter.borrow_mut(), self.storage, prefix, ) - .map_err(Error::ContextError) } - /// Storage prefix iterator for prior state (before tx execution). It will - /// try to read from the storage. - pub fn iter_pre_next( + fn iter_pre_next( &self, - iter: &mut >::PrefixIter, - ) -> Result)>> { + iter: &mut Self::PrefixIter, + ) -> Result)>, Self::Error> { vp_env::iter_pre_next::(&mut *self.gas_meter.borrow_mut(), iter) - .map_err(Error::ContextError) } - /// Storage prefix iterator next for posterior state (after tx execution). - /// It will try to read from the write log first and if no entry found - /// then from the storage. - pub fn iter_post_next( + fn iter_post_next( &self, - iter: &mut >::PrefixIter, - ) -> Result)>> { + iter: &mut Self::PrefixIter, + ) -> Result)>, Self::Error> { vp_env::iter_post_next::( &mut *self.gas_meter.borrow_mut(), self.write_log, iter, ) - .map_err(Error::ContextError) } - /// Evaluate a validity predicate with given data. The address, changed - /// storage keys and verifiers will have the same values as the input to - /// caller's validity predicate. - /// - /// If the execution fails for whatever reason, this will return `false`. - /// Otherwise returns the result of evaluation. - pub fn eval( - &mut self, - address: &Address, - keys_changed: &BTreeSet, - verifiers: &BTreeSet
, + fn eval( + &self, vp_code: Vec, input_data: Vec, - ) -> bool { + ) -> Result { #[cfg(feature = "wasm-runtime")] { use std::marker::PhantomData; @@ -263,39 +280,53 @@ where let mut iterators: PrefixIterators<'_, DB> = PrefixIterators::default(); let mut result_buffer: Option> = None; + let mut vp_wasm_cache = self.vp_wasm_cache.clone(); let ctx = VpCtx::new( - address, + self.address, self.storage, self.write_log, &mut *self.gas_meter.borrow_mut(), self.tx, &mut iterators, - verifiers, + self.verifiers, &mut result_buffer, - keys_changed, + self.keys_changed, &eval_runner, - &mut self.vp_wasm_cache, + &mut vp_wasm_cache, ); match eval_runner.eval_native_result(ctx, vp_code, input_data) { - Ok(result) => result, + Ok(result) => Ok(result), Err(err) => { tracing::warn!( "VP eval from a native VP failed with: {}", err ); - false + Ok(false) } } } #[cfg(not(feature = "wasm-runtime"))] { - let _ = (address, keys_changed, verifiers, vp_code, input_data); + // This line is here to prevent unused var clippy warning + let _ = (vp_code, input_data); unimplemented!( "The \"wasm-runtime\" feature must be enabled to use the \ `eval` function." ) } } + + fn verify_tx_signature( + &self, + pk: &crate::types::key::common::PublicKey, + sig: &crate::types::key::common::Signature, + ) -> Result { + Ok(self.tx.verify_sig(pk, sig).is_ok()) + } + + fn get_tx_code_hash(&self) -> Result { + vp_env::get_tx_code_hash(&mut *self.gas_meter.borrow_mut(), self.tx) + } } From 4e46acaa3f0ac7879b135779cb82af204cbe4a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 20 Jul 2022 20:58:28 +0200 Subject: [PATCH 04/81] shared: update native_vp implementations for VpEnv methods --- proof_of_stake/src/lib.rs | 222 +++++++++++------- shared/src/ledger/governance/vp.rs | 9 +- shared/src/ledger/ibc/handler.rs | 309 ++++++++++++++++--------- shared/src/ledger/ibc/vp/channel.rs | 26 +-- shared/src/ledger/ibc/vp/client.rs | 11 +- shared/src/ledger/ibc/vp/connection.rs | 5 +- shared/src/ledger/ibc/vp/mod.rs | 246 ++++++++++++++++---- shared/src/ledger/ibc/vp/port.rs | 5 +- shared/src/ledger/ibc/vp/token.rs | 53 +++-- shared/src/ledger/pos/vp.rs | 146 ++++++------ shared/src/ledger/treasury/mod.rs | 71 ++---- 11 files changed, 693 insertions(+), 410 deletions(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index a137eb8a91..144c88c596 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -94,122 +94,164 @@ pub trait PosReadOnly { + BorshSerialize + BorshSchema; + /// Underlying read (and write in [`PosActions`]) interface errors + type Error; + /// Address of the PoS account const POS_ADDRESS: Self::Address; + /// Address of the staking token /// TODO: this should be `const`, but in the ledger `address::xan` is not a /// `const fn` fn staking_token_address() -> Self::Address; /// Read PoS parameters. - fn read_pos_params(&self) -> PosParams; + fn read_pos_params(&self) -> Result; /// Read PoS validator's staking reward address. fn read_validator_staking_reward_address( &self, key: &Self::Address, - ) -> Option; + ) -> Result, Self::Error>; /// Read PoS validator's consensus key (used for signing block votes). fn read_validator_consensus_key( &self, key: &Self::Address, - ) -> Option>; + ) -> Result>, Self::Error>; /// Read PoS validator's state. fn read_validator_state( &self, key: &Self::Address, - ) -> Option; + ) -> Result, Self::Error>; /// Read PoS validator's total deltas of their bonds (validator self-bonds /// and delegations). fn read_validator_total_deltas( &self, key: &Self::Address, - ) -> Option>; + ) -> Result>, Self::Error>; /// Read PoS validator's voting power. fn read_validator_voting_power( &self, key: &Self::Address, - ) -> Option; + ) -> Result, Self::Error>; /// Read PoS slashes applied to a validator. - fn read_validator_slashes(&self, key: &Self::Address) -> Vec; + fn read_validator_slashes( + &self, + key: &Self::Address, + ) -> Result, Self::Error>; /// Read PoS bond (validator self-bond or a delegation). fn read_bond( &self, key: &BondId, - ) -> Option>; + ) -> Result>, Self::Error>; /// Read PoS unbond (unbonded tokens from validator self-bond or a /// delegation). fn read_unbond( &self, key: &BondId, - ) -> Option>; + ) -> Result>, Self::Error>; /// Read PoS validator set (active and inactive). - fn read_validator_set(&self) -> ValidatorSets; + fn read_validator_set( + &self, + ) -> Result, Self::Error>; /// Read PoS total voting power of all validators (active and inactive). - fn read_total_voting_power(&self) -> TotalVotingPowers; + fn read_total_voting_power(&self) + -> Result; } /// PoS system trait to be implemented in integration that can read and write /// PoS data. pub trait PosActions: PosReadOnly { + /// Error in `PosActions::become_validator` + type BecomeValidatorError: From + + From>; + + /// Error in `PosActions::bond_tokens` + type BondError: From + From>; + + /// Error in `PosActions::unbond_tokens` + type UnbondError: From + + From>; + + /// Error in `PosActions::withdraw_tokens` + type WithdrawError: From + From>; + /// Write PoS parameters. - fn write_pos_params(&mut self, params: &PosParams); + fn write_pos_params( + &mut self, + params: &PosParams, + ) -> Result<(), Self::Error>; /// Write PoS validator's raw hash its address. - fn write_validator_address_raw_hash(&mut self, address: &Self::Address); + fn write_validator_address_raw_hash( + &mut self, + address: &Self::Address, + ) -> Result<(), Self::Error>; /// Write PoS validator's staking reward address, into which staking rewards /// will be credited. fn write_validator_staking_reward_address( &mut self, key: &Self::Address, value: Self::Address, - ); + ) -> Result<(), Self::Error>; /// Write PoS validator's consensus key (used for signing block votes). fn write_validator_consensus_key( &mut self, key: &Self::Address, value: ValidatorConsensusKeys, - ); + ) -> Result<(), Self::Error>; /// Write PoS validator's state. fn write_validator_state( &mut self, key: &Self::Address, value: ValidatorStates, - ); + ) -> Result<(), Self::Error>; /// Write PoS validator's total deltas of their bonds (validator self-bonds /// and delegations). fn write_validator_total_deltas( &mut self, key: &Self::Address, value: ValidatorTotalDeltas, - ); + ) -> Result<(), Self::Error>; /// Write PoS validator's voting power. fn write_validator_voting_power( &mut self, key: &Self::Address, value: ValidatorVotingPowers, - ); + ) -> Result<(), Self::Error>; /// Write PoS bond (validator self-bond or a delegation). fn write_bond( &mut self, key: &BondId, value: Bonds, - ); + ) -> Result<(), Self::Error>; /// Write PoS unbond (unbonded tokens from validator self-bond or a /// delegation). fn write_unbond( &mut self, key: &BondId, value: Unbonds, - ); + ) -> Result<(), Self::Error>; /// Write PoS validator set (active and inactive). - fn write_validator_set(&mut self, value: ValidatorSets); + fn write_validator_set( + &mut self, + value: ValidatorSets, + ) -> Result<(), Self::Error>; /// Write PoS total voting power of all validators (active and inactive). - fn write_total_voting_power(&mut self, value: TotalVotingPowers); + fn write_total_voting_power( + &mut self, + value: TotalVotingPowers, + ) -> Result<(), Self::Error>; /// Delete an emptied PoS bond (validator self-bond or a delegation). - fn delete_bond(&mut self, key: &BondId); + fn delete_bond( + &mut self, + key: &BondId, + ) -> Result<(), Self::Error>; /// Delete an emptied PoS unbond (unbonded tokens from validator self-bond /// or a delegation). - fn delete_unbond(&mut self, key: &BondId); + fn delete_unbond( + &mut self, + key: &BondId, + ) -> Result<(), Self::Error>; /// Transfer tokens from the `src` to the `dest`. fn transfer( @@ -218,7 +260,7 @@ pub trait PosActions: PosReadOnly { amount: Self::TokenAmount, src: &Self::Address, dest: &Self::Address, - ); + ) -> Result<(), Self::Error>; /// Attempt to update the given account to become a validator. fn become_validator( @@ -227,21 +269,19 @@ pub trait PosActions: PosReadOnly { staking_reward_address: &Self::Address, consensus_key: &Self::PublicKey, current_epoch: impl Into, - ) -> Result<(), BecomeValidatorError> { + ) -> Result<(), Self::BecomeValidatorError> { let current_epoch = current_epoch.into(); - let params = self.read_pos_params(); - let mut validator_set = self.read_validator_set(); - if self.is_validator(address) { - return Err(BecomeValidatorError::AlreadyValidator( - address.clone(), - )); + let params = self.read_pos_params()?; + let mut validator_set = self.read_validator_set()?; + if self.is_validator(address)? { + Err(BecomeValidatorError::AlreadyValidator(address.clone()))?; } if address == staking_reward_address { - return Err( + Err( BecomeValidatorError::StakingRewardAddressEqValidatorAddress( address.clone(), ), - ); + )?; } let BecomeValidatorData { consensus_key, @@ -258,20 +298,24 @@ pub trait PosActions: PosReadOnly { self.write_validator_staking_reward_address( address, staking_reward_address.clone(), - ); - self.write_validator_consensus_key(address, consensus_key); - self.write_validator_state(address, state); - self.write_validator_set(validator_set); - self.write_validator_address_raw_hash(address); - self.write_validator_total_deltas(address, total_deltas); - self.write_validator_voting_power(address, voting_power); + )?; + self.write_validator_consensus_key(address, consensus_key)?; + self.write_validator_state(address, state)?; + self.write_validator_set(validator_set)?; + self.write_validator_address_raw_hash(address)?; + self.write_validator_total_deltas(address, total_deltas)?; + self.write_validator_voting_power(address, voting_power)?; Ok(()) } /// Check if the given address is a validator by checking that it has some /// state. - fn is_validator(&self, address: &Self::Address) -> bool { - self.read_validator_state(address).is_some() + fn is_validator( + &self, + address: &Self::Address, + ) -> Result { + let state = self.read_validator_state(address)?; + Ok(state.is_some()) } /// Self-bond tokens to a validator when `source` is `None` or equal to @@ -283,29 +327,27 @@ pub trait PosActions: PosReadOnly { validator: &Self::Address, amount: Self::TokenAmount, current_epoch: impl Into, - ) -> Result<(), BondError> { + ) -> Result<(), Self::BondError> { let current_epoch = current_epoch.into(); if let Some(source) = source { - if source != validator && self.is_validator(source) { - return Err(BondError::SourceMustNotBeAValidator( - source.clone(), - )); + if source != validator && self.is_validator(source)? { + Err(BondError::SourceMustNotBeAValidator(source.clone()))?; } } - let params = self.read_pos_params(); - let validator_state = self.read_validator_state(validator); + let params = self.read_pos_params()?; + let validator_state = self.read_validator_state(validator)?; let source = source.unwrap_or(validator); let bond_id = BondId { source: source.clone(), validator: validator.clone(), }; - let bond = self.read_bond(&bond_id); + let bond = self.read_bond(&bond_id)?; let validator_total_deltas = - self.read_validator_total_deltas(validator); + self.read_validator_total_deltas(validator)?; let validator_voting_power = - self.read_validator_voting_power(validator); - let mut total_voting_power = self.read_total_voting_power(); - let mut validator_set = self.read_validator_set(); + self.read_validator_voting_power(validator)?; + let mut total_voting_power = self.read_total_voting_power()?; + let mut validator_set = self.read_validator_set()?; let BondData { bond, @@ -323,12 +365,11 @@ pub trait PosActions: PosReadOnly { &mut validator_set, current_epoch, )?; - - self.write_bond(&bond_id, bond); - self.write_validator_total_deltas(validator, validator_total_deltas); - self.write_validator_voting_power(validator, validator_voting_power); - self.write_total_voting_power(total_voting_power); - self.write_validator_set(validator_set); + self.write_bond(&bond_id, bond)?; + self.write_validator_total_deltas(validator, validator_total_deltas)?; + self.write_validator_voting_power(validator, validator_voting_power)?; + self.write_total_voting_power(total_voting_power)?; + self.write_validator_set(validator_set)?; // Transfer the bonded tokens from the source to PoS self.transfer( @@ -336,8 +377,7 @@ pub trait PosActions: PosReadOnly { amount, source, &Self::POS_ADDRESS, - ); - + )?; Ok(()) } @@ -350,28 +390,32 @@ pub trait PosActions: PosReadOnly { validator: &Self::Address, amount: Self::TokenAmount, current_epoch: impl Into, - ) -> Result<(), UnbondError> { + ) -> Result<(), Self::UnbondError> { let current_epoch = current_epoch.into(); - let params = self.read_pos_params(); + let params = self.read_pos_params()?; let source = source.unwrap_or(validator); let bond_id = BondId { source: source.clone(), validator: validator.clone(), }; - let mut bond = - self.read_bond(&bond_id).ok_or(UnbondError::NoBondFound)?; - let unbond = self.read_unbond(&bond_id); - let mut validator_total_deltas = - self.read_validator_total_deltas(validator).ok_or_else(|| { + let mut bond = match self.read_bond(&bond_id)? { + Some(val) => val, + None => Err(UnbondError::NoBondFound)?, + }; + let unbond = self.read_unbond(&bond_id)?; + let mut validator_total_deltas = self + .read_validator_total_deltas(validator)? + .ok_or_else(|| { UnbondError::ValidatorHasNoBonds(validator.clone()) })?; - let mut validator_voting_power = - self.read_validator_voting_power(validator).ok_or_else(|| { + let mut validator_voting_power = self + .read_validator_voting_power(validator)? + .ok_or_else(|| { UnbondError::ValidatorHasNoVotingPower(validator.clone()) })?; - let slashes = self.read_validator_slashes(validator); - let mut total_voting_power = self.read_total_voting_power(); - let mut validator_set = self.read_validator_set(); + let slashes = self.read_validator_slashes(validator)?; + let mut total_voting_power = self.read_total_voting_power()?; + let mut validator_set = self.read_validator_set()?; let UnbondData { unbond } = unbond_tokens( ¶ms, @@ -394,18 +438,18 @@ pub trait PosActions: PosReadOnly { ); match total_bonds { Some(total_bonds) if total_bonds.sum() != 0.into() => { - self.write_bond(&bond_id, bond); + self.write_bond(&bond_id, bond)?; } _ => { // If the bond is left empty, delete it - self.delete_bond(&bond_id) + self.delete_bond(&bond_id)? } } - self.write_unbond(&bond_id, unbond); - self.write_validator_total_deltas(validator, validator_total_deltas); - self.write_validator_voting_power(validator, validator_voting_power); - self.write_total_voting_power(total_voting_power); - self.write_validator_set(validator_set); + self.write_unbond(&bond_id, unbond)?; + self.write_validator_total_deltas(validator, validator_total_deltas)?; + self.write_validator_voting_power(validator, validator_voting_power)?; + self.write_total_voting_power(total_voting_power)?; + self.write_validator_set(validator_set)?; Ok(()) } @@ -418,17 +462,17 @@ pub trait PosActions: PosReadOnly { source: Option<&Self::Address>, validator: &Self::Address, current_epoch: impl Into, - ) -> Result> { + ) -> Result { let current_epoch = current_epoch.into(); - let params = self.read_pos_params(); + let params = self.read_pos_params()?; let source = source.unwrap_or(validator); let bond_id = BondId { source: source.clone(), validator: validator.clone(), }; - let unbond = self.read_unbond(&bond_id); - let slashes = self.read_validator_slashes(&bond_id.validator); + let unbond = self.read_unbond(&bond_id)?; + let slashes = self.read_validator_slashes(&bond_id.validator)?; let WithdrawData { unbond, @@ -449,11 +493,11 @@ pub trait PosActions: PosReadOnly { ); match total_unbonds { Some(total_unbonds) if total_unbonds.sum() != 0.into() => { - self.write_unbond(&bond_id, unbond); + self.write_unbond(&bond_id, unbond)?; } _ => { // If the unbond is left empty, delete it - self.delete_unbond(&bond_id) + self.delete_unbond(&bond_id)? } } @@ -463,7 +507,7 @@ pub trait PosActions: PosReadOnly { withdrawn, &Self::POS_ADDRESS, source, - ); + )?; Ok(slashed) } diff --git a/shared/src/ledger/governance/vp.rs b/shared/src/ledger/governance/vp.rs index 6ccfc7a33e..ad941806c4 100644 --- a/shared/src/ledger/governance/vp.rs +++ b/shared/src/ledger/governance/vp.rs @@ -7,6 +7,7 @@ use super::storage as gov_storage; use crate::ledger::native_vp::{self, Ctx}; use crate::ledger::pos::{self as pos_storage, BondId, Bonds}; use crate::ledger::storage::{self as ledger_storage, StorageHasher}; +use crate::ledger::vp_env::VpEnv; use crate::types::address::{xan as m1t, Address, InternalAddress}; use crate::types::storage::{Epoch, Key}; use crate::types::token; @@ -350,7 +351,7 @@ where let max_content_length = read(ctx, &max_content_length_parameter_key, ReadType::PRE).ok(); let has_pre_content = ctx.has_key_pre(&content_key).ok(); - let post_content = ctx.read_post(&content_key).unwrap(); + let post_content = ctx.read_bytes_post(&content_key).unwrap(); match (has_pre_content, post_content, max_content_length) { ( Some(has_pre_content), @@ -377,7 +378,7 @@ where let max_content_length = read(ctx, &max_content_length_parameter_key, ReadType::PRE).ok(); let has_pre_content = ctx.has_key_pre(&content_key).ok(); - let post_content = ctx.read_post(&content_key).unwrap(); + let post_content = ctx.read_bytes_post(&content_key).unwrap(); match (has_pre_content, post_content, max_content_length) { ( Some(has_pre_content), @@ -504,8 +505,8 @@ where T: Clone + BorshDeserialize, { let storage_result = match read_type { - ReadType::PRE => context.read_pre(key), - ReadType::POST => context.read_post(key), + ReadType::PRE => context.read_bytes_pre(key), + ReadType::POST => context.read_bytes_post(key), }; match storage_result { diff --git a/shared/src/ledger/ibc/handler.rs b/shared/src/ledger/ibc/handler.rs index bf45759535..5cbee20756 100644 --- a/shared/src/ledger/ibc/handler.rs +++ b/shared/src/ledger/ibc/handler.rs @@ -122,35 +122,56 @@ pub type Result = std::result::Result; /// IBC trait to be implemented in integration that can read and write pub trait IbcActions { + /// IBC action error + type Error: From; + /// Read IBC-related data - fn read_ibc_data(&self, key: &Key) -> Option>; + fn read_ibc_data( + &self, + key: &Key, + ) -> std::result::Result>, Self::Error>; /// Write IBC-related data - fn write_ibc_data(&self, key: &Key, data: impl AsRef<[u8]>); + fn write_ibc_data( + &mut self, + key: &Key, + data: impl AsRef<[u8]>, + ) -> std::result::Result<(), Self::Error>; /// Delete IBC-related data - fn delete_ibc_data(&self, key: &Key); + fn delete_ibc_data( + &mut self, + key: &Key, + ) -> std::result::Result<(), Self::Error>; /// Emit an IBC event - fn emit_ibc_event(&self, event: AnomaIbcEvent); + fn emit_ibc_event( + &mut self, + event: AnomaIbcEvent, + ) -> std::result::Result<(), Self::Error>; /// Transfer token fn transfer_token( - &self, + &mut self, src: &Address, dest: &Address, token: &Address, amount: Amount, - ); + ) -> std::result::Result<(), Self::Error>; /// Get the current height of this chain - fn get_height(&self) -> BlockHeight; + fn get_height(&self) -> std::result::Result; /// Get the current time of the tendermint header of this chain - fn get_header_time(&self) -> Rfc3339String; + fn get_header_time( + &self, + ) -> std::result::Result; /// dispatch according to ICS26 routing - fn dispatch(&self, tx_data: &[u8]) -> Result<()> { + fn dispatch_ibc_action( + &mut self, + tx_data: &[u8], + ) -> std::result::Result<(), Self::Error> { let ibc_msg = IbcMessage::decode(tx_data).map_err(Error::IbcData)?; match &ibc_msg.0 { Ics26Envelope::Ics2Msg(ics02_msg) => match ics02_msg { @@ -200,14 +221,17 @@ pub trait IbcActions { } /// Create a new client - fn create_client(&self, msg: &MsgCreateAnyClient) -> Result<()> { + fn create_client( + &mut self, + msg: &MsgCreateAnyClient, + ) -> std::result::Result<(), Self::Error> { let counter_key = storage::client_counter_key(); let counter = self.get_and_inc_counter(&counter_key)?; let client_type = msg.client_state.client_type(); let client_id = client_id(client_type, counter)?; // client type let client_type_key = storage::client_type_key(&client_id); - self.write_ibc_data(&client_type_key, client_type.as_str().as_bytes()); + self.write_ibc_data(&client_type_key, client_type.as_str().as_bytes())?; // client state let client_state_key = storage::client_state_key(&client_id); self.write_ibc_data( @@ -215,7 +239,7 @@ pub trait IbcActions { msg.client_state .encode_vec() .expect("encoding shouldn't fail"), - ); + )?; // consensus state let height = msg.client_state.latest_height(); let consensus_state_key = @@ -225,29 +249,33 @@ pub trait IbcActions { msg.consensus_state .encode_vec() .expect("encoding shouldn't fail"), - ); + )?; self.set_client_update_time(&client_id)?; let event = make_create_client_event(&client_id, msg) .try_into() .unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Update a client - fn update_client(&self, msg: &MsgUpdateAnyClient) -> Result<()> { + fn update_client( + &mut self, + msg: &MsgUpdateAnyClient, + ) -> std::result::Result<(), Self::Error> { // get and update the client let client_id = msg.client_id.clone(); let client_state_key = storage::client_state_key(&client_id); - let value = self.read_ibc_data(&client_state_key).ok_or_else(|| { - Error::Client(format!( - "The client to be updated doesn't exist: ID {}", - client_id - )) - })?; + let value = + self.read_ibc_data(&client_state_key)?.ok_or_else(|| { + Error::Client(format!( + "The client to be updated doesn't exist: ID {}", + client_id + )) + })?; let client_state = AnyClientState::decode_vec(&value).map_err(Error::Decoding)?; let (new_client_state, new_consensus_state) = @@ -259,7 +287,7 @@ pub trait IbcActions { new_client_state .encode_vec() .expect("encoding shouldn't fail"), - ); + )?; let consensus_state_key = storage::consensus_state_key(&client_id, height); self.write_ibc_data( @@ -267,20 +295,23 @@ pub trait IbcActions { new_consensus_state .encode_vec() .expect("encoding shouldn't fail"), - ); + )?; self.set_client_update_time(&client_id)?; let event = make_update_client_event(&client_id, msg) .try_into() .unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Upgrade a client - fn upgrade_client(&self, msg: &MsgUpgradeAnyClient) -> Result<()> { + fn upgrade_client( + &mut self, + msg: &MsgUpgradeAnyClient, + ) -> std::result::Result<(), Self::Error> { let client_state_key = storage::client_state_key(&msg.client_id); let height = msg.client_state.latest_height(); let consensus_state_key = @@ -290,26 +321,29 @@ pub trait IbcActions { msg.client_state .encode_vec() .expect("encoding shouldn't fail"), - ); + )?; self.write_ibc_data( &consensus_state_key, msg.consensus_state .encode_vec() .expect("encoding shouldn't fail"), - ); + )?; self.set_client_update_time(&msg.client_id)?; let event = make_upgrade_client_event(&msg.client_id, msg) .try_into() .unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Initialize a connection for ConnectionOpenInit - fn init_connection(&self, msg: &MsgConnectionOpenInit) -> Result<()> { + fn init_connection( + &mut self, + msg: &MsgConnectionOpenInit, + ) -> std::result::Result<(), Self::Error> { let counter_key = storage::connection_counter_key(); let counter = self.get_and_inc_counter(&counter_key)?; // new connection @@ -319,18 +353,21 @@ pub trait IbcActions { self.write_ibc_data( &conn_key, connection.encode_vec().expect("encoding shouldn't fail"), - ); + )?; let event = make_open_init_connection_event(&conn_id, msg) .try_into() .unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Initialize a connection for ConnectionOpenTry - fn try_connection(&self, msg: &MsgConnectionOpenTry) -> Result<()> { + fn try_connection( + &mut self, + msg: &MsgConnectionOpenTry, + ) -> std::result::Result<(), Self::Error> { let counter_key = storage::connection_counter_key(); let counter = self.get_and_inc_counter(&counter_key)?; // new connection @@ -340,20 +377,23 @@ pub trait IbcActions { self.write_ibc_data( &conn_key, connection.encode_vec().expect("encoding shouldn't fail"), - ); + )?; let event = make_open_try_connection_event(&conn_id, msg) .try_into() .unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Open the connection for ConnectionOpenAck - fn ack_connection(&self, msg: &MsgConnectionOpenAck) -> Result<()> { + fn ack_connection( + &mut self, + msg: &MsgConnectionOpenAck, + ) -> std::result::Result<(), Self::Error> { let conn_key = storage::connection_key(&msg.connection_id); - let value = self.read_ibc_data(&conn_key).ok_or_else(|| { + let value = self.read_ibc_data(&conn_key)?.ok_or_else(|| { Error::Connection(format!( "The connection to be opened doesn't exist: ID {}", msg.connection_id @@ -369,18 +409,21 @@ pub trait IbcActions { self.write_ibc_data( &conn_key, connection.encode_vec().expect("encoding shouldn't fail"), - ); + )?; let event = make_open_ack_connection_event(msg).try_into().unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Open the connection for ConnectionOpenConfirm - fn confirm_connection(&self, msg: &MsgConnectionOpenConfirm) -> Result<()> { + fn confirm_connection( + &mut self, + msg: &MsgConnectionOpenConfirm, + ) -> std::result::Result<(), Self::Error> { let conn_key = storage::connection_key(&msg.connection_id); - let value = self.read_ibc_data(&conn_key).ok_or_else(|| { + let value = self.read_ibc_data(&conn_key)?.ok_or_else(|| { Error::Connection(format!( "The connection to be opend doesn't exist: ID {}", msg.connection_id @@ -392,16 +435,19 @@ pub trait IbcActions { self.write_ibc_data( &conn_key, connection.encode_vec().expect("encoding shouldn't fail"), - ); + )?; let event = make_open_confirm_connection_event(msg).try_into().unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Initialize a channel for ChannelOpenInit - fn init_channel(&self, msg: &MsgChannelOpenInit) -> Result<()> { + fn init_channel( + &mut self, + msg: &MsgChannelOpenInit, + ) -> std::result::Result<(), Self::Error> { self.bind_port(&msg.port_id)?; let counter_key = storage::channel_counter_key(); let counter = self.get_and_inc_counter(&counter_key)?; @@ -412,18 +458,21 @@ pub trait IbcActions { self.write_ibc_data( &channel_key, msg.channel.encode_vec().expect("encoding shouldn't fail"), - ); + )?; let event = make_open_init_channel_event(&channel_id, msg) .try_into() .unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Initialize a channel for ChannelOpenTry - fn try_channel(&self, msg: &MsgChannelOpenTry) -> Result<()> { + fn try_channel( + &mut self, + msg: &MsgChannelOpenTry, + ) -> std::result::Result<(), Self::Error> { self.bind_port(&msg.port_id)?; let counter_key = storage::channel_counter_key(); let counter = self.get_and_inc_counter(&counter_key)?; @@ -434,22 +483,25 @@ pub trait IbcActions { self.write_ibc_data( &channel_key, msg.channel.encode_vec().expect("encoding shouldn't fail"), - ); + )?; let event = make_open_try_channel_event(&channel_id, msg) .try_into() .unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Open the channel for ChannelOpenAck - fn ack_channel(&self, msg: &MsgChannelOpenAck) -> Result<()> { + fn ack_channel( + &mut self, + msg: &MsgChannelOpenAck, + ) -> std::result::Result<(), Self::Error> { let port_channel_id = port_channel_id(msg.port_id.clone(), msg.channel_id.clone()); let channel_key = storage::channel_key(&port_channel_id); - let value = self.read_ibc_data(&channel_key).ok_or_else(|| { + let value = self.read_ibc_data(&channel_key)?.ok_or_else(|| { Error::Channel(format!( "The channel to be opened doesn't exist: Port/Channel {}", port_channel_id @@ -463,20 +515,23 @@ pub trait IbcActions { self.write_ibc_data( &channel_key, channel.encode_vec().expect("encoding shouldn't fail"), - ); + )?; let event = make_open_ack_channel_event(msg).try_into().unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Open the channel for ChannelOpenConfirm - fn confirm_channel(&self, msg: &MsgChannelOpenConfirm) -> Result<()> { + fn confirm_channel( + &mut self, + msg: &MsgChannelOpenConfirm, + ) -> std::result::Result<(), Self::Error> { let port_channel_id = port_channel_id(msg.port_id.clone(), msg.channel_id.clone()); let channel_key = storage::channel_key(&port_channel_id); - let value = self.read_ibc_data(&channel_key).ok_or_else(|| { + let value = self.read_ibc_data(&channel_key)?.ok_or_else(|| { Error::Channel(format!( "The channel to be opened doesn't exist: Port/Channel {}", port_channel_id @@ -488,20 +543,23 @@ pub trait IbcActions { self.write_ibc_data( &channel_key, channel.encode_vec().expect("encoding shouldn't fail"), - ); + )?; let event = make_open_confirm_channel_event(msg).try_into().unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Close the channel for ChannelCloseInit - fn close_init_channel(&self, msg: &MsgChannelCloseInit) -> Result<()> { + fn close_init_channel( + &mut self, + msg: &MsgChannelCloseInit, + ) -> std::result::Result<(), Self::Error> { let port_channel_id = port_channel_id(msg.port_id.clone(), msg.channel_id.clone()); let channel_key = storage::channel_key(&port_channel_id); - let value = self.read_ibc_data(&channel_key).ok_or_else(|| { + let value = self.read_ibc_data(&channel_key)?.ok_or_else(|| { Error::Channel(format!( "The channel to be closed doesn't exist: Port/Channel {}", port_channel_id @@ -513,23 +571,23 @@ pub trait IbcActions { self.write_ibc_data( &channel_key, channel.encode_vec().expect("encoding shouldn't fail"), - ); + )?; let event = make_close_init_channel_event(msg).try_into().unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Close the channel for ChannelCloseConfirm fn close_confirm_channel( - &self, + &mut self, msg: &MsgChannelCloseConfirm, - ) -> Result<()> { + ) -> std::result::Result<(), Self::Error> { let port_channel_id = port_channel_id(msg.port_id.clone(), msg.channel_id.clone()); let channel_key = storage::channel_key(&port_channel_id); - let value = self.read_ibc_data(&channel_key).ok_or_else(|| { + let value = self.read_ibc_data(&channel_key)?.ok_or_else(|| { Error::Channel(format!( "The channel to be closed doesn't exist: Port/Channel {}", port_channel_id @@ -541,22 +599,22 @@ pub trait IbcActions { self.write_ibc_data( &channel_key, channel.encode_vec().expect("encoding shouldn't fail"), - ); + )?; let event = make_close_confirm_channel_event(msg).try_into().unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Send a packet fn send_packet( - &self, + &mut self, port_channel_id: PortChannelId, data: Vec, timeout_height: Height, timeout_timestamp: Timestamp, - ) -> Result<()> { + ) -> std::result::Result<(), Self::Error> { // get and increment the next sequence send let seq_key = storage::next_sequence_send_key(&port_channel_id); let sequence = self.get_and_inc_sequence(&seq_key)?; @@ -564,7 +622,7 @@ pub trait IbcActions { // get the channel for the destination info. let channel_key = storage::channel_key(&port_channel_id); let channel = self - .read_ibc_data(&channel_key) + .read_ibc_data(&channel_key)? .expect("cannot get the channel to be closed"); let channel = ChannelEnd::decode_vec(&channel).expect("cannot get the channel"); @@ -595,16 +653,19 @@ pub trait IbcActions { commitment .encode(&mut commitment_bytes) .expect("encoding shouldn't fail"); - self.write_ibc_data(&commitment_key, commitment_bytes); + self.write_ibc_data(&commitment_key, commitment_bytes)?; let event = make_send_packet_event(packet).try_into().unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Receive a packet - fn receive_packet(&self, msg: &MsgRecvPacket) -> Result<()> { + fn receive_packet( + &mut self, + msg: &MsgRecvPacket, + ) -> std::result::Result<(), Self::Error> { // check the packet data if let Ok(data) = serde_json::from_slice(&msg.packet.data) { self.receive_token(&msg.packet, &data)?; @@ -616,7 +677,7 @@ pub trait IbcActions { &msg.packet.destination_channel, msg.packet.sequence, ); - self.write_ibc_data(&receipt_key, PacketReceipt::default().as_bytes()); + self.write_ibc_data(&receipt_key, PacketReceipt::default().as_bytes())?; // store the ack let ack_key = storage::ack_key( @@ -625,7 +686,7 @@ pub trait IbcActions { msg.packet.sequence, ); let ack = PacketAck::default().encode_to_vec(); - self.write_ibc_data(&ack_key, ack.clone()); + self.write_ibc_data(&ack_key, ack.clone())?; // increment the next sequence receive let port_channel_id = port_channel_id( @@ -638,28 +699,34 @@ pub trait IbcActions { let event = make_write_ack_event(msg.packet.clone(), ack) .try_into() .unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Receive a acknowledgement - fn acknowledge_packet(&self, msg: &MsgAcknowledgement) -> Result<()> { + fn acknowledge_packet( + &mut self, + msg: &MsgAcknowledgement, + ) -> std::result::Result<(), Self::Error> { let commitment_key = storage::commitment_key( &msg.packet.source_port, &msg.packet.source_channel, msg.packet.sequence, ); - self.delete_ibc_data(&commitment_key); + self.delete_ibc_data(&commitment_key)?; let event = make_ack_event(msg.packet.clone()).try_into().unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Receive a timeout - fn timeout_packet(&self, msg: &MsgTimeout) -> Result<()> { + fn timeout_packet( + &mut self, + msg: &MsgTimeout, + ) -> std::result::Result<(), Self::Error> { // check the packet data if let Ok(data) = serde_json::from_slice(&msg.packet.data) { self.refund_token(&msg.packet, &data)?; @@ -671,7 +738,7 @@ pub trait IbcActions { &msg.packet.source_channel, msg.packet.sequence, ); - self.delete_ibc_data(&commitment_key); + self.delete_ibc_data(&commitment_key)?; // close the channel let port_channel_id = port_channel_id( @@ -679,7 +746,7 @@ pub trait IbcActions { msg.packet.source_channel.clone(), ); let channel_key = storage::channel_key(&port_channel_id); - let value = self.read_ibc_data(&channel_key).ok_or_else(|| { + let value = self.read_ibc_data(&channel_key)?.ok_or_else(|| { Error::Channel(format!( "The channel to be closed doesn't exist: Port/Channel {}", port_channel_id @@ -692,17 +759,20 @@ pub trait IbcActions { self.write_ibc_data( &channel_key, channel.encode_vec().expect("encoding shouldn't fail"), - ); + )?; } let event = make_timeout_event(msg.packet.clone()).try_into().unwrap(); - self.emit_ibc_event(event); + self.emit_ibc_event(event)?; Ok(()) } /// Receive a timeout for TimeoutOnClose - fn timeout_on_close_packet(&self, msg: &MsgTimeoutOnClose) -> Result<()> { + fn timeout_on_close_packet( + &mut self, + msg: &MsgTimeoutOnClose, + ) -> std::result::Result<(), Self::Error> { // check the packet data if let Ok(data) = serde_json::from_slice(&msg.packet.data) { self.refund_token(&msg.packet, &data)?; @@ -714,7 +784,7 @@ pub trait IbcActions { &msg.packet.source_channel, msg.packet.sequence, ); - self.delete_ibc_data(&commitment_key); + self.delete_ibc_data(&commitment_key)?; // close the channel let port_channel_id = port_channel_id( @@ -722,7 +792,7 @@ pub trait IbcActions { msg.packet.source_channel.clone(), ); let channel_key = storage::channel_key(&port_channel_id); - let value = self.read_ibc_data(&channel_key).ok_or_else(|| { + let value = self.read_ibc_data(&channel_key)?.ok_or_else(|| { Error::Channel(format!( "The channel to be closed doesn't exist: Port/Channel {}", port_channel_id @@ -735,15 +805,18 @@ pub trait IbcActions { self.write_ibc_data( &channel_key, channel.encode_vec().expect("encoding shouldn't fail"), - ); + )?; } Ok(()) } /// Set the timestamp and the height for the client update - fn set_client_update_time(&self, client_id: &ClientId) -> Result<()> { - let time = Time::parse_from_rfc3339(&self.get_header_time().0) + fn set_client_update_time( + &mut self, + client_id: &ClientId, + ) -> std::result::Result<(), Self::Error> { + let time = Time::parse_from_rfc3339(&self.get_header_time()?.0) .map_err(|e| { Error::Time(format!("The time of the header is invalid: {}", e)) })?; @@ -751,36 +824,42 @@ pub trait IbcActions { self.write_ibc_data( &key, time.encode_vec().expect("encoding shouldn't fail"), - ); + )?; // the revision number is always 0 - let height = Height::new(0, self.get_height().0); + let height = Height::new(0, self.get_height()?.0); let height_key = storage::client_update_height_key(client_id); // write the current height as u64 self.write_ibc_data( &height_key, height.encode_vec().expect("Encoding shouldn't fail"), - ); + )?; Ok(()) } /// Get and increment the counter - fn get_and_inc_counter(&self, key: &Key) -> Result { - let value = self.read_ibc_data(key).ok_or_else(|| { + fn get_and_inc_counter( + &mut self, + key: &Key, + ) -> std::result::Result { + let value = self.read_ibc_data(key)?.ok_or_else(|| { Error::Counter(format!("The counter doesn't exist: {}", key)) })?; let value: [u8; 8] = value.try_into().map_err(|_| { Error::Counter(format!("The counter value wasn't u64: Key {}", key)) })?; let counter = u64::from_be_bytes(value); - self.write_ibc_data(key, (counter + 1).to_be_bytes()); + self.write_ibc_data(key, (counter + 1).to_be_bytes())?; Ok(counter) } /// Get and increment the sequence - fn get_and_inc_sequence(&self, key: &Key) -> Result { - let index = match self.read_ibc_data(key) { + fn get_and_inc_sequence( + &mut self, + key: &Key, + ) -> std::result::Result { + let index = match self.read_ibc_data(key)? { Some(v) => { let index: [u8; 8] = v.try_into().map_err(|_| { Error::Sequence(format!( @@ -793,29 +872,35 @@ pub trait IbcActions { // when the sequence has never been used, returns the initial value None => 1, }; - self.write_ibc_data(key, (index + 1).to_be_bytes()); + self.write_ibc_data(key, (index + 1).to_be_bytes())?; Ok(index.into()) } /// Bind a new port - fn bind_port(&self, port_id: &PortId) -> Result<()> { + fn bind_port( + &mut self, + port_id: &PortId, + ) -> std::result::Result<(), Self::Error> { let port_key = storage::port_key(port_id); - match self.read_ibc_data(&port_key) { + match self.read_ibc_data(&port_key)? { Some(_) => {} None => { // create a new capability and claim it let index_key = storage::capability_index_key(); let cap_index = self.get_and_inc_counter(&index_key)?; - self.write_ibc_data(&port_key, cap_index.to_be_bytes()); + self.write_ibc_data(&port_key, cap_index.to_be_bytes())?; let cap_key = storage::capability_key(cap_index); - self.write_ibc_data(&cap_key, port_id.as_bytes()); + self.write_ibc_data(&cap_key, port_id.as_bytes())?; } } Ok(()) } /// Send the specified token by escrowing or burning - fn send_token(&self, msg: &MsgTransfer) -> Result<()> { + fn send_token( + &mut self, + msg: &MsgTransfer, + ) -> std::result::Result<(), Self::Error> { let data = FungibleTokenPacketData::from(msg.clone()); let source = Address::decode(data.sender.clone()).map_err(|e| { Error::SendingToken(format!( @@ -852,7 +937,7 @@ pub trait IbcActions { if data.denomination.starts_with(&prefix) { // sink zone let burn = Address::Internal(InternalAddress::IbcBurn); - self.transfer_token(&source, &burn, &token, amount); + self.transfer_token(&source, &burn, &token, amount)?; } else { // source zone let escrow = @@ -860,7 +945,7 @@ pub trait IbcActions { msg.source_port.to_string(), msg.source_channel.to_string(), )); - self.transfer_token(&source, &escrow, &token, amount); + self.transfer_token(&source, &escrow, &token, amount)?; } // send a packet @@ -880,10 +965,10 @@ pub trait IbcActions { /// Receive the specified token by unescrowing or minting fn receive_token( - &self, + &mut self, packet: &Packet, data: &FungibleTokenPacketData, - ) -> Result<()> { + ) -> std::result::Result<(), Self::Error> { let dest = Address::decode(data.receiver.clone()).map_err(|e| { Error::ReceivingToken(format!( "Invalid receiver address: receiver {}, error {}", @@ -922,21 +1007,21 @@ pub trait IbcActions { packet.destination_port.to_string(), packet.destination_channel.to_string(), )); - self.transfer_token(&escrow, &dest, &token, amount); + self.transfer_token(&escrow, &dest, &token, amount)?; } else { // mint the token because the sender chain is the source let mint = Address::Internal(InternalAddress::IbcMint); - self.transfer_token(&mint, &dest, &token, amount); + self.transfer_token(&mint, &dest, &token, amount)?; } Ok(()) } /// Refund the specified token by unescrowing or minting fn refund_token( - &self, + &mut self, packet: &Packet, data: &FungibleTokenPacketData, - ) -> Result<()> { + ) -> std::result::Result<(), Self::Error> { let dest = Address::decode(data.sender.clone()).map_err(|e| { Error::ReceivingToken(format!( "Invalid sender address: sender {}, error {}", @@ -971,7 +1056,7 @@ pub trait IbcActions { if data.denomination.starts_with(&prefix) { // mint the token because the sender chain is the sink zone let mint = Address::Internal(InternalAddress::IbcMint); - self.transfer_token(&mint, &dest, &token, amount); + self.transfer_token(&mint, &dest, &token, amount)?; } else { // unescrow the token because the sender chain is the source zone let escrow = @@ -979,7 +1064,7 @@ pub trait IbcActions { packet.source_port.to_string(), packet.source_channel.to_string(), )); - self.transfer_token(&escrow, &dest, &token, amount); + self.transfer_token(&escrow, &dest, &token, amount)?; } Ok(()) } diff --git a/shared/src/ledger/ibc/vp/channel.rs b/shared/src/ledger/ibc/vp/channel.rs index 354899f31d..08ed322452 100644 --- a/shared/src/ledger/ibc/vp/channel.rs +++ b/shared/src/ledger/ibc/vp/channel.rs @@ -45,7 +45,7 @@ use crate::ibc::core::ics24_host::identifier::{ use crate::ibc::core::ics26_routing::msgs::Ics26Envelope; use crate::ibc::proofs::Proofs; use crate::ibc::timestamp::Timestamp; -use crate::ledger::native_vp::Error as NativeVpError; +use crate::ledger::native_vp::{Error as NativeVpError, VpEnv}; use crate::ledger::parameters; use crate::ledger::storage::{self as ledger_storage, StorageHasher}; use crate::tendermint::Time; @@ -490,7 +490,7 @@ where } fn get_sequence_pre(&self, key: &Key) -> Result { - match self.ctx.read_pre(key)? { + match self.ctx.read_bytes_pre(key)? { Some(value) => { // As ibc-go, u64 like a counter is encoded with big-endian let index: [u8; 8] = value.try_into().map_err(|_| { @@ -508,7 +508,7 @@ where } fn get_sequence(&self, key: &Key) -> Result { - match self.ctx.read_post(key)? { + match self.ctx.read_bytes_post(key)? { Some(value) => { // As ibc-go, u64 like a counter is encoded with big-endian let index: [u8; 8] = value.try_into().map_err(|_| { @@ -547,7 +547,7 @@ where port_channel_id: &PortChannelId, ) -> Result { let key = channel_key(port_channel_id); - match self.ctx.read_pre(&key) { + match self.ctx.read_bytes_pre(&key) { Ok(Some(value)) => ChannelEnd::decode_vec(&value).map_err(|e| { Error::InvalidChannel(format!( "Decoding the channel failed: Port/Channel {}, {}", @@ -594,7 +594,7 @@ where key: &(PortId, ChannelId, Sequence), ) -> Result { let key = commitment_key(&key.0, &key.1, key.2); - match self.ctx.read_pre(&key)? { + match self.ctx.read_bytes_pre(&key)? { Some(value) => String::decode(&value[..]).map_err(|e| { Error::InvalidPacketInfo(format!( "Decoding the prior commitment failed: {}", @@ -613,7 +613,7 @@ where client_id: &ClientId, ) -> Result { let key = client_update_timestamp_key(client_id); - match self.ctx.read_pre(&key)? { + match self.ctx.read_bytes_pre(&key)? { Some(value) => { let time = Time::decode_vec(&value).map_err(|_| { Error::InvalidTimestamp(format!( @@ -635,7 +635,7 @@ where client_id: &ClientId, ) -> Result { let key = client_update_height_key(client_id); - match self.ctx.read_pre(&key)? { + match self.ctx.read_bytes_pre(&key)? { Some(value) => Height::decode_vec(&value).map_err(|_| { Error::InvalidHeight(format!( "Height conversion failed: ID {}", @@ -671,7 +671,7 @@ where channel_id: port_channel_id.1.clone(), }; let key = channel_key(&port_channel_id); - match self.ctx.read_post(&key) { + match self.ctx.read_bytes_post(&key) { Ok(Some(value)) => ChannelEnd::decode_vec(&value) .map_err(|_| Ics04Error::implementation_specific()), Ok(None) => Err(Ics04Error::channel_not_found( @@ -818,7 +818,7 @@ where key: &(PortId, ChannelId, Sequence), ) -> Ics04Result { let commitment_key = commitment_key(&key.0, &key.1, key.2); - match self.ctx.read_post(&commitment_key) { + match self.ctx.read_bytes_post(&commitment_key) { Ok(Some(value)) => String::decode(&value[..]) .map_err(|_| Ics04Error::implementation_specific()), Ok(None) => Err(Ics04Error::packet_commitment_not_found(key.2)), @@ -832,7 +832,7 @@ where ) -> Ics04Result { let receipt_key = receipt_key(&key.0, &key.1, key.2); let expect = PacketReceipt::default().as_bytes().to_vec(); - match self.ctx.read_post(&receipt_key) { + match self.ctx.read_bytes_post(&receipt_key) { Ok(Some(v)) if v == expect => Ok(Receipt::Ok), _ => Err(Ics04Error::packet_receipt_not_found(key.2)), } @@ -844,7 +844,7 @@ where key: &(PortId, ChannelId, Sequence), ) -> Ics04Result { let ack_key = ack_key(&key.0, &key.1, key.2); - match self.ctx.read_post(&ack_key) { + match self.ctx.read_bytes_post(&ack_key) { Ok(Some(_)) => Ok(PacketAck::default().to_string()), Ok(None) => Err(Ics04Error::packet_commitment_not_found(key.2)), Err(_) => Err(Ics04Error::implementation_specific()), @@ -881,7 +881,7 @@ where height: Height, ) -> Ics04Result { let key = client_update_timestamp_key(client_id); - match self.ctx.read_post(&key) { + match self.ctx.read_bytes_post(&key) { Ok(Some(value)) => { let time = Time::decode_vec(&value) .map_err(|_| Ics04Error::implementation_specific())?; @@ -901,7 +901,7 @@ where height: Height, ) -> Ics04Result { let key = client_update_height_key(client_id); - match self.ctx.read_post(&key) { + match self.ctx.read_bytes_post(&key) { Ok(Some(value)) => Height::decode_vec(&value) .map_err(|_| Ics04Error::implementation_specific()), Ok(None) => Err(Ics04Error::processed_height_not_found( diff --git a/shared/src/ledger/ibc/vp/client.rs b/shared/src/ledger/ibc/vp/client.rs index 4b89e1ce30..453006f356 100644 --- a/shared/src/ledger/ibc/vp/client.rs +++ b/shared/src/ledger/ibc/vp/client.rs @@ -31,6 +31,7 @@ use crate::ibc::core::ics04_channel::context::ChannelReader; use crate::ibc::core::ics23_commitment::commitment::CommitmentRoot; use crate::ibc::core::ics24_host::identifier::ClientId; use crate::ibc::core::ics26_routing::msgs::Ics26Envelope; +use crate::ledger::native_vp::VpEnv; use crate::ledger::storage::{self, StorageHasher}; use crate::tendermint_proto::Protobuf; use crate::types::ibc::data::{Error as IbcDataError, IbcMessage}; @@ -378,7 +379,7 @@ where fn client_state_pre(&self, client_id: &ClientId) -> Result { let key = client_state_key(client_id); - match self.ctx.read_pre(&key) { + match self.ctx.read_bytes_pre(&key) { Ok(Some(value)) => { AnyClientState::decode_vec(&value).map_err(|e| { Error::InvalidClient(format!( @@ -410,7 +411,7 @@ where { fn client_type(&self, client_id: &ClientId) -> Ics02Result { let key = client_type_key(client_id); - match self.ctx.read_post(&key) { + match self.ctx.read_bytes_post(&key) { Ok(Some(value)) => { let type_str = std::str::from_utf8(&value) .map_err(|_| Ics02Error::implementation_specific())?; @@ -427,7 +428,7 @@ where client_id: &ClientId, ) -> Ics02Result { let key = client_state_key(client_id); - match self.ctx.read_post(&key) { + match self.ctx.read_bytes_post(&key) { Ok(Some(value)) => AnyClientState::decode_vec(&value) .map_err(|_| Ics02Error::implementation_specific()), Ok(None) => Err(Ics02Error::client_not_found(client_id.clone())), @@ -441,7 +442,7 @@ where height: Height, ) -> Ics02Result { let key = consensus_state_key(client_id, height); - match self.ctx.read_post(&key) { + match self.ctx.read_bytes_post(&key) { Ok(Some(value)) => AnyConsensusState::decode_vec(&value) .map_err(|_| Ics02Error::implementation_specific()), Ok(None) => Err(Ics02Error::consensus_state_not_found( @@ -459,7 +460,7 @@ where height: Height, ) -> Ics02Result> { let key = consensus_state_key(client_id, height); - match self.ctx.read_pre(&key) { + match self.ctx.read_bytes_pre(&key) { Ok(Some(value)) => { let cs = AnyConsensusState::decode_vec(&value) .map_err(|_| Ics02Error::implementation_specific())?; diff --git a/shared/src/ledger/ibc/vp/connection.rs b/shared/src/ledger/ibc/vp/connection.rs index 2e721bed08..0130dd3b84 100644 --- a/shared/src/ledger/ibc/vp/connection.rs +++ b/shared/src/ledger/ibc/vp/connection.rs @@ -27,6 +27,7 @@ use crate::ibc::core::ics03_connection::msgs::conn_open_confirm::MsgConnectionOp use crate::ibc::core::ics03_connection::msgs::conn_open_try::MsgConnectionOpenTry; use crate::ibc::core::ics23_commitment::commitment::CommitmentPrefix; use crate::ibc::core::ics24_host::identifier::{ClientId, ConnectionId}; +use crate::ledger::native_vp::VpEnv; use crate::ledger::storage::{self, StorageHasher}; use crate::tendermint_proto::Protobuf; use crate::types::ibc::data::{Error as IbcDataError, IbcMessage}; @@ -324,7 +325,7 @@ where conn_id: &ConnectionId, ) -> Result { let key = connection_key(conn_id); - match self.ctx.read_pre(&key) { + match self.ctx.read_bytes_pre(&key) { Ok(Some(value)) => ConnectionEnd::decode_vec(&value).map_err(|e| { Error::InvalidConnection(format!( "Decoding the connection failed: {}", @@ -356,7 +357,7 @@ where conn_id: &ConnectionId, ) -> Ics03Result { let key = connection_key(conn_id); - match self.ctx.read_post(&key) { + match self.ctx.read_bytes_post(&key) { Ok(Some(value)) => ConnectionEnd::decode_vec(&value) .map_err(|_| Ics03Error::implementation_specific()), Ok(None) => Err(Ics03Error::connection_not_found(conn_id.clone())), diff --git a/shared/src/ledger/ibc/vp/mod.rs b/shared/src/ledger/ibc/vp/mod.rs index b6bd15e43b..059862144d 100644 --- a/shared/src/ledger/ibc/vp/mod.rs +++ b/shared/src/ledger/ibc/vp/mod.rs @@ -17,7 +17,7 @@ pub use token::{Error as IbcTokenError, IbcToken}; use super::storage::{client_id, ibc_prefix, is_client_counter_key, IbcPrefix}; use crate::ibc::core::ics02_client::context::ClientReader; use crate::ibc::events::IbcEvent; -use crate::ledger::native_vp::{self, Ctx, NativeVp}; +use crate::ledger::native_vp::{self, Ctx, NativeVp, VpEnv}; use crate::ledger::storage::{self as ledger_storage, StorageHasher}; use crate::proto::SignedTxData; use crate::types::address::{Address, InternalAddress}; @@ -184,7 +184,7 @@ where } fn read_counter_pre(&self, key: &Key) -> Result { - match self.ctx.read_pre(key) { + match self.ctx.read_bytes_pre(key) { Ok(Some(value)) => { // As ibc-go, u64 like a counter is encoded with big-endian let counter: [u8; 8] = value.try_into().map_err(|_| { @@ -205,7 +205,7 @@ where } fn read_counter(&self, key: &Key) -> Result { - match self.ctx.read_post(key) { + match self.ctx.read_bytes_post(key) { Ok(Some(value)) => { // As ibc-go, u64 like a counter is encoded with big-endian let counter: [u8; 8] = value.try_into().map_err(|_| { @@ -375,6 +375,8 @@ mod tests { use crate::vm::wasm; use crate::types::storage::{BlockHash, BlockHeight}; + const ADDRESS: Address = Address::Internal(InternalAddress::Ibc); + fn get_client_id() -> ClientId { ClientId::from_str("test_client").expect("Creating a client ID failed") } @@ -568,13 +570,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); - let mut keys_changed = BTreeSet::new(); let client_state_key = client_state_key(&get_client_id()); keys_changed.insert(client_state_key); let verifiers = BTreeSet::new(); + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; // this should return true because state has been stored @@ -598,13 +608,22 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); let client_state_key = client_state_key(&get_client_id()); keys_changed.insert(client_state_key); let verifiers = BTreeSet::new(); + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; // this should fail because no state is stored @@ -668,13 +687,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); keys_changed.insert(client_state_key); let verifiers = BTreeSet::new(); - + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; // this should return true because state has been stored assert!( @@ -717,13 +744,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); keys_changed.insert(conn_key); let verifiers = BTreeSet::new(); - + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; // this should return true because state has been stored assert!( @@ -763,13 +798,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); keys_changed.insert(conn_key); let verifiers = BTreeSet::new(); - + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; // this should fail because no client exists let result = ibc @@ -835,13 +878,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); keys_changed.insert(conn_key); let verifiers = BTreeSet::new(); - + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; // this should return true because state has been stored assert!( @@ -913,13 +964,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); keys_changed.insert(conn_key); let verifiers = BTreeSet::new(); - + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; assert!( ibc.validate_tx( @@ -978,13 +1037,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); keys_changed.insert(conn_key); let verifiers = BTreeSet::new(); - + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; assert!( ibc.validate_tx( @@ -1029,13 +1096,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); keys_changed.insert(channel_key); let verifiers = BTreeSet::new(); - + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; assert!( ibc.validate_tx( @@ -1099,13 +1174,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); keys_changed.insert(channel_key); let verifiers = BTreeSet::new(); - + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; assert!( ibc.validate_tx( @@ -1177,13 +1260,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); keys_changed.insert(channel_key); let verifiers = BTreeSet::new(); - + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; assert!( ibc.validate_tx( @@ -1250,13 +1341,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); keys_changed.insert(channel_key); let verifiers = BTreeSet::new(); - + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; assert!( ibc.validate_tx( @@ -1281,13 +1380,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); keys_changed.insert(port_key(&get_port_id())); let verifiers = BTreeSet::new(); - + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; assert!( ibc.validate_tx( @@ -1313,13 +1420,22 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); let cap_key = capability_key(index); keys_changed.insert(cap_key); let verifiers = BTreeSet::new(); + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; assert!( @@ -1390,13 +1506,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); keys_changed.insert(seq_key); let verifiers = BTreeSet::new(); - + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; assert!( ibc.validate_tx( @@ -1469,13 +1593,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); keys_changed.insert(seq_key); let verifiers = BTreeSet::new(); - + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; assert!( ibc.validate_tx( @@ -1553,13 +1685,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); keys_changed.insert(seq_key); let verifiers = BTreeSet::new(); - + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; assert!( ibc.validate_tx( @@ -1633,13 +1773,21 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); let mut keys_changed = BTreeSet::new(); keys_changed.insert(commitment_key); let verifiers = BTreeSet::new(); - + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; assert!( ibc.validate_tx( @@ -1717,12 +1865,20 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); - let mut keys_changed = BTreeSet::new(); keys_changed.insert(receipt_key); let verifiers = BTreeSet::new(); + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; assert!( @@ -1757,12 +1913,20 @@ mod tests { let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); - let ctx = Ctx::new(&storage, &write_log, &tx, gas_meter, vp_wasm_cache); - let mut keys_changed = BTreeSet::new(); keys_changed.insert(ack_key); let verifiers = BTreeSet::new(); + let ctx = Ctx::new( + &ADDRESS, + &storage, + &write_log, + &tx, + gas_meter, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); let ibc = Ibc { ctx }; assert!( diff --git a/shared/src/ledger/ibc/vp/port.rs b/shared/src/ledger/ibc/vp/port.rs index 2819cdeab5..073f89147b 100644 --- a/shared/src/ledger/ibc/vp/port.rs +++ b/shared/src/ledger/ibc/vp/port.rs @@ -13,6 +13,7 @@ use crate::ibc::core::ics05_port::capabilities::{Capability, CapabilityName}; use crate::ibc::core::ics05_port::context::{CapabilityReader, PortReader}; use crate::ibc::core::ics05_port::error::Error as Ics05Error; use crate::ibc::core::ics24_host::identifier::PortId; +use crate::ledger::native_vp::VpEnv; use crate::ledger::storage::{self as ledger_storage, StorageHasher}; use crate::types::storage::Key; use crate::vm::WasmCacheAccess; @@ -122,7 +123,7 @@ where fn get_port_by_capability(&self, cap: &Capability) -> Result { let key = capability_key(cap.index()); - match self.ctx.read_post(&key) { + match self.ctx.read_bytes_post(&key) { Ok(Some(value)) => { let id = std::str::from_utf8(&value).map_err(|e| { Error::InvalidPort(format!( @@ -161,7 +162,7 @@ where port_id: &PortId, ) -> Ics05Result<(Self::ModuleId, Capability)> { let key = port_key(port_id); - match self.ctx.read_post(&key) { + match self.ctx.read_bytes_post(&key) { Ok(Some(value)) => { let index: [u8; 8] = value .try_into() diff --git a/shared/src/ledger/ibc/vp/token.rs b/shared/src/ledger/ibc/vp/token.rs index f56382b360..06181bdd45 100644 --- a/shared/src/ledger/ibc/vp/token.rs +++ b/shared/src/ledger/ibc/vp/token.rs @@ -10,7 +10,7 @@ use crate::ibc::applications::ics20_fungible_token_transfer::msgs::transfer::Msg use crate::ibc::core::ics04_channel::msgs::PacketMsg; use crate::ibc::core::ics04_channel::packet::Packet; use crate::ibc::core::ics26_routing::msgs::Ics26Envelope; -use crate::ledger::native_vp::{self, Ctx, NativeVp}; +use crate::ledger::native_vp::{self, Ctx, NativeVp, VpEnv}; use crate::ledger::storage::{self as ledger_storage, StorageHasher}; use crate::proto::SignedTxData; use crate::types::address::{Address, Error as AddressError, InternalAddress}; @@ -136,9 +136,10 @@ where // sink zone let target = Address::Internal(InternalAddress::IbcBurn); let target_key = token::balance_key(&token, &target); - let post = - try_decode_token_amount(self.ctx.read_temp(&target_key)?)? - .unwrap_or_default(); + let post = try_decode_token_amount( + self.ctx.read_bytes_temp(&target_key)?, + )? + .unwrap_or_default(); // the previous balance of the burn address should be zero post.change() } else { @@ -149,11 +150,13 @@ where msg.source_channel.to_string(), )); let target_key = token::balance_key(&token, &target); - let pre = try_decode_token_amount(self.ctx.read_pre(&target_key)?)? - .unwrap_or_default(); - let post = - try_decode_token_amount(self.ctx.read_post(&target_key)?)? + let pre = + try_decode_token_amount(self.ctx.read_bytes_pre(&target_key)?)? .unwrap_or_default(); + let post = try_decode_token_amount( + self.ctx.read_bytes_post(&target_key)?, + )? + .unwrap_or_default(); post.change() - pre.change() }; @@ -189,19 +192,22 @@ where packet.destination_channel.to_string(), )); let source_key = token::balance_key(&token, &source); - let pre = try_decode_token_amount(self.ctx.read_pre(&source_key)?)? - .unwrap_or_default(); - let post = - try_decode_token_amount(self.ctx.read_post(&source_key)?)? + let pre = + try_decode_token_amount(self.ctx.read_bytes_pre(&source_key)?)? .unwrap_or_default(); + let post = try_decode_token_amount( + self.ctx.read_bytes_post(&source_key)?, + )? + .unwrap_or_default(); pre.change() - post.change() } else { // the sender is the source let source = Address::Internal(InternalAddress::IbcMint); let source_key = token::balance_key(&token, &source); - let post = - try_decode_token_amount(self.ctx.read_temp(&source_key)?)? - .unwrap_or_default(); + let post = try_decode_token_amount( + self.ctx.read_bytes_temp(&source_key)?, + )? + .unwrap_or_default(); // the previous balance of the mint address should be the maximum Amount::max().change() - post.change() }; @@ -235,9 +241,10 @@ where // sink zone: mint the token for the refund let source = Address::Internal(InternalAddress::IbcMint); let source_key = token::balance_key(&token, &source); - let post = - try_decode_token_amount(self.ctx.read_temp(&source_key)?)? - .unwrap_or_default(); + let post = try_decode_token_amount( + self.ctx.read_bytes_temp(&source_key)?, + )? + .unwrap_or_default(); // the previous balance of the mint address should be the maximum Amount::max().change() - post.change() } else { @@ -248,11 +255,13 @@ where packet.source_channel.to_string(), )); let source_key = token::balance_key(&token, &source); - let pre = try_decode_token_amount(self.ctx.read_pre(&source_key)?)? - .unwrap_or_default(); - let post = - try_decode_token_amount(self.ctx.read_post(&source_key)?)? + let pre = + try_decode_token_amount(self.ctx.read_bytes_pre(&source_key)?)? .unwrap_or_default(); + let post = try_decode_token_amount( + self.ctx.read_bytes_post(&source_key)?, + )? + .unwrap_or_default(); pre.change() - post.change() }; diff --git a/shared/src/ledger/pos/vp.rs b/shared/src/ledger/pos/vp.rs index 26be440536..80572c1f57 100644 --- a/shared/src/ledger/pos/vp.rs +++ b/shared/src/ledger/pos/vp.rs @@ -27,7 +27,7 @@ use super::{ Unbonds, ValidatorConsensusKeys, ValidatorSets, ValidatorTotalDeltas, }; use crate::ledger::governance::vp::is_proposal_accepted; -use crate::ledger::native_vp::{self, Ctx, NativeVp}; +use crate::ledger::native_vp::{self, Ctx, NativeVp, VpEnv}; use crate::ledger::pos::{ is_validator_address_raw_hash_key, is_validator_consensus_key_key, is_validator_state_key, @@ -137,18 +137,18 @@ where return Ok(false); } } else if is_validator_set_key(key) { - let pre = self.ctx.read_pre(key)?.and_then(|bytes| { + let pre = self.ctx.read_bytes_pre(key)?.and_then(|bytes| { ValidatorSets::try_from_slice(&bytes[..]).ok() }); - let post = self.ctx.read_post(key)?.and_then(|bytes| { + let post = self.ctx.read_bytes_post(key)?.and_then(|bytes| { ValidatorSets::try_from_slice(&bytes[..]).ok() }); changes.push(ValidatorSet(Data { pre, post })); } else if let Some(validator) = is_validator_state_key(key) { - let pre = self.ctx.read_pre(key)?.and_then(|bytes| { + let pre = self.ctx.read_bytes_pre(key)?.and_then(|bytes| { ValidatorStates::try_from_slice(&bytes[..]).ok() }); - let post = self.ctx.read_post(key)?.and_then(|bytes| { + let post = self.ctx.read_bytes_post(key)?.and_then(|bytes| { ValidatorStates::try_from_slice(&bytes[..]).ok() }); changes.push(Validator { @@ -160,11 +160,11 @@ where { let pre = self .ctx - .read_pre(key)? + .read_bytes_pre(key)? .and_then(|bytes| Address::try_from_slice(&bytes[..]).ok()); let post = self .ctx - .read_post(key)? + .read_bytes_post(key)? .and_then(|bytes| Address::try_from_slice(&bytes[..]).ok()); changes.push(Validator { address: validator.clone(), @@ -172,10 +172,10 @@ where }); } else if let Some(validator) = is_validator_consensus_key_key(key) { - let pre = self.ctx.read_pre(key)?.and_then(|bytes| { + let pre = self.ctx.read_bytes_pre(key)?.and_then(|bytes| { ValidatorConsensusKeys::try_from_slice(&bytes[..]).ok() }); - let post = self.ctx.read_post(key)?.and_then(|bytes| { + let post = self.ctx.read_bytes_post(key)?.and_then(|bytes| { ValidatorConsensusKeys::try_from_slice(&bytes[..]).ok() }); changes.push(Validator { @@ -183,10 +183,10 @@ where update: ConsensusKey(Data { pre, post }), }); } else if let Some(validator) = is_validator_total_deltas_key(key) { - let pre = self.ctx.read_pre(key)?.and_then(|bytes| { + let pre = self.ctx.read_bytes_pre(key)?.and_then(|bytes| { ValidatorTotalDeltas::try_from_slice(&bytes[..]).ok() }); - let post = self.ctx.read_post(key)?.and_then(|bytes| { + let post = self.ctx.read_bytes_post(key)?.and_then(|bytes| { ValidatorTotalDeltas::try_from_slice(&bytes[..]).ok() }); changes.push(Validator { @@ -194,10 +194,10 @@ where update: TotalDeltas(Data { pre, post }), }); } else if let Some(validator) = is_validator_voting_power_key(key) { - let pre = self.ctx.read_pre(key)?.and_then(|bytes| { + let pre = self.ctx.read_bytes_pre(key)?.and_then(|bytes| { ValidatorVotingPowers::try_from_slice(&bytes[..]).ok() }); - let post = self.ctx.read_post(key)?.and_then(|bytes| { + let post = self.ctx.read_bytes_post(key)?.and_then(|bytes| { ValidatorVotingPowers::try_from_slice(&bytes[..]).ok() }); changes.push(Validator { @@ -209,11 +209,11 @@ where { let pre = self .ctx - .read_pre(key)? + .read_bytes_pre(key)? .and_then(|bytes| Address::try_from_slice(&bytes[..]).ok()); let post = self .ctx - .read_post(key)? + .read_bytes_post(key)? .and_then(|bytes| Address::try_from_slice(&bytes[..]).ok()); // Find the raw hashes of the addresses let pre = pre.map(|pre| { @@ -236,26 +236,26 @@ where if owner != &addr { continue; } - let pre = self.ctx.read_pre(key)?.and_then(|bytes| { + let pre = self.ctx.read_bytes_pre(key)?.and_then(|bytes| { token::Amount::try_from_slice(&bytes[..]).ok() }); - let post = self.ctx.read_post(key)?.and_then(|bytes| { + let post = self.ctx.read_bytes_post(key)?.and_then(|bytes| { token::Amount::try_from_slice(&bytes[..]).ok() }); changes.push(Balance(Data { pre, post })); } else if let Some(bond_id) = is_bond_key(key) { let pre = self .ctx - .read_pre(key)? + .read_bytes_pre(key)? .and_then(|bytes| Bonds::try_from_slice(&bytes[..]).ok()); let post = self .ctx - .read_post(key)? + .read_bytes_post(key)? .and_then(|bytes| Bonds::try_from_slice(&bytes[..]).ok()); // For bonds, we need to look-up slashes let slashes = self .ctx - .read_pre(&validator_slashes_key(&bond_id.validator))? + .read_bytes_pre(&validator_slashes_key(&bond_id.validator))? .and_then(|bytes| Slashes::try_from_slice(&bytes[..]).ok()) .unwrap_or_default(); changes.push(Bond { @@ -266,16 +266,18 @@ where } else if let Some(unbond_id) = is_unbond_key(key) { let pre = self .ctx - .read_pre(key)? + .read_bytes_pre(key)? .and_then(|bytes| Unbonds::try_from_slice(&bytes[..]).ok()); let post = self .ctx - .read_post(key)? + .read_bytes_post(key)? .and_then(|bytes| Unbonds::try_from_slice(&bytes[..]).ok()); // For unbonds, we need to look-up slashes let slashes = self .ctx - .read_pre(&validator_slashes_key(&unbond_id.validator))? + .read_bytes_pre(&validator_slashes_key( + &unbond_id.validator, + ))? .and_then(|bytes| Slashes::try_from_slice(&bytes[..]).ok()) .unwrap_or_default(); changes.push(Unbond { @@ -284,10 +286,10 @@ where slashes, }); } else if is_total_voting_power_key(key) { - let pre = self.ctx.read_pre(key)?.and_then(|bytes| { + let pre = self.ctx.read_bytes_pre(key)?.and_then(|bytes| { TotalVotingPowers::try_from_slice(&bytes[..]).ok() }); - let post = self.ctx.read_post(key)?.and_then(|bytes| { + let post = self.ctx.read_bytes_post(key)?.and_then(|bytes| { TotalVotingPowers::try_from_slice(&bytes[..]).ok() }); changes.push(TotalVotingPower(Data { pre, post })); @@ -301,7 +303,7 @@ where } } - let params = self.read_pos_params(); + let params = self.read_pos_params()?; let errors = validate(¶ms, changes, current_epoch); Ok(if errors.is_empty() { true @@ -322,6 +324,7 @@ where CA: 'static + WasmCacheAccess, { type Address = Address; + type Error = native_vp::Error; type PublicKey = key::common::PublicKey; type TokenAmount = token::Amount; type TokenChange = token::Change; @@ -332,88 +335,95 @@ where super::staking_token_address() } - fn read_pos_params(&self) -> PosParams { - let value = self.ctx.read_pre(¶ms_key()).unwrap().unwrap(); - decode(value).unwrap() + fn read_pos_params(&self) -> std::result::Result { + let value = self.ctx.read_bytes_pre(¶ms_key())?.unwrap(); + Ok(decode(value).unwrap()) } fn read_validator_staking_reward_address( &self, key: &Self::Address, - ) -> Option { + ) -> std::result::Result, Self::Error> { let value = self .ctx - .read_pre(&validator_staking_reward_address_key(key)) - .unwrap(); - value.map(|value| decode(value).unwrap()) + .read_bytes_pre(&validator_staking_reward_address_key(key))?; + Ok(value.map(|value| decode(value).unwrap())) } fn read_validator_consensus_key( &self, key: &Self::Address, - ) -> Option { - let value = self - .ctx - .read_pre(&validator_consensus_key_key(key)) - .unwrap(); - value.map(|value| decode(value).unwrap()) + ) -> std::result::Result, Self::Error> { + let value = + self.ctx.read_bytes_pre(&validator_consensus_key_key(key))?; + Ok(value.map(|value| decode(value).unwrap())) } fn read_validator_state( &self, key: &Self::Address, - ) -> Option { - let value = self.ctx.read_pre(&validator_state_key(key)).unwrap(); - value.map(|value| decode(value).unwrap()) + ) -> std::result::Result, Self::Error> { + let value = self.ctx.read_bytes_pre(&validator_state_key(key))?; + Ok(value.map(|value| decode(value).unwrap())) } fn read_validator_total_deltas( &self, key: &Self::Address, - ) -> Option { + ) -> std::result::Result, Self::Error> { let value = - self.ctx.read_pre(&validator_total_deltas_key(key)).unwrap(); - value.map(|value| decode(value).unwrap()) + self.ctx.read_bytes_pre(&validator_total_deltas_key(key))?; + Ok(value.map(|value| decode(value).unwrap())) } fn read_validator_voting_power( &self, key: &Self::Address, - ) -> Option { + ) -> std::result::Result, Self::Error> { let value = - self.ctx.read_pre(&validator_voting_power_key(key)).unwrap(); - value.map(|value| decode(value).unwrap()) + self.ctx.read_bytes_pre(&validator_voting_power_key(key))?; + Ok(value.map(|value| decode(value).unwrap())) } - fn read_validator_slashes(&self, key: &Self::Address) -> Vec { - let value = self.ctx.read_pre(&validator_slashes_key(key)).unwrap(); - value + fn read_validator_slashes( + &self, + key: &Self::Address, + ) -> std::result::Result, Self::Error> { + let value = self.ctx.read_bytes_pre(&validator_slashes_key(key))?; + Ok(value .map(|value| decode(value).unwrap()) - .unwrap_or_default() + .unwrap_or_default()) } - fn read_bond(&self, key: &BondId) -> Option { - let value = self.ctx.read_pre(&bond_key(key)).unwrap(); - value.map(|value| decode(value).unwrap()) + fn read_bond( + &self, + key: &BondId, + ) -> std::result::Result, Self::Error> { + let value = self.ctx.read_bytes_pre(&bond_key(key))?; + Ok(value.map(|value| decode(value).unwrap())) } - fn read_unbond(&self, key: &BondId) -> Option { - let value = self.ctx.read_pre(&unbond_key(key)).unwrap(); - value.map(|value| decode(value).unwrap()) + fn read_unbond( + &self, + key: &BondId, + ) -> std::result::Result, Self::Error> { + let value = self.ctx.read_bytes_pre(&unbond_key(key))?; + Ok(value.map(|value| decode(value).unwrap())) } - fn read_validator_set(&self) -> ValidatorSets { - let value = self.ctx.read_pre(&validator_set_key()).unwrap().unwrap(); - decode(value).unwrap() + fn read_validator_set( + &self, + ) -> std::result::Result { + let value = self.ctx.read_bytes_pre(&validator_set_key())?.unwrap(); + Ok(decode(value).unwrap()) } - fn read_total_voting_power(&self) -> TotalVotingPowers { - let value = self - .ctx - .read_pre(&total_voting_power_key()) - .unwrap() - .unwrap(); - decode(value).unwrap() + fn read_total_voting_power( + &self, + ) -> std::result::Result { + let value = + self.ctx.read_bytes_pre(&total_voting_power_key())?.unwrap(); + Ok(decode(value).unwrap()) } } diff --git a/shared/src/ledger/treasury/mod.rs b/shared/src/ledger/treasury/mod.rs index 071019059b..97965282da 100644 --- a/shared/src/ledger/treasury/mod.rs +++ b/shared/src/ledger/treasury/mod.rs @@ -11,7 +11,7 @@ use thiserror::Error; use self::storage as treasury_storage; use super::governance::vp::is_proposal_accepted; -use crate::ledger::native_vp::{self, Ctx, NativeVp}; +use crate::ledger::native_vp::{self, Ctx, NativeVp, VpEnv}; use crate::ledger::storage::{self as ledger_storage, StorageHasher}; use crate::types::address::{xan as nam, Address, InternalAddress}; use crate::types::storage::Key; @@ -80,63 +80,30 @@ where let is_max_funds_transfer_key = treasury_storage::get_max_transferable_fund_key(); let balance_key = token::balance_key(&nam(), &ADDRESS); - let max_transfer_amount = - self.ctx.read_pre(&is_max_funds_transfer_key); - let pre_balance = self.ctx.read_pre(&balance_key); - let post_balance = self.ctx.read_post(&balance_key); + let max_transfer_amount: std::result::Result< + Option, + _, + > = self.ctx.read_pre(&is_max_funds_transfer_key); + let pre_balance: std::result::Result< + Option, + _, + > = self.ctx.read_pre(&balance_key); + let post_balance: std::result::Result< + Option, + _, + > = self.ctx.read_post(&balance_key); if addr.ne(&ADDRESS) { return true; } match (max_transfer_amount, pre_balance, post_balance) { ( - Ok(max_transfer_amount), - Ok(pre_balance), - Ok(post_balance), + Ok(Some(max_transfer_amount)), + Ok(Some(pre_balance)), + Ok(Some(post_balance)), ) => { - match ( - max_transfer_amount, - pre_balance, - post_balance, - ) { - ( - Some(max_transfer_amount), - Some(pre_balance), - Some(post_balance), - ) => { - let max_transfer_amount = - token::Amount::try_from_slice( - &max_transfer_amount[..], - ) - .ok(); - let pre_balance = - token::Amount::try_from_slice( - &pre_balance[..], - ) - .ok(); - let post_balance = - token::Amount::try_from_slice( - &post_balance[..], - ) - .ok(); - match ( - max_transfer_amount, - pre_balance, - post_balance, - ) { - ( - Some(max_transfer_amount), - Some(pre_balance), - Some(post_balance), - ) => { - post_balance > pre_balance - || (pre_balance - post_balance - <= max_transfer_amount) - } - _ => false, - } - } - _ => false, - } + post_balance > pre_balance + || (pre_balance - post_balance + <= max_transfer_amount) } _ => false, } From e4b84a00452fd3534fcdaa7ac1eb7fcca4c6bab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 20 Jul 2022 21:01:42 +0200 Subject: [PATCH 05/81] VM: move vm_env sub-mod into tx/vp_prelude and add Tx/VpEnv and Ctx --- Cargo.lock | 10 +- tx_prelude/Cargo.toml | 4 + tx_prelude/src/error.rs | 103 +++++ tx_prelude/src/governance.rs | 78 ++++ tx_prelude/src/ibc.rs | 79 ++++ tx_prelude/src/intent.rs | 18 + tx_prelude/src/lib.rs | 293 +++++++++++++- tx_prelude/src/nft.rs | 89 +++++ tx_prelude/src/proof_of_stake.rs | 338 ++++++++++++++++ tx_prelude/src/token.rs | 53 +++ vm_env/Cargo.toml | 2 - vm_env/src/governance.rs | 81 ---- vm_env/src/ibc.rs | 50 --- vm_env/src/imports.rs | 665 ------------------------------- vm_env/src/intent.rs | 39 -- vm_env/src/key/ed25519.rs | 0 vm_env/src/key/mod.rs | 16 - vm_env/src/lib.rs | 262 +++++++++--- vm_env/src/nft.rs | 194 --------- vm_env/src/proof_of_stake.rs | 261 ------------ vm_env/src/token.rs | 113 ------ vp_prelude/Cargo.toml | 4 + vp_prelude/src/error.rs | 103 +++++ vp_prelude/src/intent.rs | 19 + vp_prelude/src/key.rs | 13 + vp_prelude/src/lib.rs | 284 ++++++++++++- vp_prelude/src/nft.rs | 116 ++++++ vp_prelude/src/token.rs | 61 +++ 28 files changed, 1869 insertions(+), 1479 deletions(-) create mode 100644 tx_prelude/src/error.rs create mode 100644 tx_prelude/src/governance.rs create mode 100644 tx_prelude/src/ibc.rs create mode 100644 tx_prelude/src/intent.rs create mode 100644 tx_prelude/src/nft.rs create mode 100644 tx_prelude/src/proof_of_stake.rs create mode 100644 tx_prelude/src/token.rs delete mode 100644 vm_env/src/governance.rs delete mode 100644 vm_env/src/ibc.rs delete mode 100644 vm_env/src/imports.rs delete mode 100644 vm_env/src/intent.rs delete mode 100644 vm_env/src/key/ed25519.rs delete mode 100644 vm_env/src/key/mod.rs delete mode 100644 vm_env/src/nft.rs delete mode 100644 vm_env/src/proof_of_stake.rs delete mode 100644 vm_env/src/token.rs create mode 100644 vp_prelude/src/error.rs create mode 100644 vp_prelude/src/intent.rs create mode 100644 vp_prelude/src/key.rs create mode 100644 vp_prelude/src/nft.rs create mode 100644 vp_prelude/src/token.rs diff --git a/Cargo.lock b/Cargo.lock index f9c1414272..76df02e9b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4036,8 +4036,12 @@ dependencies = [ name = "namada_tx_prelude" version = "0.7.0" dependencies = [ + "borsh", + "namada", + "namada_macros", "namada_vm_env", "sha2 0.10.2", + "thiserror", ] [[package]] @@ -4045,17 +4049,19 @@ name = "namada_vm_env" version = "0.7.0" dependencies = [ "borsh", - "hex", "namada", - "namada_macros", ] [[package]] name = "namada_vp_prelude" version = "0.7.0" dependencies = [ + "borsh", + "namada", + "namada_macros", "namada_vm_env", "sha2 0.10.2", + "thiserror", ] [[package]] diff --git a/tx_prelude/Cargo.toml b/tx_prelude/Cargo.toml index a35324da05..e916a13e01 100644 --- a/tx_prelude/Cargo.toml +++ b/tx_prelude/Cargo.toml @@ -10,5 +10,9 @@ version = "0.7.0" default = [] [dependencies] +namada = {path = "../shared"} namada_vm_env = {path = "../vm_env"} +namada_macros = {path = "../macros"} +borsh = "0.9.0" sha2 = "0.10.1" +thiserror = "1.0.30" diff --git a/tx_prelude/src/error.rs b/tx_prelude/src/error.rs new file mode 100644 index 0000000000..b34d954166 --- /dev/null +++ b/tx_prelude/src/error.rs @@ -0,0 +1,103 @@ +//! Helpers for error handling in WASM +//! +//! This module is currently duplicated in tx_prelude and vp_prelude crates to +//! be able to implement `From` conversion on error types from other crates, +//! avoiding `error[E0117]: only traits defined in the current crate can be +//! implemented for arbitrary types` + +use thiserror::Error; + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + SimpleMessage(&'static str), + #[error("{0}")] + Custom(CustomError), + #[error("{0}: {1}")] + CustomWithMessage(&'static str, CustomError), +} + +/// Result of transaction or VP. +pub type EnvResult = Result; + +pub trait ResultExt { + /// Replace a possible error with a static message in [`EnvResult`]. + fn err_msg(self, msg: &'static str) -> EnvResult; +} + +// This is separate from `ResultExt`, because the implementation requires +// different bounds for `T`. +pub trait ResultExt2 { + /// Convert a [`Result`] into [`EnvResult`]. + fn into_env_result(self) -> EnvResult; + + /// Add a static message to a possible error in [`EnvResult`]. + fn wrap_err(self, msg: &'static str) -> EnvResult; +} + +pub trait OptionExt { + /// Transforms the [`Option`] into a [`EnvResult`], mapping + /// [`Some(v)`] to [`Ok(v)`] and [`None`] to the given static error + /// message. + fn ok_or_err_msg(self, msg: &'static str) -> EnvResult; +} + +impl ResultExt for Result { + fn err_msg(self, msg: &'static str) -> EnvResult { + self.map_err(|_err| Error::new_const(msg)) + } +} + +impl ResultExt2 for Result +where + E: std::error::Error + Send + Sync + 'static, +{ + fn into_env_result(self) -> EnvResult { + self.map_err(Error::new) + } + + fn wrap_err(self, msg: &'static str) -> EnvResult { + self.map_err(|err| Error::wrap(msg, err)) + } +} + +impl OptionExt for Option { + fn ok_or_err_msg(self, msg: &'static str) -> EnvResult { + self.ok_or_else(|| Error::new_const(msg)) + } +} + +impl Error { + /// Create an [`enum@Error`] from a static message. + #[inline] + pub const fn new_const(msg: &'static str) -> Self { + Self::SimpleMessage(msg) + } + + /// Create an [`enum@Error`] from another [`std::error::Error`]. + pub fn new(error: E) -> Self + where + E: Into>, + { + Self::Custom(CustomError(error.into())) + } + + /// Wrap another [`std::error::Error`] with a static message. + pub fn wrap(msg: &'static str, error: E) -> Self + where + E: Into>, + { + Self::CustomWithMessage(msg, CustomError(error.into())) + } +} + +/// A custom error +#[derive(Debug)] +pub struct CustomError(Box); + +impl std::fmt::Display for CustomError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} diff --git a/tx_prelude/src/governance.rs b/tx_prelude/src/governance.rs new file mode 100644 index 0000000000..a0dfce2c77 --- /dev/null +++ b/tx_prelude/src/governance.rs @@ -0,0 +1,78 @@ +//! Governance + +use namada::ledger::governance::storage; +use namada::ledger::governance::vp::ADDRESS as governance_address; +use namada::types::address::xan as m1t; +use namada::types::token::Amount; +use namada::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 { + let counter_key = storage::get_counter_key(); + let proposal_id = if let Some(id) = data.id { + id + } else { + ctx.read(&counter_key)?.unwrap() + }; + + let content_key = storage::get_content_key(proposal_id); + ctx.write_bytes(&content_key, data.content)?; + + let author_key = storage::get_author_key(proposal_id); + ctx.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)?; + + let voting_end_epoch_key = storage::get_voting_end_epoch_key(proposal_id); + ctx.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)?; + + 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)?; + } + + ctx.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 funds_key = storage::get_funds_key(proposal_id); + ctx.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, ())?; + + transfer( + ctx, + &data.author, + &governance_address, + &m1t(), + min_proposal_funds, + ) +} + +/// A proposal vote transaction. +pub fn vote_proposal(ctx: &mut Ctx, data: VoteProposalData) -> TxResult { + 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())?; + } + Ok(()) +} diff --git a/tx_prelude/src/ibc.rs b/tx_prelude/src/ibc.rs new file mode 100644 index 0000000000..21be213ea3 --- /dev/null +++ b/tx_prelude/src/ibc.rs @@ -0,0 +1,79 @@ +//! IBC lower-level functions for transactions. + +pub use namada::ledger::ibc::handler::{Error, IbcActions, Result}; +use namada::ledger::tx_env::TxEnv; +use namada::types::address::Address; +pub use namada::types::ibc::IbcEvent; +use namada::types::storage::{BlockHeight, Key}; +use namada::types::time::Rfc3339String; +use namada::types::token::Amount; + +use crate::token::transfer; +use crate::Ctx; + +// This is needed to use `ibc::Handler::Error` with `IbcActions` below +impl From for crate::Error { + fn from(err: Error) -> Self { + crate::Error::new(err) + } +} + +impl IbcActions for Ctx { + type Error = crate::Error; + + fn read_ibc_data( + &self, + key: &Key, + ) -> std::result::Result>, Self::Error> { + let data = self.read_bytes(key)?; + Ok(data) + } + + fn write_ibc_data( + &mut self, + key: &Key, + data: impl AsRef<[u8]>, + ) -> std::result::Result<(), Self::Error> { + self.write_bytes(key, data)?; + Ok(()) + } + + fn delete_ibc_data( + &mut self, + key: &Key, + ) -> std::result::Result<(), Self::Error> { + self.delete(key)?; + Ok(()) + } + + fn emit_ibc_event( + &mut self, + event: IbcEvent, + ) -> std::result::Result<(), Self::Error> { + ::emit_ibc_event(self, &event)?; + Ok(()) + } + + fn transfer_token( + &mut self, + src: &Address, + dest: &Address, + token: &Address, + amount: Amount, + ) -> std::result::Result<(), Self::Error> { + transfer(self, src, dest, token, amount)?; + Ok(()) + } + + fn get_height(&self) -> std::result::Result { + let val = self.get_block_height()?; + Ok(val) + } + + fn get_header_time( + &self, + ) -> std::result::Result { + let val = self.get_block_time()?; + Ok(val) + } +} diff --git a/tx_prelude/src/intent.rs b/tx_prelude/src/intent.rs new file mode 100644 index 0000000000..7b6826342e --- /dev/null +++ b/tx_prelude/src/intent.rs @@ -0,0 +1,18 @@ +use std::collections::HashSet; + +use namada::proto::Signed; +use namada::types::intent; +pub use namada::types::intent::*; +use namada::types::key::*; + +use super::*; +pub fn invalidate_exchange( + ctx: &mut Ctx, + intent: &Signed, +) -> TxResult { + let key = intent::invalid_intent_key(&intent.data.addr); + let mut invalid_intent: HashSet = + ctx.read(&key)?.unwrap_or_default(); + invalid_intent.insert(intent.sig.clone()); + ctx.write(&key, &invalid_intent) +} diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index 315c68384e..5d0009b01b 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -6,7 +6,47 @@ #![deny(rustdoc::broken_intra_doc_links)] #![deny(rustdoc::private_intra_doc_links)] -pub use namada_vm_env::tx_prelude::*; +mod error; +pub mod governance; +pub mod ibc; +pub mod intent; +pub mod nft; +pub mod proof_of_stake; +pub mod token; + +use core::slice; +use std::marker::PhantomData; + +pub use borsh::{BorshDeserialize, BorshSerialize}; +pub use error::*; +pub use namada::ledger::governance::storage as gov_storage; +pub use namada::ledger::parameters::storage as parameters_storage; +pub use namada::ledger::storage::types::encode; +pub use namada::ledger::treasury::storage as treasury_storage; +pub use namada::ledger::tx_env::TxEnv; +pub use namada::proto::{Signed, SignedTxData}; +pub use namada::types::address::Address; +use namada::types::chain::CHAIN_ID_LENGTH; +use namada::types::internal::HostEnvResult; +use namada::types::storage::{ + BlockHash, BlockHeight, Epoch, BLOCK_HASH_LENGTH, +}; +use namada::types::time::Rfc3339String; +pub use namada::types::*; +pub use namada_macros::transaction; +use namada_vm_env::tx::*; +use namada_vm_env::{read_from_buffer, read_key_val_bytes_from_buffer}; + +pub use crate::ibc::IbcActions; +pub use crate::proof_of_stake::{PosRead, PosWrite}; + +/// Log a string. The message will be printed at the `tracing::Level::Info`. +pub fn log_string>(msg: T) { + let msg = msg.as_ref(); + unsafe { + anoma_tx_log_string(msg.as_ptr() as _, msg.len() as _); + } +} /// Log a string in a debug build. The message will be printed at the /// `tracing::Level::Info`. Any `debug_log!` statements are only enabled in @@ -19,3 +59,254 @@ macro_rules! debug_log { (if cfg!(debug_assertions) { log_string(format!($($arg)*)) }) }} } + +/// Execution context provides access to the host environment functions +pub struct Ctx(()); + +impl Ctx { + /// Create a host context. The context on WASM side is only provided by + /// the VM once its being executed (in here it's implicit). But + /// because we want to have interface identical with the native + /// VPs, in which the context is explicit, in here we're just + /// using an empty `Ctx` to "fake" it. + /// + /// # Safety + /// + /// When using `#[transaction]` macro from `anoma_macros`, + /// the constructor should not be called from transactions and validity + /// predicates implementation directly - they receive `&Self` as + /// an argument provided by the macro that wrap the low-level WASM + /// interface with Rust native types. + /// + /// Otherwise, this should only be called once to initialize this "fake" + /// context in order to benefit from type-safety of the host environment + /// methods implemented on the context. + #[allow(clippy::new_without_default)] + pub const unsafe fn new() -> Self { + Self(()) + } +} + +/// Transaction result +pub type TxResult = EnvResult<()>; + +#[derive(Debug)] +pub struct KeyValIterator(pub u64, pub PhantomData); + +impl TxEnv for Ctx { + type Error = Error; + type PrefixIter = KeyValIterator<(String, Vec)>; + + fn read( + &self, + key: &namada::types::storage::Key, + ) -> Result, Error> { + let key = key.to_string(); + let read_result = + unsafe { anoma_tx_read(key.as_ptr() as _, key.len() as _) }; + Ok(read_from_buffer(read_result, anoma_tx_result_buffer) + .and_then(|t| T::try_from_slice(&t[..]).ok())) + } + + fn read_bytes( + &self, + key: &namada::types::storage::Key, + ) -> Result>, Error> { + let key = key.to_string(); + let read_result = + unsafe { anoma_tx_read(key.as_ptr() as _, key.len() as _) }; + Ok(read_from_buffer(read_result, anoma_tx_result_buffer)) + } + + fn has_key( + &self, + key: &namada::types::storage::Key, + ) -> Result { + let key = key.to_string(); + let found = + unsafe { anoma_tx_has_key(key.as_ptr() as _, key.len() as _) }; + Ok(HostEnvResult::is_success(found)) + } + + fn get_chain_id(&self) -> Result { + let result = Vec::with_capacity(CHAIN_ID_LENGTH); + unsafe { + anoma_tx_get_chain_id(result.as_ptr() as _); + } + let slice = + unsafe { slice::from_raw_parts(result.as_ptr(), CHAIN_ID_LENGTH) }; + Ok(String::from_utf8(slice.to_vec()) + .expect("Cannot convert the ID string")) + } + + fn get_block_height( + &self, + ) -> Result { + Ok(BlockHeight(unsafe { anoma_tx_get_block_height() })) + } + + fn get_block_hash( + &self, + ) -> Result { + let result = Vec::with_capacity(BLOCK_HASH_LENGTH); + unsafe { + anoma_tx_get_block_hash(result.as_ptr() as _); + } + let slice = unsafe { + slice::from_raw_parts(result.as_ptr(), BLOCK_HASH_LENGTH) + }; + Ok(BlockHash::try_from(slice).expect("Cannot convert the hash")) + } + + fn get_block_epoch(&self) -> Result { + Ok(Epoch(unsafe { anoma_tx_get_block_epoch() })) + } + + fn get_block_time(&self) -> Result { + let read_result = unsafe { anoma_tx_get_block_time() }; + let time_value = read_from_buffer(read_result, anoma_tx_result_buffer) + .expect("The block time should exist"); + Ok(Rfc3339String( + String::try_from_slice(&time_value[..]) + .expect("The conversion shouldn't fail"), + )) + } + + fn iter_prefix( + &self, + prefix: &namada::types::storage::Key, + ) -> Result { + let prefix = prefix.to_string(); + let iter_id = unsafe { + anoma_tx_iter_prefix(prefix.as_ptr() as _, prefix.len() as _) + }; + Ok(KeyValIterator(iter_id, PhantomData)) + } + + fn iter_next( + &self, + iter: &mut Self::PrefixIter, + ) -> Result)>, Error> { + let read_result = unsafe { anoma_tx_iter_next(iter.0) }; + Ok(read_key_val_bytes_from_buffer( + read_result, + anoma_tx_result_buffer, + )) + } + + fn write( + &mut self, + key: &namada::types::storage::Key, + val: T, + ) -> Result<(), Error> { + let buf = val.try_to_vec().unwrap(); + self.write_bytes(key, buf) + } + + fn write_bytes( + &mut self, + key: &namada::types::storage::Key, + val: impl AsRef<[u8]>, + ) -> Result<(), Error> { + let key = key.to_string(); + unsafe { + anoma_tx_write( + key.as_ptr() as _, + key.len() as _, + val.as_ref().as_ptr() as _, + val.as_ref().len() as _, + ) + }; + Ok(()) + } + + fn write_temp( + &mut self, + key: &namada::types::storage::Key, + val: T, + ) -> Result<(), Error> { + let buf = val.try_to_vec().unwrap(); + self.write_bytes_temp(key, buf) + } + + fn write_bytes_temp( + &mut self, + key: &namada::types::storage::Key, + val: impl AsRef<[u8]>, + ) -> Result<(), Error> { + let key = key.to_string(); + unsafe { + anoma_tx_write_temp( + key.as_ptr() as _, + key.len() as _, + val.as_ref().as_ptr() as _, + val.as_ref().len() as _, + ) + }; + Ok(()) + } + + fn delete( + &mut self, + key: &namada::types::storage::Key, + ) -> Result<(), Error> { + let key = key.to_string(); + unsafe { anoma_tx_delete(key.as_ptr() as _, key.len() as _) }; + Ok(()) + } + + fn insert_verifier(&mut self, addr: &Address) -> Result<(), Error> { + let addr = addr.encode(); + unsafe { anoma_tx_insert_verifier(addr.as_ptr() as _, addr.len() as _) } + Ok(()) + } + + fn init_account( + &mut self, + code: impl AsRef<[u8]>, + ) -> Result { + let code = code.as_ref(); + let result = Vec::with_capacity(address::ESTABLISHED_ADDRESS_BYTES_LEN); + unsafe { + anoma_tx_init_account( + code.as_ptr() as _, + code.len() as _, + result.as_ptr() as _, + ) + }; + let slice = unsafe { + slice::from_raw_parts( + result.as_ptr(), + address::ESTABLISHED_ADDRESS_BYTES_LEN, + ) + }; + Ok(Address::try_from_slice(slice) + .expect("Decoding address created by the ledger shouldn't fail")) + } + + fn update_validity_predicate( + &mut self, + addr: &Address, + code: impl AsRef<[u8]>, + ) -> Result<(), Error> { + let addr = addr.encode(); + let code = code.as_ref(); + unsafe { + anoma_tx_update_validity_predicate( + addr.as_ptr() as _, + addr.len() as _, + code.as_ptr() as _, + code.len() as _, + ) + }; + Ok(()) + } + + fn emit_ibc_event(&mut self, event: &ibc::IbcEvent) -> Result<(), Error> { + let event = BorshSerialize::try_to_vec(event).unwrap(); + unsafe { + anoma_tx_emit_ibc_event(event.as_ptr() as _, event.len() as _) + }; + Ok(()) + } +} diff --git a/tx_prelude/src/nft.rs b/tx_prelude/src/nft.rs new file mode 100644 index 0000000000..4ed179fe27 --- /dev/null +++ b/tx_prelude/src/nft.rs @@ -0,0 +1,89 @@ +use namada::types::address::Address; +use namada::types::nft; +use namada::types::nft::NftToken; +use namada::types::transaction::nft::{CreateNft, MintNft}; + +use super::*; + +/// Initialize a new NFT token address. +pub fn init_nft(ctx: &mut Ctx, nft: CreateNft) -> EnvResult
{ + let address = ctx.init_account(&nft.vp_code)?; + + // write tag + let tag_key = nft::get_tag_key(&address); + ctx.write(&tag_key, &nft.tag)?; + + // write creator + let creator_key = nft::get_creator_key(&address); + ctx.write(&creator_key, &nft.creator)?; + + // write keys + let keys_key = nft::get_keys_key(&address); + ctx.write(&keys_key, &nft.keys)?; + + // write optional keys + let optional_keys_key = nft::get_optional_keys_key(&address); + ctx.write(&optional_keys_key, nft.opt_keys)?; + + // mint tokens + aux_mint_token(ctx, &address, &nft.creator, nft.tokens, &nft.creator)?; + + ctx.insert_verifier(&nft.creator)?; + + Ok(address) +} + +pub fn mint_tokens(ctx: &mut Ctx, nft: MintNft) -> TxResult { + aux_mint_token(ctx, &nft.address, &nft.creator, nft.tokens, &nft.creator) +} + +fn aux_mint_token( + ctx: &mut Ctx, + nft_address: &Address, + creator_address: &Address, + tokens: Vec, + verifier: &Address, +) -> TxResult { + for token in tokens { + // write token metadata + let metadata_key = + nft::get_token_metadata_key(nft_address, &token.id.to_string()); + ctx.write(&metadata_key, &token.metadata)?; + + // write current owner token as creator + let current_owner_key = nft::get_token_current_owner_key( + nft_address, + &token.id.to_string(), + ); + ctx.write( + ¤t_owner_key, + &token + .current_owner + .unwrap_or_else(|| creator_address.clone()), + )?; + + // write value key + let value_key = + nft::get_token_value_key(nft_address, &token.id.to_string()); + ctx.write(&value_key, &token.values)?; + + // write optional value keys + let optional_value_key = nft::get_token_optional_value_key( + nft_address, + &token.id.to_string(), + ); + ctx.write(&optional_value_key, &token.opt_values)?; + + // write approval addresses + let approval_key = + nft::get_token_approval_key(nft_address, &token.id.to_string()); + ctx.write(&approval_key, &token.approvals)?; + + // write burnt propriety + let burnt_key = + nft::get_token_burnt_key(nft_address, &token.id.to_string()); + ctx.write(&burnt_key, token.burnt)?; + } + ctx.insert_verifier(verifier)?; + Ok(()) +} diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs new file mode 100644 index 0000000000..ce856cd876 --- /dev/null +++ b/tx_prelude/src/proof_of_stake.rs @@ -0,0 +1,338 @@ +//! Proof of Stake system integration with functions for transactions + +use namada::ledger::pos::types::Slash; +pub use namada::ledger::pos::*; +use namada::ledger::pos::{ + bond_key, namada_proof_of_stake, params_key, total_voting_power_key, + unbond_key, validator_address_raw_hash_key, validator_consensus_key_key, + validator_set_key, validator_slashes_key, + validator_staking_reward_address_key, validator_state_key, + validator_total_deltas_key, validator_voting_power_key, +}; +use namada::types::address::{self, Address, InternalAddress}; +use namada::types::transaction::InitValidator; +use namada::types::{key, token}; +pub use namada_proof_of_stake::{ + epoched, parameters, types, PosActions as PosWrite, PosReadOnly as PosRead, +}; + +use super::*; + +impl Ctx { + /// Self-bond tokens to a validator when `source` is `None` or equal to + /// the `validator` address, or delegate tokens from the `source` to the + /// `validator`. + pub fn bond_tokens( + &mut self, + source: Option<&Address>, + validator: &Address, + amount: token::Amount, + ) -> TxResult { + let current_epoch = self.get_block_epoch()?; + namada_proof_of_stake::PosActions::bond_tokens( + self, + source, + validator, + amount, + current_epoch, + ) + } + + /// Unbond self-bonded tokens from a validator when `source` is `None` or + /// equal to the `validator` address, or unbond delegated tokens from + /// the `source` to the `validator`. + pub fn unbond_tokens( + &mut self, + source: Option<&Address>, + validator: &Address, + amount: token::Amount, + ) -> TxResult { + let current_epoch = self.get_block_epoch()?; + namada_proof_of_stake::PosActions::unbond_tokens( + self, + source, + validator, + amount, + current_epoch, + ) + } + + /// Withdraw unbonded tokens from a self-bond to a validator when `source` + /// is `None` or equal to the `validator` address, or withdraw unbonded + /// tokens delegated to the `validator` to the `source`. + pub fn withdraw_tokens( + &mut self, + source: Option<&Address>, + validator: &Address, + ) -> EnvResult { + let current_epoch = self.get_block_epoch()?; + namada_proof_of_stake::PosActions::withdraw_tokens( + self, + source, + validator, + current_epoch, + ) + } + + /// Attempt to initialize a validator account. On success, returns the + /// initialized validator account's address and its staking reward address. + pub fn init_validator( + &mut self, + InitValidator { + account_key, + consensus_key, + rewards_account_key, + protocol_key, + dkg_key, + validator_vp_code, + rewards_vp_code, + }: InitValidator, + ) -> EnvResult<(Address, Address)> { + let current_epoch = self.get_block_epoch()?; + // Init validator account + let validator_address = self.init_account(&validator_vp_code)?; + let pk_key = key::pk_key(&validator_address); + self.write(&pk_key, &account_key)?; + let protocol_pk_key = key::protocol_pk_key(&validator_address); + self.write(&protocol_pk_key, &protocol_key)?; + let dkg_pk_key = key::dkg_session_keys::dkg_pk_key(&validator_address); + self.write(&dkg_pk_key, &dkg_key)?; + + // Init staking reward account + let rewards_address = self.init_account(&rewards_vp_code)?; + let pk_key = key::pk_key(&rewards_address); + self.write(&pk_key, &rewards_account_key)?; + + self.become_validator( + &validator_address, + &rewards_address, + &consensus_key, + current_epoch, + )?; + + Ok((validator_address, rewards_address)) + } +} + +impl namada_proof_of_stake::PosReadOnly for Ctx { + type Address = Address; + type Error = crate::Error; + type PublicKey = key::common::PublicKey; + type TokenAmount = token::Amount; + type TokenChange = token::Change; + + const POS_ADDRESS: Self::Address = Address::Internal(InternalAddress::PoS); + + fn staking_token_address() -> Self::Address { + address::xan() + } + + fn read_pos_params(&self) -> Result { + let params = self.read(¶ms_key())?; + Ok(params.expect("PoS params should always be set")) + } + + fn read_validator_staking_reward_address( + &self, + key: &Self::Address, + ) -> Result, Self::Error> { + self.read(&validator_staking_reward_address_key(key)) + } + + fn read_validator_consensus_key( + &self, + key: &Self::Address, + ) -> Result, Self::Error> { + self.read(&validator_consensus_key_key(key)) + } + + fn read_validator_state( + &self, + key: &Self::Address, + ) -> Result, Self::Error> { + self.read(&validator_state_key(key)) + } + + fn read_validator_total_deltas( + &self, + key: &Self::Address, + ) -> Result, Self::Error> { + self.read(&validator_total_deltas_key(key)) + } + + fn read_validator_voting_power( + &self, + key: &Self::Address, + ) -> Result, Self::Error> { + self.read(&validator_voting_power_key(key)) + } + + fn read_validator_slashes( + &self, + key: &Self::Address, + ) -> Result, Self::Error> { + let val = self.read(&validator_slashes_key(key))?; + Ok(val.unwrap_or_default()) + } + + fn read_bond(&self, key: &BondId) -> Result, Self::Error> { + self.read(&bond_key(key)) + } + + fn read_unbond( + &self, + key: &BondId, + ) -> Result, Self::Error> { + self.read(&unbond_key(key)) + } + + fn read_validator_set(&self) -> Result { + let val = self.read(&validator_set_key())?; + Ok(val.expect("Validator sets must always have a value")) + } + + fn read_total_voting_power( + &self, + ) -> Result { + let val = self.read(&total_voting_power_key())?; + Ok(val.expect("Total voting power must always have a value")) + } +} + +impl From> for Error { + fn from(err: namada_proof_of_stake::BecomeValidatorError
) -> Self { + Self::new(err) + } +} + +impl From> for Error { + fn from(err: namada_proof_of_stake::BondError
) -> Self { + Self::new(err) + } +} + +impl From> + for Error +{ + fn from( + err: namada_proof_of_stake::UnbondError, + ) -> Self { + Self::new(err) + } +} + +impl From> for Error { + fn from(err: namada_proof_of_stake::WithdrawError
) -> Self { + Self::new(err) + } +} + +impl namada_proof_of_stake::PosActions for Ctx { + type BecomeValidatorError = crate::Error; + type BondError = crate::Error; + type UnbondError = crate::Error; + type WithdrawError = crate::Error; + + fn write_pos_params( + &mut self, + params: &PosParams, + ) -> Result<(), Self::Error> { + self.write(¶ms_key(), params) + } + + fn write_validator_address_raw_hash( + &mut self, + address: &Self::Address, + ) -> Result<(), Self::Error> { + let raw_hash = address.raw_hash().unwrap().to_owned(); + self.write(&validator_address_raw_hash_key(raw_hash), address) + } + + fn write_validator_staking_reward_address( + &mut self, + key: &Self::Address, + value: Self::Address, + ) -> Result<(), Self::Error> { + self.write(&validator_staking_reward_address_key(key), &value) + } + + fn write_validator_consensus_key( + &mut self, + key: &Self::Address, + value: ValidatorConsensusKeys, + ) -> Result<(), Self::Error> { + self.write(&validator_consensus_key_key(key), &value) + } + + fn write_validator_state( + &mut self, + key: &Self::Address, + value: ValidatorStates, + ) -> Result<(), Self::Error> { + self.write(&validator_state_key(key), &value) + } + + fn write_validator_total_deltas( + &mut self, + key: &Self::Address, + value: ValidatorTotalDeltas, + ) -> Result<(), Self::Error> { + self.write(&validator_total_deltas_key(key), &value) + } + + fn write_validator_voting_power( + &mut self, + key: &Self::Address, + value: ValidatorVotingPowers, + ) -> Result<(), Self::Error> { + self.write(&validator_voting_power_key(key), &value) + } + + fn write_bond( + &mut self, + key: &BondId, + value: Bonds, + ) -> Result<(), Self::Error> { + self.write(&bond_key(key), &value) + } + + fn write_unbond( + &mut self, + key: &BondId, + value: Unbonds, + ) -> Result<(), Self::Error> { + self.write(&unbond_key(key), &value) + } + + fn write_validator_set( + &mut self, + value: ValidatorSets, + ) -> Result<(), Self::Error> { + self.write(&validator_set_key(), &value) + } + + fn write_total_voting_power( + &mut self, + value: TotalVotingPowers, + ) -> Result<(), Self::Error> { + self.write(&total_voting_power_key(), &value) + } + + fn delete_bond(&mut self, key: &BondId) -> Result<(), Self::Error> { + self.delete(&bond_key(key)) + } + + fn delete_unbond(&mut self, key: &BondId) -> Result<(), Self::Error> { + self.delete(&unbond_key(key)) + } + + fn transfer( + &mut self, + token: &Self::Address, + amount: Self::TokenAmount, + src: &Self::Address, + dest: &Self::Address, + ) -> Result<(), Self::Error> { + crate::token::transfer(self, src, dest, token, amount) + } +} diff --git a/tx_prelude/src/token.rs b/tx_prelude/src/token.rs new file mode 100644 index 0000000000..2fa86efd45 --- /dev/null +++ b/tx_prelude/src/token.rs @@ -0,0 +1,53 @@ +use namada::types::address::{Address, InternalAddress}; +use namada::types::token; +pub use namada::types::token::*; + +use super::*; + +/// A token transfer that can be used in a transaction. +pub fn transfer( + ctx: &mut Ctx, + src: &Address, + dest: &Address, + token: &Address, + amount: Amount, +) -> TxResult { + let src_key = token::balance_key(token, src); + let dest_key = token::balance_key(token, dest); + let src_bal: Option = ctx.read(&src_key)?; + let mut src_bal = src_bal.unwrap_or_else(|| match src { + Address::Internal(InternalAddress::IbcMint) => Amount::max(), + _ => { + log_string(format!("src {} has no balance", src)); + unreachable!() + } + }); + src_bal.spend(&amount); + let mut dest_bal: Amount = ctx.read(&dest_key)?.unwrap_or_default(); + dest_bal.receive(&amount); + match src { + Address::Internal(InternalAddress::IbcMint) => { + ctx.write_temp(&src_key, src_bal)?; + } + Address::Internal(InternalAddress::IbcBurn) => { + log_string("invalid transfer from the burn address"); + unreachable!() + } + _ => { + ctx.write(&src_key, src_bal)?; + } + } + match dest { + Address::Internal(InternalAddress::IbcMint) => { + log_string("invalid transfer to the mint address"); + unreachable!() + } + Address::Internal(InternalAddress::IbcBurn) => { + ctx.write_temp(&dest_key, dest_bal)?; + } + _ => { + ctx.write(&dest_key, dest_bal)?; + } + } + Ok(()) +} diff --git a/vm_env/Cargo.toml b/vm_env/Cargo.toml index f11d57d1b0..06a9e210ca 100644 --- a/vm_env/Cargo.toml +++ b/vm_env/Cargo.toml @@ -11,6 +11,4 @@ default = [] [dependencies] namada = {path = "../shared"} -namada_macros = {path = "../macros"} borsh = "0.9.0" -hex = "0.4.3" diff --git a/vm_env/src/governance.rs b/vm_env/src/governance.rs deleted file mode 100644 index db4ea7916f..0000000000 --- a/vm_env/src/governance.rs +++ /dev/null @@ -1,81 +0,0 @@ -/// Tx imports and functions. -pub mod tx { - - use namada::ledger::governance::storage; - use namada::ledger::governance::vp::ADDRESS as governance_address; - use namada::types::address::xan as m1t; - use namada::types::token::Amount; - use namada::types::transaction::governance::{ - InitProposalData, VoteProposalData, - }; - - use crate::imports::tx; - use crate::token::tx::transfer; - - /// A proposal creation transaction. - pub fn init_proposal(data: InitProposalData) { - let counter_key = storage::get_counter_key(); - let proposal_id = if let Some(id) = data.id { - id - } else { - tx::read(&counter_key.to_string()).unwrap() - }; - - let content_key = storage::get_content_key(proposal_id); - tx::write_bytes(&content_key.to_string(), data.content); - - let author_key = storage::get_author_key(proposal_id); - tx::write(&author_key.to_string(), data.author.clone()); - - let voting_start_epoch_key = - storage::get_voting_start_epoch_key(proposal_id); - tx::write(&voting_start_epoch_key.to_string(), data.voting_start_epoch); - - let voting_end_epoch_key = - storage::get_voting_end_epoch_key(proposal_id); - tx::write(&voting_end_epoch_key.to_string(), data.voting_end_epoch); - - let grace_epoch_key = storage::get_grace_epoch_key(proposal_id); - tx::write(&grace_epoch_key.to_string(), data.grace_epoch); - - if let Some(proposal_code) = data.proposal_code { - let proposal_code_key = storage::get_proposal_code_key(proposal_id); - tx::write_bytes(&proposal_code_key.to_string(), proposal_code); - } - - tx::write(&counter_key.to_string(), proposal_id + 1); - - let min_proposal_funds_key = storage::get_min_proposal_fund_key(); - let min_proposal_funds: Amount = - tx::read(&min_proposal_funds_key.to_string()).unwrap(); - - let funds_key = storage::get_funds_key(proposal_id); - tx::write(&funds_key.to_string(), 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, - ); - tx::write(&committing_proposals_key.to_string(), ()); - - transfer( - &data.author, - &governance_address, - &m1t(), - min_proposal_funds, - ); - } - - /// A proposal vote transaction. - pub fn vote_proposal(data: VoteProposalData) { - for delegation in data.delegations { - let vote_key = storage::get_vote_proposal_key( - data.id, - data.voter.clone(), - delegation, - ); - tx::write(&vote_key.to_string(), data.vote.clone()); - } - } -} diff --git a/vm_env/src/ibc.rs b/vm_env/src/ibc.rs deleted file mode 100644 index febaa78560..0000000000 --- a/vm_env/src/ibc.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! IBC functions for transactions. - -pub use namada::ledger::ibc::handler::IbcActions; -use namada::types::address::Address; -use namada::types::ibc::IbcEvent; -use namada::types::storage::{BlockHeight, Key}; -use namada::types::time::Rfc3339String; -use namada::types::token::Amount; - -use crate::imports::tx; -use crate::token::tx::transfer; - -/// This struct integrates and gives access to lower-level IBC functions. -pub struct Ibc; - -impl IbcActions for Ibc { - fn read_ibc_data(&self, key: &Key) -> Option> { - tx::read_bytes(key.to_string()) - } - - fn write_ibc_data(&self, key: &Key, data: impl AsRef<[u8]>) { - tx::write_bytes(key.to_string(), data) - } - - fn delete_ibc_data(&self, key: &Key) { - tx::delete(key.to_string()) - } - - fn emit_ibc_event(&self, event: IbcEvent) { - tx::emit_ibc_event(&event) - } - - fn transfer_token( - &self, - src: &Address, - dest: &Address, - token: &Address, - amount: Amount, - ) { - transfer(src, dest, token, amount) - } - - fn get_height(&self) -> BlockHeight { - tx::get_block_height() - } - - fn get_header_time(&self) -> Rfc3339String { - tx::get_block_time() - } -} diff --git a/vm_env/src/imports.rs b/vm_env/src/imports.rs deleted file mode 100644 index 2eabe77e54..0000000000 --- a/vm_env/src/imports.rs +++ /dev/null @@ -1,665 +0,0 @@ -use std::mem::ManuallyDrop; - -use borsh::BorshDeserialize; -use namada::types::internal::HostEnvResult; -use namada::vm::types::KeyVal; - -/// This function is a helper to handle the second step of reading var-len -/// values from the host. -/// -/// In cases where we're reading a value from the host in the guest and -/// we don't know the byte size up-front, we have to read it in 2-steps. The -/// first step reads the value into a result buffer and returns the size (if -/// any) back to the guest, the second step reads the value from cache into a -/// pre-allocated buffer with the obtained size. -fn read_from_buffer( - read_result: i64, - result_buffer: unsafe extern "C" fn(u64), -) -> Option> { - if HostEnvResult::is_fail(read_result) { - None - } else { - let result: Vec = Vec::with_capacity(read_result as _); - // The `result` will be dropped from the `target`, which is - // reconstructed from the same memory - let result = ManuallyDrop::new(result); - let offset = result.as_slice().as_ptr() as u64; - unsafe { result_buffer(offset) }; - let target = unsafe { - Vec::from_raw_parts(offset as _, read_result as _, read_result as _) - }; - Some(target) - } -} - -/// This function is a helper to handle the second step of reading var-len -/// values in a key-value pair from the host. -fn read_key_val_from_buffer( - read_result: i64, - result_buffer: unsafe extern "C" fn(u64), -) -> Option<(String, T)> { - let key_val = read_from_buffer(read_result, result_buffer) - .and_then(|t| KeyVal::try_from_slice(&t[..]).ok()); - key_val.and_then(|key_val| { - // decode the value - T::try_from_slice(&key_val.val) - .map(|val| (key_val.key, val)) - .ok() - }) -} - -/// Transaction environment imports -pub mod tx { - use core::slice; - use std::convert::TryFrom; - use std::marker::PhantomData; - - pub use borsh::{BorshDeserialize, BorshSerialize}; - use namada::types::address; - use namada::types::address::Address; - use namada::types::chain::CHAIN_ID_LENGTH; - use namada::types::ibc::IbcEvent; - use namada::types::internal::HostEnvResult; - use namada::types::storage::{ - BlockHash, BlockHeight, Epoch, BLOCK_HASH_LENGTH, - }; - use namada::types::time::Rfc3339String; - - #[derive(Debug)] - pub struct KeyValIterator(pub u64, pub PhantomData); - - /// Try to read a Borsh encoded variable-length value at the given key from - /// storage. - pub fn read(key: impl AsRef) -> Option { - let key = key.as_ref(); - let read_result = - unsafe { anoma_tx_read(key.as_ptr() as _, key.len() as _) }; - super::read_from_buffer(read_result, anoma_tx_result_buffer) - .and_then(|t| T::try_from_slice(&t[..]).ok()) - } - - /// Try to read a variable-length value as bytes at the given key from - /// storage. - pub fn read_bytes(key: impl AsRef) -> Option> { - let key = key.as_ref(); - let read_result = - unsafe { anoma_tx_read(key.as_ptr() as _, key.len() as _) }; - super::read_from_buffer(read_result, anoma_tx_result_buffer) - } - - /// Check if the given key is present in storage. - pub fn has_key(key: impl AsRef) -> bool { - let key = key.as_ref(); - let found = - unsafe { anoma_tx_has_key(key.as_ptr() as _, key.len() as _) }; - HostEnvResult::is_success(found) - } - - /// Write a value to be encoded with Borsh at the given key to storage. - pub fn write(key: impl AsRef, val: T) { - let buf = val.try_to_vec().unwrap(); - write_bytes(key, buf); - } - - /// Write a value as bytes at the given key to storage. - pub fn write_bytes(key: impl AsRef, val: impl AsRef<[u8]>) { - let key = key.as_ref(); - unsafe { - anoma_tx_write( - key.as_ptr() as _, - key.len() as _, - val.as_ref().as_ptr() as _, - val.as_ref().len() as _, - ) - }; - } - - /// Write a temporary value to be encoded with Borsh at the given key to - /// storage. - pub fn write_temp(key: impl AsRef, val: T) { - let buf = val.try_to_vec().unwrap(); - write_bytes_temp(key, buf); - } - - /// Write a temporary value as bytes at the given key to storage. - pub fn write_bytes_temp(key: impl AsRef, val: impl AsRef<[u8]>) { - let key = key.as_ref(); - unsafe { - anoma_tx_write_temp( - key.as_ptr() as _, - key.len() as _, - val.as_ref().as_ptr() as _, - val.as_ref().len() as _, - ) - }; - } - - /// Delete a value at the given key from storage. - pub fn delete(key: impl AsRef) { - let key = key.as_ref(); - unsafe { anoma_tx_delete(key.as_ptr() as _, key.len() as _) }; - } - - /// Get an iterator with the given prefix. - /// - /// Important note: The prefix iterator will ignore keys that are not yet - /// committed to storage from the block in which this transaction is being - /// applied. It will only find keys that are already committed to - /// storage (i.e. from predecessor blocks). However, it will provide the - /// most up-to-date value for such keys. - pub fn iter_prefix( - prefix: impl AsRef, - ) -> KeyValIterator { - let prefix = prefix.as_ref(); - let iter_id = unsafe { - anoma_tx_iter_prefix(prefix.as_ptr() as _, prefix.len() as _) - }; - KeyValIterator(iter_id, PhantomData) - } - - impl Iterator for KeyValIterator { - type Item = (String, T); - - fn next(&mut self) -> Option<(String, T)> { - let read_result = unsafe { anoma_tx_iter_next(self.0) }; - super::read_key_val_from_buffer(read_result, anoma_tx_result_buffer) - } - } - - /// Insert a verifier address. This address must exist on chain, otherwise - /// the transaction will be rejected. - /// - /// Validity predicates of each verifier addresses inserted in the - /// transaction will validate the transaction and will receive all the - /// changed storage keys and initialized accounts in their inputs. - pub fn insert_verifier(addr: &Address) { - let addr = addr.encode(); - unsafe { anoma_tx_insert_verifier(addr.as_ptr() as _, addr.len() as _) } - } - - /// Update a validity predicate - pub fn update_validity_predicate(addr: &Address, code: impl AsRef<[u8]>) { - let addr = addr.encode(); - let code = code.as_ref(); - unsafe { - anoma_tx_update_validity_predicate( - addr.as_ptr() as _, - addr.len() as _, - code.as_ptr() as _, - code.len() as _, - ) - }; - } - - // Initialize a new account - pub fn init_account(code: impl AsRef<[u8]>) -> Address { - let code = code.as_ref(); - let result = Vec::with_capacity(address::ESTABLISHED_ADDRESS_BYTES_LEN); - unsafe { - anoma_tx_init_account( - code.as_ptr() as _, - code.len() as _, - result.as_ptr() as _, - ) - }; - let slice = unsafe { - slice::from_raw_parts( - result.as_ptr(), - address::ESTABLISHED_ADDRESS_BYTES_LEN, - ) - }; - Address::try_from_slice(slice) - .expect("Decoding address created by the ledger shouldn't fail") - } - - /// Emit an IBC event. There can be only one event per transaction. On - /// multiple calls, only the last emitted event will be used. - pub fn emit_ibc_event(event: &IbcEvent) { - let event = BorshSerialize::try_to_vec(event).unwrap(); - unsafe { - anoma_tx_emit_ibc_event(event.as_ptr() as _, event.len() as _) - }; - } - - /// Get the chain ID - pub fn get_chain_id() -> String { - let result = Vec::with_capacity(CHAIN_ID_LENGTH); - unsafe { - anoma_tx_get_chain_id(result.as_ptr() as _); - } - let slice = - unsafe { slice::from_raw_parts(result.as_ptr(), CHAIN_ID_LENGTH) }; - String::from_utf8(slice.to_vec()).expect("Cannot convert the ID string") - } - - /// Get height of the current block - pub fn get_block_height() -> BlockHeight { - BlockHeight(unsafe { anoma_tx_get_block_height() }) - } - - /// Get time of the current block header as rfc 3339 string - pub fn get_block_time() -> Rfc3339String { - let read_result = unsafe { anoma_tx_get_block_time() }; - let time_value = - super::read_from_buffer(read_result, anoma_tx_result_buffer) - .expect("The block time should exist"); - Rfc3339String( - String::try_from_slice(&time_value[..]) - .expect("The conversion shouldn't fail"), - ) - } - - /// Get hash of the current block - pub fn get_block_hash() -> BlockHash { - let result = Vec::with_capacity(BLOCK_HASH_LENGTH); - unsafe { - anoma_tx_get_block_hash(result.as_ptr() as _); - } - let slice = unsafe { - slice::from_raw_parts(result.as_ptr(), BLOCK_HASH_LENGTH) - }; - BlockHash::try_from(slice).expect("Cannot convert the hash") - } - - /// Get epoch of the current block - pub fn get_block_epoch() -> Epoch { - Epoch(unsafe { anoma_tx_get_block_epoch() }) - } - - /// Log a string. The message will be printed at the `tracing::Level::Info`. - pub fn log_string>(msg: T) { - let msg = msg.as_ref(); - unsafe { - anoma_tx_log_string(msg.as_ptr() as _, msg.len() as _); - } - } - - // These host functions are implemented in the Anoma's [`host_env`] - // module. The environment provides calls to them via this C interface. - extern "C" { - // Read variable-length data when we don't know the size up-front, - // returns the size of the value (can be 0), or -1 if the key is - // not present. If a value is found, it will be placed in the read - // cache, because we cannot allocate a buffer for it before we know - // its size. - fn anoma_tx_read(key_ptr: u64, key_len: u64) -> i64; - - // Read a value from result buffer. - fn anoma_tx_result_buffer(result_ptr: u64); - - // Returns 1 if the key is present, -1 otherwise. - fn anoma_tx_has_key(key_ptr: u64, key_len: u64) -> i64; - - // Write key/value - fn anoma_tx_write( - key_ptr: u64, - key_len: u64, - val_ptr: u64, - val_len: u64, - ); - - // Write a temporary key/value - fn anoma_tx_write_temp( - key_ptr: u64, - key_len: u64, - val_ptr: u64, - val_len: u64, - ); - - // Delete the given key and its value - fn anoma_tx_delete(key_ptr: u64, key_len: u64); - - // Get an ID of a data iterator with key prefix - fn anoma_tx_iter_prefix(prefix_ptr: u64, prefix_len: u64) -> u64; - - // Returns the size of the value (can be 0), or -1 if there's no next - // value. If a value is found, it will be placed in the read - // cache, because we cannot allocate a buffer for it before we know - // its size. - fn anoma_tx_iter_next(iter_id: u64) -> i64; - - // Insert a verifier - fn anoma_tx_insert_verifier(addr_ptr: u64, addr_len: u64); - - // Update a validity predicate - fn anoma_tx_update_validity_predicate( - addr_ptr: u64, - addr_len: u64, - code_ptr: u64, - code_len: u64, - ); - - // Initialize a new account - fn anoma_tx_init_account(code_ptr: u64, code_len: u64, result_ptr: u64); - - // Emit an IBC event - fn anoma_tx_emit_ibc_event(event_ptr: u64, event_len: u64); - - // Get the chain ID - fn anoma_tx_get_chain_id(result_ptr: u64); - - // Get the current block height - fn anoma_tx_get_block_height() -> u64; - - // Get the time of the current block header - fn anoma_tx_get_block_time() -> i64; - - // Get the current block hash - fn anoma_tx_get_block_hash(result_ptr: u64); - - // Get the current block epoch - fn anoma_tx_get_block_epoch() -> u64; - - // Requires a node running with "Info" log level - fn anoma_tx_log_string(str_ptr: u64, str_len: u64); - } -} - -/// Validity predicate environment imports -pub mod vp { - use core::slice; - use std::convert::TryFrom; - use std::marker::PhantomData; - - pub use borsh::{BorshDeserialize, BorshSerialize}; - use namada::types::chain::CHAIN_ID_LENGTH; - use namada::types::hash::{Hash, HASH_LENGTH}; - use namada::types::internal::HostEnvResult; - use namada::types::key::*; - use namada::types::storage::{ - BlockHash, BlockHeight, Epoch, BLOCK_HASH_LENGTH, - }; - - pub struct PreKeyValIterator(pub u64, pub PhantomData); - - pub struct PostKeyValIterator(pub u64, pub PhantomData); - - /// Try to read a Borsh encoded variable-length value at the given key from - /// storage before transaction execution. - pub fn read_pre(key: impl AsRef) -> Option { - let key = key.as_ref(); - let read_result = - unsafe { anoma_vp_read_pre(key.as_ptr() as _, key.len() as _) }; - super::read_from_buffer(read_result, anoma_vp_result_buffer) - .and_then(|t| T::try_from_slice(&t[..]).ok()) - } - - /// Try to read a variable-length value as bytesat the given key from - /// storage before transaction execution. - pub fn read_bytes_pre(key: impl AsRef) -> Option> { - let key = key.as_ref(); - let read_result = - unsafe { anoma_vp_read_pre(key.as_ptr() as _, key.len() as _) }; - super::read_from_buffer(read_result, anoma_vp_result_buffer) - } - - /// Try to read a Borsh encoded variable-length value at the given key from - /// storage after transaction execution. - pub fn read_post(key: impl AsRef) -> Option { - let key = key.as_ref(); - let read_result = - unsafe { anoma_vp_read_post(key.as_ptr() as _, key.len() as _) }; - super::read_from_buffer(read_result, anoma_vp_result_buffer) - .and_then(|t| T::try_from_slice(&t[..]).ok()) - } - - /// Try to read a variable-length value as bytes at the given key from - /// storage after transaction execution. - pub fn read_bytes_post(key: impl AsRef) -> Option> { - let key = key.as_ref(); - let read_result = - unsafe { anoma_vp_read_post(key.as_ptr() as _, key.len() as _) }; - super::read_from_buffer(read_result, anoma_vp_result_buffer) - } - - /// Try to read a Borsh encoded variable-length value at the given key from - /// storage before transaction execution. - pub fn read_temp(key: impl AsRef) -> Option { - let key = key.as_ref(); - let read_result = - unsafe { anoma_vp_read_temp(key.as_ptr() as _, key.len() as _) }; - super::read_from_buffer(read_result, anoma_vp_result_buffer) - .and_then(|t| T::try_from_slice(&t[..]).ok()) - } - - /// Try to read a variable-length value as bytes at the given key from - /// storage before transaction execution. - pub fn read_bytes_temp(key: impl AsRef) -> Option> { - let key = key.as_ref(); - let read_result = - unsafe { anoma_vp_read_temp(key.as_ptr() as _, key.len() as _) }; - super::read_from_buffer(read_result, anoma_vp_result_buffer) - } - - /// Check if the given key was present in storage before transaction - /// execution. - pub fn has_key_pre(key: impl AsRef) -> bool { - let key = key.as_ref(); - let found = - unsafe { anoma_vp_has_key_pre(key.as_ptr() as _, key.len() as _) }; - HostEnvResult::is_success(found) - } - - /// Check if the given key is present in storage after transaction - /// execution. - pub fn has_key_post(key: impl AsRef) -> bool { - let key = key.as_ref(); - let found = - unsafe { anoma_vp_has_key_post(key.as_ptr() as _, key.len() as _) }; - HostEnvResult::is_success(found) - } - - /// Get an iterator with the given prefix before transaction execution - pub fn iter_prefix_pre( - prefix: impl AsRef, - ) -> PreKeyValIterator { - let prefix = prefix.as_ref(); - let iter_id = unsafe { - anoma_vp_iter_prefix(prefix.as_ptr() as _, prefix.len() as _) - }; - PreKeyValIterator(iter_id, PhantomData) - } - - impl Iterator for PreKeyValIterator { - type Item = (String, T); - - fn next(&mut self) -> Option<(String, T)> { - let read_result = unsafe { anoma_vp_iter_pre_next(self.0) }; - super::read_key_val_from_buffer(read_result, anoma_vp_result_buffer) - } - } - - /// Get an iterator with the given prefix after transaction execution - pub fn iter_prefix_post( - prefix: impl AsRef, - ) -> PostKeyValIterator { - let prefix = prefix.as_ref(); - let iter_id = unsafe { - anoma_vp_iter_prefix(prefix.as_ptr() as _, prefix.len() as _) - }; - PostKeyValIterator(iter_id, PhantomData) - } - - impl Iterator for PostKeyValIterator { - type Item = (String, T); - - fn next(&mut self) -> Option<(String, T)> { - let read_result = unsafe { anoma_vp_iter_post_next(self.0) }; - super::read_key_val_from_buffer(read_result, anoma_vp_result_buffer) - } - } - - /// Get the chain ID - pub fn get_chain_id() -> String { - let result = Vec::with_capacity(CHAIN_ID_LENGTH); - unsafe { - anoma_vp_get_chain_id(result.as_ptr() as _); - } - let slice = - unsafe { slice::from_raw_parts(result.as_ptr(), CHAIN_ID_LENGTH) }; - String::from_utf8(slice.to_vec()).expect("Cannot convert the ID string") - } - - /// Get height of the current block - pub fn get_block_height() -> BlockHeight { - BlockHeight(unsafe { anoma_vp_get_block_height() }) - } - - /// Get a block hash - pub fn get_block_hash() -> BlockHash { - let result = Vec::with_capacity(BLOCK_HASH_LENGTH); - unsafe { - anoma_vp_get_block_hash(result.as_ptr() as _); - } - let slice = unsafe { - slice::from_raw_parts(result.as_ptr(), BLOCK_HASH_LENGTH) - }; - BlockHash::try_from(slice).expect("Cannot convert the hash") - } - - /// Get a tx hash - pub fn get_tx_code_hash() -> Hash { - let result = Vec::with_capacity(HASH_LENGTH); - unsafe { - anoma_vp_get_tx_code_hash(result.as_ptr() as _); - } - let slice = - unsafe { slice::from_raw_parts(result.as_ptr(), HASH_LENGTH) }; - Hash::try_from(slice).expect("Cannot convert the hash") - } - - /// Get epoch of the current block - pub fn get_block_epoch() -> Epoch { - Epoch(unsafe { anoma_vp_get_block_epoch() }) - } - - /// Verify a transaction signature. The signature is expected to have been - /// produced on the encoded transaction [`namada::proto::Tx`] - /// using [`namada::proto::Tx::sign`]. - pub fn verify_tx_signature( - pk: &common::PublicKey, - sig: &common::Signature, - ) -> bool { - let pk = BorshSerialize::try_to_vec(pk).unwrap(); - let sig = BorshSerialize::try_to_vec(sig).unwrap(); - let valid = unsafe { - anoma_vp_verify_tx_signature( - pk.as_ptr() as _, - pk.len() as _, - sig.as_ptr() as _, - sig.len() as _, - ) - }; - HostEnvResult::is_success(valid) - } - - /// Log a string. The message will be printed at the `tracing::Level::Info`. - pub fn log_string>(msg: T) { - let msg = msg.as_ref(); - unsafe { - anoma_vp_log_string(msg.as_ptr() as _, msg.len() as _); - } - } - - /// Evaluate a validity predicate with given data. The address, changed - /// storage keys and verifiers will have the same values as the input to - /// caller's validity predicate. - /// - /// If the execution fails for whatever reason, this will return `false`. - /// Otherwise returns the result of evaluation. - pub fn eval(vp_code: Vec, input_data: Vec) -> bool { - let result = unsafe { - anoma_vp_eval( - vp_code.as_ptr() as _, - vp_code.len() as _, - input_data.as_ptr() as _, - input_data.len() as _, - ) - }; - HostEnvResult::is_success(result) - } - - // These host functions are implemented in the Anoma's [`host_env`] - // module. The environment provides calls to them via this C interface. - extern "C" { - // Read variable-length prior state when we don't know the size - // up-front, returns the size of the value (can be 0), or -1 if - // the key is not present. If a value is found, it will be placed in the - // result buffer, because we cannot allocate a buffer for it before - // we know its size. - fn anoma_vp_read_pre(key_ptr: u64, key_len: u64) -> i64; - - // Read variable-length posterior state when we don't know the size - // up-front, returns the size of the value (can be 0), or -1 if - // the key is not present. If a value is found, it will be placed in the - // result buffer, because we cannot allocate a buffer for it before - // we know its size. - fn anoma_vp_read_post(key_ptr: u64, key_len: u64) -> i64; - - // Read variable-length temporary state when we don't know the size - // up-front, returns the size of the value (can be 0), or -1 if - // the key is not present. If a value is found, it will be placed in the - // result buffer, because we cannot allocate a buffer for it before - // we know its size. - fn anoma_vp_read_temp(key_ptr: u64, key_len: u64) -> i64; - - // Read a value from result buffer. - fn anoma_vp_result_buffer(result_ptr: u64); - - // Returns 1 if the key is present in prior state, -1 otherwise. - fn anoma_vp_has_key_pre(key_ptr: u64, key_len: u64) -> i64; - - // Returns 1 if the key is present in posterior state, -1 otherwise. - fn anoma_vp_has_key_post(key_ptr: u64, key_len: u64) -> i64; - - // Get an ID of a data iterator with key prefix - fn anoma_vp_iter_prefix(prefix_ptr: u64, prefix_len: u64) -> u64; - - // Read variable-length prior state when we don't know the size - // up-front, returns the size of the value (can be 0), or -1 if - // the key is not present. If a value is found, it will be placed in the - // result buffer, because we cannot allocate a buffer for it before - // we know its size. - fn anoma_vp_iter_pre_next(iter_id: u64) -> i64; - - // Read variable-length posterior state when we don't know the size - // up-front, returns the size of the value (can be 0), or -1 if the - // key is not present. If a value is found, it will be placed in the - // result buffer, because we cannot allocate a buffer for it before - // we know its size. - fn anoma_vp_iter_post_next(iter_id: u64) -> i64; - - // Get the chain ID - fn anoma_vp_get_chain_id(result_ptr: u64); - - // Get the current block height - fn anoma_vp_get_block_height() -> u64; - - // Get the current block hash - fn anoma_vp_get_block_hash(result_ptr: u64); - - // Get the current tx hash - fn anoma_vp_get_tx_code_hash(result_ptr: u64); - - // Get the current block epoch - fn anoma_vp_get_block_epoch() -> u64; - - // Verify a transaction signature - fn anoma_vp_verify_tx_signature( - pk_ptr: u64, - pk_len: u64, - sig_ptr: u64, - sig_len: u64, - ) -> i64; - - // Requires a node running with "Info" log level - fn anoma_vp_log_string(str_ptr: u64, str_len: u64); - - fn anoma_vp_eval( - vp_code_ptr: u64, - vp_code_len: u64, - input_data_ptr: u64, - input_data_len: u64, - ) -> i64; - } -} diff --git a/vm_env/src/intent.rs b/vm_env/src/intent.rs deleted file mode 100644 index 226cb708db..0000000000 --- a/vm_env/src/intent.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::collections::HashSet; - -use namada::proto::Signed; -use namada::types::intent; -use namada::types::key::*; - -/// Tx imports and functions. -pub mod tx { - pub use namada::types::intent::*; - - use super::*; - pub fn invalidate_exchange(intent: &Signed) { - use crate::imports::tx; - let key = intent::invalid_intent_key(&intent.data.addr); - let mut invalid_intent: HashSet = - tx::read(&key.to_string()).unwrap_or_default(); - invalid_intent.insert(intent.sig.clone()); - tx::write(&key.to_string(), &invalid_intent) - } -} - -/// Vp imports and functions. -pub mod vp { - pub use namada::types::intent::*; - - use super::*; - - pub fn vp_exchange(intent: &Signed) -> bool { - use crate::imports::vp; - let key = intent::invalid_intent_key(&intent.data.addr); - - let invalid_intent_pre: HashSet = - vp::read_pre(&key.to_string()).unwrap_or_default(); - let invalid_intent_post: HashSet = - vp::read_post(&key.to_string()).unwrap_or_default(); - !invalid_intent_pre.contains(&intent.sig) - && invalid_intent_post.contains(&intent.sig) - } -} diff --git a/vm_env/src/key/ed25519.rs b/vm_env/src/key/ed25519.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/vm_env/src/key/mod.rs b/vm_env/src/key/mod.rs deleted file mode 100644 index 30aea96c46..0000000000 --- a/vm_env/src/key/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -use namada::types::address::Address; - -/// Vp imports and functions. -pub mod vp { - pub use namada::types::key::*; - - use super::*; - use crate::imports::vp; - - /// Get the public key associated with the given address. Panics if not - /// found. - pub fn get(owner: &Address) -> Option { - let key = pk_key(owner).to_string(); - vp::read_pre(&key) - } -} diff --git a/vm_env/src/lib.rs b/vm_env/src/lib.rs index 578079d695..53c594dbab 100644 --- a/vm_env/src/lib.rs +++ b/vm_env/src/lib.rs @@ -1,55 +1,225 @@ -//! This crate contains library code for wasm. Some of the code is re-exported -//! from the `shared` crate. +//! This crate contains the WASM VM low-level interface. #![doc(html_favicon_url = "https://dev.anoma.net/master/favicon.png")] #![doc(html_logo_url = "https://dev.anoma.net/master/rustdoc-logo.png")] #![deny(rustdoc::broken_intra_doc_links)] #![deny(rustdoc::private_intra_doc_links)] -pub mod governance; -pub mod ibc; -pub mod imports; -pub mod intent; -pub mod key; -pub mod nft; -pub mod proof_of_stake; -pub mod token; - -pub mod tx_prelude { - pub use namada::ledger::governance::storage; - pub use namada::ledger::parameters::storage as parameters_storage; - pub use namada::ledger::storage::types::encode; - pub use namada::ledger::treasury::storage as treasury_storage; - pub use namada::proto::{Signed, SignedTxData}; - pub use namada::types::address::Address; - pub use namada::types::storage::Key; - pub use namada::types::*; - pub use namada_macros::transaction; - - pub use crate::governance::tx as governance; - pub use crate::ibc::{Ibc, IbcActions}; - pub use crate::imports::tx::*; - pub use crate::intent::tx as intent; - pub use crate::nft::tx as nft; - pub use crate::proof_of_stake::{self, PoS, PosRead, PosWrite}; - pub use crate::token::tx as token; +use std::mem::ManuallyDrop; + +use borsh::BorshDeserialize; +use namada::types::internal::HostEnvResult; +use namada::vm::types::KeyVal; + +/// Transaction environment imports +pub mod tx { + // These host functions are implemented in the Anoma's [`host_env`] + // module. The environment provides calls to them via this C interface. + extern "C" { + // Read variable-length data when we don't know the size up-front, + // returns the size of the value (can be 0), or -1 if the key is + // not present. If a value is found, it will be placed in the read + // cache, because we cannot allocate a buffer for it before we know + // its size. + pub fn anoma_tx_read(key_ptr: u64, key_len: u64) -> i64; + + // Read a value from result buffer. + pub fn anoma_tx_result_buffer(result_ptr: u64); + + // Returns 1 if the key is present, -1 otherwise. + pub fn anoma_tx_has_key(key_ptr: u64, key_len: u64) -> i64; + + // Write key/value + pub fn anoma_tx_write( + key_ptr: u64, + key_len: u64, + val_ptr: u64, + val_len: u64, + ); + + // Write a temporary key/value + pub fn anoma_tx_write_temp( + key_ptr: u64, + key_len: u64, + val_ptr: u64, + val_len: u64, + ); + + // Delete the given key and its value + pub fn anoma_tx_delete(key_ptr: u64, key_len: u64); + + // Get an ID of a data iterator with key prefix + pub fn anoma_tx_iter_prefix(prefix_ptr: u64, prefix_len: u64) -> u64; + + // Returns the size of the value (can be 0), or -1 if there's no next + // value. If a value is found, it will be placed in the read + // cache, because we cannot allocate a buffer for it before we know + // its size. + pub fn anoma_tx_iter_next(iter_id: u64) -> i64; + + // Insert a verifier + pub fn anoma_tx_insert_verifier(addr_ptr: u64, addr_len: u64); + + // Update a validity predicate + pub fn anoma_tx_update_validity_predicate( + addr_ptr: u64, + addr_len: u64, + code_ptr: u64, + code_len: u64, + ); + + // Initialize a new account + pub fn anoma_tx_init_account( + code_ptr: u64, + code_len: u64, + result_ptr: u64, + ); + + // Emit an IBC event + pub fn anoma_tx_emit_ibc_event(event_ptr: u64, event_len: u64); + + // Get the chain ID + pub fn anoma_tx_get_chain_id(result_ptr: u64); + + // Get the current block height + pub fn anoma_tx_get_block_height() -> u64; + + // Get the time of the current block header + pub fn anoma_tx_get_block_time() -> i64; + + // Get the current block hash + pub fn anoma_tx_get_block_hash(result_ptr: u64); + + // Get the current block epoch + pub fn anoma_tx_get_block_epoch() -> u64; + + // Requires a node running with "Info" log level + pub fn anoma_tx_log_string(str_ptr: u64, str_len: u64); + } +} + +/// Validity predicate environment imports +pub mod vp { + // These host functions are implemented in the Anoma's [`host_env`] + // module. The environment provides calls to them via this C interface. + extern "C" { + // Read variable-length prior state when we don't know the size + // up-front, returns the size of the value (can be 0), or -1 if + // the key is not present. If a value is found, it will be placed in the + // result buffer, because we cannot allocate a buffer for it before + // we know its size. + pub fn anoma_vp_read_pre(key_ptr: u64, key_len: u64) -> i64; + + // Read variable-length posterior state when we don't know the size + // up-front, returns the size of the value (can be 0), or -1 if + // the key is not present. If a value is found, it will be placed in the + // result buffer, because we cannot allocate a buffer for it before + // we know its size. + pub fn anoma_vp_read_post(key_ptr: u64, key_len: u64) -> i64; + + // Read variable-length temporary state when we don't know the size + // up-front, returns the size of the value (can be 0), or -1 if + // the key is not present. If a value is found, it will be placed in the + // result buffer, because we cannot allocate a buffer for it before + // we know its size. + pub fn anoma_vp_read_temp(key_ptr: u64, key_len: u64) -> i64; + + // Read a value from result buffer. + pub fn anoma_vp_result_buffer(result_ptr: u64); + + // Returns 1 if the key is present in prior state, -1 otherwise. + pub fn anoma_vp_has_key_pre(key_ptr: u64, key_len: u64) -> i64; + + // Returns 1 if the key is present in posterior state, -1 otherwise. + pub fn anoma_vp_has_key_post(key_ptr: u64, key_len: u64) -> i64; + + // Get an ID of a data iterator with key prefix + pub fn anoma_vp_iter_prefix(prefix_ptr: u64, prefix_len: u64) -> u64; + + // Read variable-length prior state when we don't know the size + // up-front, returns the size of the value (can be 0), or -1 if + // the key is not present. If a value is found, it will be placed in the + // result buffer, because we cannot allocate a buffer for it before + // we know its size. + pub fn anoma_vp_iter_pre_next(iter_id: u64) -> i64; + + // Read variable-length posterior state when we don't know the size + // up-front, returns the size of the value (can be 0), or -1 if the + // key is not present. If a value is found, it will be placed in the + // result buffer, because we cannot allocate a buffer for it before + // we know its size. + pub fn anoma_vp_iter_post_next(iter_id: u64) -> i64; + + // Get the chain ID + pub fn anoma_vp_get_chain_id(result_ptr: u64); + + // Get the current block height + pub fn anoma_vp_get_block_height() -> u64; + + // Get the current block hash + pub fn anoma_vp_get_block_hash(result_ptr: u64); + + // Get the current tx hash + pub fn anoma_vp_get_tx_code_hash(result_ptr: u64); + + // Get the current block epoch + pub fn anoma_vp_get_block_epoch() -> u64; + + // Verify a transaction signature + pub fn anoma_vp_verify_tx_signature( + pk_ptr: u64, + pk_len: u64, + sig_ptr: u64, + sig_len: u64, + ) -> i64; + + // Requires a node running with "Info" log level + pub fn anoma_vp_log_string(str_ptr: u64, str_len: u64); + + pub fn anoma_vp_eval( + vp_code_ptr: u64, + vp_code_len: u64, + input_data_ptr: u64, + input_data_len: u64, + ) -> i64; + } +} + +/// This function is a helper to handle the second step of reading var-len +/// values from the host. +/// +/// In cases where we're reading a value from the host in the guest and +/// we don't know the byte size up-front, we have to read it in 2-steps. The +/// first step reads the value into a result buffer and returns the size (if +/// any) back to the guest, the second step reads the value from cache into a +/// pre-allocated buffer with the obtained size. +pub fn read_from_buffer( + read_result: i64, + result_buffer: unsafe extern "C" fn(u64), +) -> Option> { + if HostEnvResult::is_fail(read_result) { + None + } else { + let result: Vec = Vec::with_capacity(read_result as _); + // The `result` will be dropped from the `target`, which is + // reconstructed from the same memory + let result = ManuallyDrop::new(result); + let offset = result.as_slice().as_ptr() as u64; + unsafe { result_buffer(offset) }; + let target = unsafe { + Vec::from_raw_parts(offset as _, read_result as _, read_result as _) + }; + Some(target) + } } -pub mod vp_prelude { - // used in the VP input - pub use std::collections::{BTreeSet, HashSet}; - - pub use namada::ledger::governance::storage as gov_storage; - pub use namada::ledger::{parameters, pos as proof_of_stake}; - pub use namada::proto::{Signed, SignedTxData}; - pub use namada::types::address::Address; - pub use namada::types::storage::Key; - pub use namada::types::*; - pub use namada_macros::validity_predicate; - - pub use crate::imports::vp::*; - pub use crate::intent::vp as intent; - pub use crate::key::vp as key; - pub use crate::nft::vp as nft; - pub use crate::token::vp as token; +/// This function is a helper to handle the second step of reading var-len +/// values in a key-value pair from the host. +pub fn read_key_val_bytes_from_buffer( + read_result: i64, + result_buffer: unsafe extern "C" fn(u64), +) -> Option<(String, Vec)> { + let key_val = read_from_buffer(read_result, result_buffer) + .and_then(|t| KeyVal::try_from_slice(&t[..]).ok()); + key_val.map(|key_val| (key_val.key, key_val.val)) } diff --git a/vm_env/src/nft.rs b/vm_env/src/nft.rs deleted file mode 100644 index 4a685acd72..0000000000 --- a/vm_env/src/nft.rs +++ /dev/null @@ -1,194 +0,0 @@ -use namada::types::nft; - -/// Tx imports and functions. -pub mod tx { - use namada::types::address::Address; - use namada::types::nft::NftToken; - use namada::types::transaction::nft::{CreateNft, MintNft}; - - use super::*; - use crate::imports::tx; - pub fn init_nft(nft: CreateNft) -> Address { - let address = tx::init_account(&nft.vp_code); - - // write tag - let tag_key = nft::get_tag_key(&address); - tx::write(&tag_key.to_string(), &nft.tag); - - // write creator - let creator_key = nft::get_creator_key(&address); - tx::write(&creator_key.to_string(), &nft.creator); - - // write keys - let keys_key = nft::get_keys_key(&address); - tx::write(&keys_key.to_string(), &nft.keys); - - // write optional keys - let optional_keys_key = nft::get_optional_keys_key(&address); - tx::write(&optional_keys_key.to_string(), nft.opt_keys); - - // mint tokens - aux_mint_token(&address, &nft.creator, nft.tokens, &nft.creator); - - tx::insert_verifier(&nft.creator); - - address - } - - pub fn mint_tokens(nft: MintNft) { - aux_mint_token(&nft.address, &nft.creator, nft.tokens, &nft.creator); - } - - fn aux_mint_token( - nft_address: &Address, - creator_address: &Address, - tokens: Vec, - verifier: &Address, - ) { - for token in tokens { - // write token metadata - let metadata_key = - nft::get_token_metadata_key(nft_address, &token.id.to_string()); - tx::write(&metadata_key.to_string(), &token.metadata); - - // write current owner token as creator - let current_owner_key = nft::get_token_current_owner_key( - nft_address, - &token.id.to_string(), - ); - tx::write( - ¤t_owner_key.to_string(), - &token - .current_owner - .unwrap_or_else(|| creator_address.clone()), - ); - - // write value key - let value_key = - nft::get_token_value_key(nft_address, &token.id.to_string()); - tx::write(&value_key.to_string(), &token.values); - - // write optional value keys - let optional_value_key = nft::get_token_optional_value_key( - nft_address, - &token.id.to_string(), - ); - tx::write(&optional_value_key.to_string(), &token.opt_values); - - // write approval addresses - let approval_key = - nft::get_token_approval_key(nft_address, &token.id.to_string()); - tx::write(&approval_key.to_string(), &token.approvals); - - // write burnt propriety - let burnt_key = - nft::get_token_burnt_key(nft_address, &token.id.to_string()); - tx::write(&burnt_key.to_string(), token.burnt); - } - tx::insert_verifier(verifier); - } -} - -/// A Nft validity predicate -pub mod vp { - use std::collections::BTreeSet; - - use namada::types::address::Address; - pub use namada::types::nft::*; - use namada::types::storage::Key; - - use crate::imports::vp; - - enum KeyType { - Metadata(Address, String), - Approval(Address, String), - CurrentOwner(Address, String), - Creator(Address), - PastOwners(Address, String), - Unknown, - } - - pub fn vp( - _tx_da_ta: Vec, - nft_address: &Address, - keys_changed: &BTreeSet, - verifiers: &BTreeSet
, - ) -> bool { - keys_changed - .iter() - .all(|key| match get_key_type(key, nft_address) { - KeyType::Creator(_creator_addr) => { - vp::log_string("creator cannot be changed."); - false - } - KeyType::Approval(nft_address, token_id) => { - vp::log_string(format!( - "nft vp, checking approvals with token id: {}", - token_id - )); - - is_creator(&nft_address, verifiers) - || is_approved( - &nft_address, - token_id.as_ref(), - verifiers, - ) - } - KeyType::Metadata(nft_address, token_id) => { - vp::log_string(format!( - "nft vp, checking if metadata changed: {}", - token_id - )); - is_creator(&nft_address, verifiers) - } - _ => is_creator(nft_address, verifiers), - }) - } - - fn is_approved( - nft_address: &Address, - nft_token_id: &str, - verifiers: &BTreeSet
, - ) -> bool { - let approvals_key = - get_token_approval_key(nft_address, nft_token_id).to_string(); - let approval_addresses: Vec
= - vp::read_pre(approvals_key).unwrap_or_default(); - return approval_addresses - .iter() - .any(|addr| verifiers.contains(addr)); - } - - fn is_creator( - nft_address: &Address, - verifiers: &BTreeSet
, - ) -> bool { - let creator_key = get_creator_key(nft_address).to_string(); - let creator_address: Address = vp::read_pre(creator_key).unwrap(); - verifiers.contains(&creator_address) - } - - fn get_key_type(key: &Key, nft_address: &Address) -> KeyType { - let is_creator_key = is_nft_creator_key(key, nft_address); - let is_metadata_key = is_nft_metadata_key(key, nft_address); - let is_approval_key = is_nft_approval_key(key, nft_address); - let is_current_owner_key = is_nft_current_owner_key(key, nft_address); - let is_past_owner_key = is_nft_past_owners_key(key, nft_address); - if let Some(nft_address) = is_creator_key { - return KeyType::Creator(nft_address); - } - if let Some((nft_address, token_id)) = is_metadata_key { - return KeyType::Metadata(nft_address, token_id); - } - if let Some((nft_address, token_id)) = is_approval_key { - return KeyType::Approval(nft_address, token_id); - } - if let Some((nft_address, token_id)) = is_current_owner_key { - return KeyType::CurrentOwner(nft_address, token_id); - } - if let Some((nft_address, token_id)) = is_past_owner_key { - return KeyType::PastOwners(nft_address, token_id); - } - KeyType::Unknown - } -} diff --git a/vm_env/src/proof_of_stake.rs b/vm_env/src/proof_of_stake.rs deleted file mode 100644 index 8e4bba4223..0000000000 --- a/vm_env/src/proof_of_stake.rs +++ /dev/null @@ -1,261 +0,0 @@ -//! Proof of Stake system integration with functions for transactions - -use namada::ledger::pos::namada_proof_of_stake::{ - BecomeValidatorError, BondError, UnbondError, WithdrawError, -}; -use namada::ledger::pos::types::Slash; -pub use namada::ledger::pos::*; -use namada::ledger::pos::{ - bond_key, namada_proof_of_stake, params_key, total_voting_power_key, - unbond_key, validator_address_raw_hash_key, validator_consensus_key_key, - validator_set_key, validator_slashes_key, - validator_staking_reward_address_key, validator_state_key, - validator_total_deltas_key, validator_voting_power_key, -}; -use namada::types::address::{self, Address, InternalAddress}; -use namada::types::transaction::InitValidator; -use namada::types::{key, token}; -pub use namada_proof_of_stake::{ - epoched, parameters, types, PosActions as PosWrite, PosReadOnly as PosRead, -}; - -use crate::imports::tx; - -/// Self-bond tokens to a validator when `source` is `None` or equal to -/// the `validator` address, or delegate tokens from the `source` to the -/// `validator`. -pub fn bond_tokens( - source: Option<&Address>, - validator: &Address, - amount: token::Amount, -) -> Result<(), BondError
> { - let current_epoch = tx::get_block_epoch(); - PoS.bond_tokens(source, validator, amount, current_epoch) -} - -/// Unbond self-bonded tokens from a validator when `source` is `None` or -/// equal to the `validator` address, or unbond delegated tokens from -/// the `source` to the `validator`. -pub fn unbond_tokens( - source: Option<&Address>, - validator: &Address, - amount: token::Amount, -) -> Result<(), UnbondError> { - let current_epoch = tx::get_block_epoch(); - PoS.unbond_tokens(source, validator, amount, current_epoch) -} - -/// Withdraw unbonded tokens from a self-bond to a validator when `source` -/// is `None` or equal to the `validator` address, or withdraw unbonded -/// tokens delegated to the `validator` to the `source`. -pub fn withdraw_tokens( - source: Option<&Address>, - validator: &Address, -) -> Result> { - let current_epoch = tx::get_block_epoch(); - PoS.withdraw_tokens(source, validator, current_epoch) -} - -/// Attempt to initialize a validator account. On success, returns the -/// initialized validator account's address and its staking reward address. -pub fn init_validator( - InitValidator { - account_key, - consensus_key, - rewards_account_key, - protocol_key, - dkg_key, - validator_vp_code, - rewards_vp_code, - }: InitValidator, -) -> Result<(Address, Address), BecomeValidatorError
> { - let current_epoch = tx::get_block_epoch(); - // Init validator account - let validator_address = tx::init_account(&validator_vp_code); - let pk_key = key::pk_key(&validator_address); - tx::write(&pk_key.to_string(), &account_key); - let protocol_pk_key = key::protocol_pk_key(&validator_address); - tx::write(&protocol_pk_key.to_string(), &protocol_key); - let dkg_pk_key = key::dkg_session_keys::dkg_pk_key(&validator_address); - tx::write(&dkg_pk_key.to_string(), &dkg_key); - - // Init staking reward account - let rewards_address = tx::init_account(&rewards_vp_code); - let pk_key = key::pk_key(&rewards_address); - tx::write(&pk_key.to_string(), &rewards_account_key); - - PoS.become_validator( - &validator_address, - &rewards_address, - &consensus_key, - current_epoch, - )?; - Ok((validator_address, rewards_address)) -} - -/// Proof of Stake system. This struct integrates and gives access to -/// lower-level PoS functions. -pub struct PoS; - -impl namada_proof_of_stake::PosReadOnly for PoS { - type Address = Address; - type PublicKey = key::common::PublicKey; - type TokenAmount = token::Amount; - type TokenChange = token::Change; - - const POS_ADDRESS: Self::Address = Address::Internal(InternalAddress::PoS); - - fn staking_token_address() -> Self::Address { - address::xan() - } - - fn read_pos_params(&self) -> PosParams { - tx::read(params_key().to_string()).unwrap() - } - - fn read_validator_staking_reward_address( - &self, - key: &Self::Address, - ) -> Option { - tx::read(validator_staking_reward_address_key(key).to_string()) - } - - fn read_validator_consensus_key( - &self, - key: &Self::Address, - ) -> Option { - tx::read(validator_consensus_key_key(key).to_string()) - } - - fn read_validator_state( - &self, - key: &Self::Address, - ) -> Option { - tx::read(validator_state_key(key).to_string()) - } - - fn read_validator_total_deltas( - &self, - key: &Self::Address, - ) -> Option { - tx::read(validator_total_deltas_key(key).to_string()) - } - - fn read_validator_voting_power( - &self, - key: &Self::Address, - ) -> Option { - tx::read(validator_voting_power_key(key).to_string()) - } - - fn read_validator_slashes(&self, key: &Self::Address) -> Vec { - tx::read(validator_slashes_key(key).to_string()).unwrap_or_default() - } - - fn read_bond(&self, key: &BondId) -> Option { - tx::read(bond_key(key).to_string()) - } - - fn read_unbond(&self, key: &BondId) -> Option { - tx::read(unbond_key(key).to_string()) - } - - fn read_validator_set(&self) -> ValidatorSets { - tx::read(validator_set_key().to_string()).unwrap() - } - - fn read_total_voting_power(&self) -> TotalVotingPowers { - tx::read(total_voting_power_key().to_string()).unwrap() - } -} - -impl namada_proof_of_stake::PosActions for PoS { - fn write_pos_params(&mut self, params: &PosParams) { - tx::write(params_key().to_string(), params) - } - - fn write_validator_address_raw_hash(&mut self, address: &Self::Address) { - let raw_hash = address.raw_hash().unwrap().to_owned(); - tx::write( - validator_address_raw_hash_key(raw_hash).to_string(), - address, - ) - } - - fn write_validator_staking_reward_address( - &mut self, - key: &Self::Address, - value: Self::Address, - ) { - tx::write( - validator_staking_reward_address_key(key).to_string(), - &value, - ) - } - - fn write_validator_consensus_key( - &mut self, - key: &Self::Address, - value: ValidatorConsensusKeys, - ) { - tx::write(validator_consensus_key_key(key).to_string(), &value) - } - - fn write_validator_state( - &mut self, - key: &Self::Address, - value: ValidatorStates, - ) { - tx::write(validator_state_key(key).to_string(), &value) - } - - fn write_validator_total_deltas( - &mut self, - key: &Self::Address, - value: ValidatorTotalDeltas, - ) { - tx::write(validator_total_deltas_key(key).to_string(), &value) - } - - fn write_validator_voting_power( - &mut self, - key: &Self::Address, - value: ValidatorVotingPowers, - ) { - tx::write(validator_voting_power_key(key).to_string(), &value) - } - - fn write_bond(&mut self, key: &BondId, value: Bonds) { - tx::write(bond_key(key).to_string(), &value) - } - - fn write_unbond(&mut self, key: &BondId, value: Unbonds) { - tx::write(unbond_key(key).to_string(), &value) - } - - fn write_validator_set(&mut self, value: ValidatorSets) { - tx::write(validator_set_key().to_string(), &value) - } - - fn write_total_voting_power(&mut self, value: TotalVotingPowers) { - tx::write(total_voting_power_key().to_string(), &value) - } - - fn delete_bond(&mut self, key: &BondId) { - tx::delete(bond_key(key).to_string()) - } - - fn delete_unbond(&mut self, key: &BondId) { - tx::delete(unbond_key(key).to_string()) - } - - fn transfer( - &mut self, - token: &Self::Address, - amount: Self::TokenAmount, - src: &Self::Address, - dest: &Self::Address, - ) { - crate::token::tx::transfer(src, dest, token, amount) - } -} diff --git a/vm_env/src/token.rs b/vm_env/src/token.rs deleted file mode 100644 index 8a7367afb9..0000000000 --- a/vm_env/src/token.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::collections::BTreeSet; - -use namada::types::address::{Address, InternalAddress}; -use namada::types::storage::Key; -use namada::types::token; - -/// Vp imports and functions. -pub mod vp { - use namada::types::storage::KeySeg; - pub use namada::types::token::*; - - use super::*; - use crate::imports::vp; - - /// A token validity predicate. - pub fn vp( - token: &Address, - keys_changed: &BTreeSet, - verifiers: &BTreeSet
, - ) -> bool { - let mut change: Change = 0; - let all_checked = keys_changed.iter().all(|key| { - match token::is_balance_key(token, key) { - None => { - // Unknown changes to this address space are disallowed, but - // unknown changes anywhere else are permitted - key.segments.get(0) != Some(&token.to_db_key()) - } - Some(owner) => { - // accumulate the change - let key = key.to_string(); - let pre: Amount = match owner { - Address::Internal(InternalAddress::IbcMint) => { - Amount::max() - } - Address::Internal(InternalAddress::IbcBurn) => { - Amount::default() - } - _ => vp::read_pre(&key).unwrap_or_default(), - }; - let post: Amount = match owner { - Address::Internal(InternalAddress::IbcMint) => { - vp::read_temp(&key).unwrap_or_else(Amount::max) - } - Address::Internal(InternalAddress::IbcBurn) => { - vp::read_temp(&key).unwrap_or_default() - } - _ => vp::read_post(&key).unwrap_or_default(), - }; - let this_change = post.change() - pre.change(); - change += this_change; - // make sure that the spender approved the transaction - if this_change < 0 { - return verifiers.contains(owner); - } - true - } - } - }); - all_checked && change == 0 - } -} - -/// Tx imports and functions. -pub mod tx { - pub use namada::types::token::*; - - use super::*; - use crate::imports::tx; - - /// A token transfer that can be used in a transaction. - pub fn transfer( - src: &Address, - dest: &Address, - token: &Address, - amount: Amount, - ) { - let src_key = token::balance_key(token, src); - let dest_key = token::balance_key(token, dest); - let src_bal: Option = tx::read(&src_key.to_string()); - let mut src_bal = src_bal.unwrap_or_else(|| match src { - Address::Internal(InternalAddress::IbcMint) => Amount::max(), - _ => { - tx::log_string(format!("src {} has no balance", src)); - unreachable!() - } - }); - src_bal.spend(&amount); - let mut dest_bal: Amount = - tx::read(&dest_key.to_string()).unwrap_or_default(); - dest_bal.receive(&amount); - match src { - Address::Internal(InternalAddress::IbcMint) => { - tx::write_temp(&src_key.to_string(), src_bal) - } - Address::Internal(InternalAddress::IbcBurn) => { - tx::log_string("invalid transfer from the burn address"); - unreachable!() - } - _ => tx::write(&src_key.to_string(), src_bal), - } - match dest { - Address::Internal(InternalAddress::IbcMint) => { - tx::log_string("invalid transfer to the mint address"); - unreachable!() - } - Address::Internal(InternalAddress::IbcBurn) => { - tx::write_temp(&dest_key.to_string(), dest_bal) - } - _ => tx::write(&dest_key.to_string(), dest_bal), - } - } -} diff --git a/vp_prelude/Cargo.toml b/vp_prelude/Cargo.toml index f59c5ed032..a36f998b83 100644 --- a/vp_prelude/Cargo.toml +++ b/vp_prelude/Cargo.toml @@ -10,5 +10,9 @@ version = "0.7.0" default = [] [dependencies] +namada = {path = "../shared"} namada_vm_env = {path = "../vm_env"} +namada_macros = {path = "../macros"} +borsh = "0.9.0" sha2 = "0.10.1" +thiserror = "1.0.30" diff --git a/vp_prelude/src/error.rs b/vp_prelude/src/error.rs new file mode 100644 index 0000000000..b34d954166 --- /dev/null +++ b/vp_prelude/src/error.rs @@ -0,0 +1,103 @@ +//! Helpers for error handling in WASM +//! +//! This module is currently duplicated in tx_prelude and vp_prelude crates to +//! be able to implement `From` conversion on error types from other crates, +//! avoiding `error[E0117]: only traits defined in the current crate can be +//! implemented for arbitrary types` + +use thiserror::Error; + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + SimpleMessage(&'static str), + #[error("{0}")] + Custom(CustomError), + #[error("{0}: {1}")] + CustomWithMessage(&'static str, CustomError), +} + +/// Result of transaction or VP. +pub type EnvResult = Result; + +pub trait ResultExt { + /// Replace a possible error with a static message in [`EnvResult`]. + fn err_msg(self, msg: &'static str) -> EnvResult; +} + +// This is separate from `ResultExt`, because the implementation requires +// different bounds for `T`. +pub trait ResultExt2 { + /// Convert a [`Result`] into [`EnvResult`]. + fn into_env_result(self) -> EnvResult; + + /// Add a static message to a possible error in [`EnvResult`]. + fn wrap_err(self, msg: &'static str) -> EnvResult; +} + +pub trait OptionExt { + /// Transforms the [`Option`] into a [`EnvResult`], mapping + /// [`Some(v)`] to [`Ok(v)`] and [`None`] to the given static error + /// message. + fn ok_or_err_msg(self, msg: &'static str) -> EnvResult; +} + +impl ResultExt for Result { + fn err_msg(self, msg: &'static str) -> EnvResult { + self.map_err(|_err| Error::new_const(msg)) + } +} + +impl ResultExt2 for Result +where + E: std::error::Error + Send + Sync + 'static, +{ + fn into_env_result(self) -> EnvResult { + self.map_err(Error::new) + } + + fn wrap_err(self, msg: &'static str) -> EnvResult { + self.map_err(|err| Error::wrap(msg, err)) + } +} + +impl OptionExt for Option { + fn ok_or_err_msg(self, msg: &'static str) -> EnvResult { + self.ok_or_else(|| Error::new_const(msg)) + } +} + +impl Error { + /// Create an [`enum@Error`] from a static message. + #[inline] + pub const fn new_const(msg: &'static str) -> Self { + Self::SimpleMessage(msg) + } + + /// Create an [`enum@Error`] from another [`std::error::Error`]. + pub fn new(error: E) -> Self + where + E: Into>, + { + Self::Custom(CustomError(error.into())) + } + + /// Wrap another [`std::error::Error`] with a static message. + pub fn wrap(msg: &'static str, error: E) -> Self + where + E: Into>, + { + Self::CustomWithMessage(msg, CustomError(error.into())) + } +} + +/// A custom error +#[derive(Debug)] +pub struct CustomError(Box); + +impl std::fmt::Display for CustomError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} diff --git a/vp_prelude/src/intent.rs b/vp_prelude/src/intent.rs new file mode 100644 index 0000000000..93a93e1183 --- /dev/null +++ b/vp_prelude/src/intent.rs @@ -0,0 +1,19 @@ +use std::collections::HashSet; + +use namada::proto::Signed; +use namada::types::intent; +pub use namada::types::intent::*; +use namada::types::key::*; + +use super::*; + +pub fn vp_exchange(ctx: &Ctx, intent: &Signed) -> EnvResult { + let key = intent::invalid_intent_key(&intent.data.addr); + + let invalid_intent_pre: HashSet = + ctx.read_pre(&key)?.unwrap_or_default(); + let invalid_intent_post: HashSet = + ctx.read_post(&key)?.unwrap_or_default(); + Ok(!invalid_intent_pre.contains(&intent.sig) + && invalid_intent_post.contains(&intent.sig)) +} diff --git a/vp_prelude/src/key.rs b/vp_prelude/src/key.rs new file mode 100644 index 0000000000..5ef2a5e28c --- /dev/null +++ b/vp_prelude/src/key.rs @@ -0,0 +1,13 @@ +//! Cryptographic signature keys + +use namada::types::address::Address; +pub use namada::types::key::*; + +use super::*; + +/// Get the public key associated with the given address. Panics if not +/// found. +pub fn get(ctx: &Ctx, owner: &Address) -> EnvResult> { + let key = pk_key(owner); + ctx.read_pre(&key) +} diff --git a/vp_prelude/src/lib.rs b/vp_prelude/src/lib.rs index 957354d848..c1be4465f1 100644 --- a/vp_prelude/src/lib.rs +++ b/vp_prelude/src/lib.rs @@ -6,10 +6,37 @@ #![deny(rustdoc::broken_intra_doc_links)] #![deny(rustdoc::private_intra_doc_links)] +mod error; +pub mod intent; +pub mod key; +pub mod nft; +pub mod token; + +// used in the VP input use core::convert::AsRef; +use core::slice; +pub use std::collections::{BTreeSet, HashSet}; +use std::convert::TryFrom; +use std::marker::PhantomData; -use namada_vm_env::vp_prelude::hash::Hash; -pub use namada_vm_env::vp_prelude::*; +pub use borsh::{BorshDeserialize, BorshSerialize}; +pub use error::*; +pub use namada::ledger::governance::storage as gov_storage; +pub use namada::ledger::vp_env::VpEnv; +pub use namada::ledger::{parameters, pos as proof_of_stake}; +pub use namada::proto::{Signed, SignedTxData}; +pub use namada::types::address::Address; +use namada::types::chain::CHAIN_ID_LENGTH; +use namada::types::hash::{Hash, HASH_LENGTH}; +use namada::types::internal::HostEnvResult; +use namada::types::key::*; +use namada::types::storage::{ + BlockHash, BlockHeight, Epoch, BLOCK_HASH_LENGTH, +}; +pub use namada::types::*; +pub use namada_macros::validity_predicate; +use namada_vm_env::vp::*; +use namada_vm_env::{read_from_buffer, read_key_val_bytes_from_buffer}; pub use sha2::{Digest, Sha256, Sha384, Sha512}; pub fn sha256(bytes: &[u8]) -> Hash { @@ -17,20 +44,28 @@ pub fn sha256(bytes: &[u8]) -> Hash { Hash(*digest.as_ref()) } -pub fn is_tx_whitelisted() -> bool { - let tx_hash = get_tx_code_hash(); +pub fn is_tx_whitelisted(ctx: &Ctx) -> VpResult { + let tx_hash = ctx.get_tx_code_hash()?; let key = parameters::storage::get_tx_whitelist_storage_key(); - let whitelist: Vec = read_pre(&key.to_string()).unwrap_or_default(); + let whitelist: Vec = ctx.read_pre(&key)?.unwrap_or_default(); // if whitelist is empty, allow any transaction - whitelist.is_empty() || whitelist.contains(&tx_hash.to_string()) + Ok(whitelist.is_empty() || whitelist.contains(&tx_hash.to_string())) } -pub fn is_vp_whitelisted(vp_bytes: &[u8]) -> bool { +pub fn is_vp_whitelisted(ctx: &Ctx, vp_bytes: &[u8]) -> VpResult { let vp_hash = sha256(vp_bytes); let key = parameters::storage::get_vp_whitelist_storage_key(); - let whitelist: Vec = read_pre(&key.to_string()).unwrap_or_default(); + let whitelist: Vec = ctx.read_pre(&key)?.unwrap_or_default(); // if whitelist is empty, allow any transaction - whitelist.is_empty() || whitelist.contains(&vp_hash.to_string()) + Ok(whitelist.is_empty() || whitelist.contains(&vp_hash.to_string())) +} + +/// Log a string. The message will be printed at the `tracing::Level::Info`. +pub fn log_string>(msg: T) { + let msg = msg.as_ref(); + unsafe { + anoma_vp_log_string(msg.as_ptr() as _, msg.len() as _); + } } /// Log a string in a debug build. The message will be printed at the @@ -44,3 +79,234 @@ macro_rules! debug_log { (if cfg!(debug_assertions) { log_string(format!($($arg)*)) }) }} } + +pub struct Ctx(()); + +impl Ctx { + /// Create a host context. The context on WASM side is only provided by + /// the VM once its being executed (in here it's implicit). But + /// because we want to have interface identical with the native + /// VPs, in which the context is explicit, in here we're just + /// using an empty `Ctx` to "fake" it. + /// + /// # Safety + /// + /// When using `#[validity_predicate]` macro from `anoma_macros`, + /// the constructor should not be called from transactions and validity + /// predicates implementation directly - they receive `&Self` as + /// an argument provided by the macro that wrap the low-level WASM + /// interface with Rust native types. + /// + /// Otherwise, this should only be called once to initialize this "fake" + /// context in order to benefit from type-safety of the host environment + /// methods implemented on the context. + #[allow(clippy::new_without_default)] + pub const unsafe fn new() -> Self { + Self(()) + } +} + +/// Validity predicate result +pub type VpResult = EnvResult; + +/// Accept a transaction +pub fn accept() -> VpResult { + Ok(true) +} + +/// Reject a transaction +pub fn reject() -> VpResult { + Ok(false) +} + +#[derive(Debug)] +pub struct KeyValIterator(pub u64, pub PhantomData); + +impl VpEnv for Ctx { + type Error = Error; + type PrefixIter = KeyValIterator<(String, Vec)>; + + fn read_pre( + &self, + key: &storage::Key, + ) -> Result, Self::Error> { + let key = key.to_string(); + let read_result = + unsafe { anoma_vp_read_pre(key.as_ptr() as _, key.len() as _) }; + Ok(read_from_buffer(read_result, anoma_vp_result_buffer) + .and_then(|bytes| T::try_from_slice(&bytes[..]).ok())) + } + + fn read_bytes_pre( + &self, + key: &storage::Key, + ) -> Result>, Self::Error> { + let key = key.to_string(); + let read_result = + unsafe { anoma_vp_read_pre(key.as_ptr() as _, key.len() as _) }; + Ok(read_from_buffer(read_result, anoma_vp_result_buffer)) + } + + fn read_post( + &self, + key: &storage::Key, + ) -> Result, Self::Error> { + let key = key.to_string(); + let read_result = + unsafe { anoma_vp_read_post(key.as_ptr() as _, key.len() as _) }; + Ok(read_from_buffer(read_result, anoma_vp_result_buffer) + .and_then(|bytes| T::try_from_slice(&bytes[..]).ok())) + } + + fn read_bytes_post( + &self, + key: &storage::Key, + ) -> Result>, Self::Error> { + let key = key.to_string(); + let read_result = + unsafe { anoma_vp_read_post(key.as_ptr() as _, key.len() as _) }; + Ok(read_from_buffer(read_result, anoma_vp_result_buffer)) + } + + fn read_temp( + &self, + key: &storage::Key, + ) -> Result, Self::Error> { + let key = key.to_string(); + let read_result = + unsafe { anoma_vp_read_temp(key.as_ptr() as _, key.len() as _) }; + Ok(read_from_buffer(read_result, anoma_vp_result_buffer) + .and_then(|t| T::try_from_slice(&t[..]).ok())) + } + + fn read_bytes_temp( + &self, + key: &storage::Key, + ) -> Result>, Self::Error> { + let key = key.to_string(); + let read_result = + unsafe { anoma_vp_read_temp(key.as_ptr() as _, key.len() as _) }; + Ok(read_from_buffer(read_result, anoma_vp_result_buffer)) + } + + fn has_key_pre(&self, key: &storage::Key) -> Result { + let key = key.to_string(); + let found = + unsafe { anoma_vp_has_key_pre(key.as_ptr() as _, key.len() as _) }; + Ok(HostEnvResult::is_success(found)) + } + + fn has_key_post(&self, key: &storage::Key) -> Result { + let key = key.to_string(); + let found = + unsafe { anoma_vp_has_key_post(key.as_ptr() as _, key.len() as _) }; + Ok(HostEnvResult::is_success(found)) + } + + fn get_chain_id(&self) -> Result { + let result = Vec::with_capacity(CHAIN_ID_LENGTH); + unsafe { + anoma_vp_get_chain_id(result.as_ptr() as _); + } + let slice = + unsafe { slice::from_raw_parts(result.as_ptr(), CHAIN_ID_LENGTH) }; + Ok(String::from_utf8(slice.to_vec()) + .expect("Cannot convert the ID string")) + } + + fn get_block_height(&self) -> Result { + Ok(BlockHeight(unsafe { anoma_vp_get_block_height() })) + } + + fn get_block_hash(&self) -> Result { + let result = Vec::with_capacity(BLOCK_HASH_LENGTH); + unsafe { + anoma_vp_get_block_hash(result.as_ptr() as _); + } + let slice = unsafe { + slice::from_raw_parts(result.as_ptr(), BLOCK_HASH_LENGTH) + }; + Ok(BlockHash::try_from(slice).expect("Cannot convert the hash")) + } + + fn get_block_epoch(&self) -> Result { + Ok(Epoch(unsafe { anoma_vp_get_block_epoch() })) + } + + fn iter_prefix( + &self, + prefix: &storage::Key, + ) -> Result { + let prefix = prefix.to_string(); + let iter_id = unsafe { + anoma_vp_iter_prefix(prefix.as_ptr() as _, prefix.len() as _) + }; + Ok(KeyValIterator(iter_id, PhantomData)) + } + + fn iter_pre_next( + &self, + iter: &mut Self::PrefixIter, + ) -> Result)>, Self::Error> { + let read_result = unsafe { anoma_vp_iter_pre_next(iter.0) }; + Ok(read_key_val_bytes_from_buffer( + read_result, + anoma_vp_result_buffer, + )) + } + + fn iter_post_next( + &self, + iter: &mut Self::PrefixIter, + ) -> Result)>, Self::Error> { + let read_result = unsafe { anoma_vp_iter_post_next(iter.0) }; + Ok(read_key_val_bytes_from_buffer( + read_result, + anoma_vp_result_buffer, + )) + } + + fn eval( + &self, + vp_code: Vec, + input_data: Vec, + ) -> Result { + let result = unsafe { + anoma_vp_eval( + vp_code.as_ptr() as _, + vp_code.len() as _, + input_data.as_ptr() as _, + input_data.len() as _, + ) + }; + Ok(HostEnvResult::is_success(result)) + } + + fn verify_tx_signature( + &self, + pk: &common::PublicKey, + sig: &common::Signature, + ) -> Result { + let pk = BorshSerialize::try_to_vec(pk).unwrap(); + let sig = BorshSerialize::try_to_vec(sig).unwrap(); + let valid = unsafe { + anoma_vp_verify_tx_signature( + pk.as_ptr() as _, + pk.len() as _, + sig.as_ptr() as _, + sig.len() as _, + ) + }; + Ok(HostEnvResult::is_success(valid)) + } + + fn get_tx_code_hash(&self) -> Result { + let result = Vec::with_capacity(HASH_LENGTH); + unsafe { + anoma_vp_get_tx_code_hash(result.as_ptr() as _); + } + let slice = + unsafe { slice::from_raw_parts(result.as_ptr(), HASH_LENGTH) }; + Ok(Hash::try_from(slice).expect("Cannot convert the hash")) + } +} diff --git a/vp_prelude/src/nft.rs b/vp_prelude/src/nft.rs new file mode 100644 index 0000000000..1d5d019169 --- /dev/null +++ b/vp_prelude/src/nft.rs @@ -0,0 +1,116 @@ +//! NFT validity predicate + +use std::collections::BTreeSet; + +use namada::ledger::native_vp::VpEnv; +use namada::types::address::Address; +pub use namada::types::nft::*; +use namada::types::storage::Key; + +use super::{accept, reject, Ctx, EnvResult, VpResult}; + +enum KeyType { + Metadata(Address, String), + Approval(Address, String), + CurrentOwner(Address, String), + Creator(Address), + PastOwners(Address, String), + Unknown, +} + +pub fn vp( + ctx: &Ctx, + _tx_da_ta: Vec, + nft_address: &Address, + keys_changed: &BTreeSet, + verifiers: &BTreeSet
, +) -> VpResult { + for key in keys_changed { + match get_key_type(key, nft_address) { + KeyType::Creator(_creator_addr) => { + super::log_string("creator cannot be changed."); + return reject(); + } + KeyType::Approval(nft_address, token_id) => { + super::log_string(format!( + "nft vp, checking approvals with token id: {}", + token_id + )); + + if !(is_creator(ctx, &nft_address, verifiers)? + || is_approved( + ctx, + &nft_address, + token_id.as_ref(), + verifiers, + )?) + { + return reject(); + } + } + KeyType::Metadata(nft_address, token_id) => { + super::log_string(format!( + "nft vp, checking if metadata changed: {}", + token_id + )); + if !is_creator(ctx, &nft_address, verifiers)? { + return reject(); + } + } + _ => { + if !is_creator(ctx, nft_address, verifiers)? { + return reject(); + } + } + } + } + accept() +} + +fn is_approved( + ctx: &Ctx, + nft_address: &Address, + nft_token_id: &str, + verifiers: &BTreeSet
, +) -> EnvResult { + let approvals_key = get_token_approval_key(nft_address, nft_token_id); + let approval_addresses: Vec
= + ctx.read_pre(&approvals_key)?.unwrap_or_default(); + return Ok(approval_addresses + .iter() + .any(|addr| verifiers.contains(addr))); +} + +fn is_creator( + ctx: &Ctx, + nft_address: &Address, + verifiers: &BTreeSet
, +) -> EnvResult { + let creator_key = get_creator_key(nft_address); + let creator_address: Address = ctx.read_pre(&creator_key)?.unwrap(); + Ok(verifiers.contains(&creator_address)) +} + +fn get_key_type(key: &Key, nft_address: &Address) -> KeyType { + let is_creator_key = is_nft_creator_key(key, nft_address); + let is_metadata_key = is_nft_metadata_key(key, nft_address); + let is_approval_key = is_nft_approval_key(key, nft_address); + let is_current_owner_key = is_nft_current_owner_key(key, nft_address); + let is_past_owner_key = is_nft_past_owners_key(key, nft_address); + if let Some(nft_address) = is_creator_key { + return KeyType::Creator(nft_address); + } + if let Some((nft_address, token_id)) = is_metadata_key { + return KeyType::Metadata(nft_address, token_id); + } + if let Some((nft_address, token_id)) = is_approval_key { + return KeyType::Approval(nft_address, token_id); + } + if let Some((nft_address, token_id)) = is_current_owner_key { + return KeyType::CurrentOwner(nft_address, token_id); + } + if let Some((nft_address, token_id)) = is_past_owner_key { + return KeyType::PastOwners(nft_address, token_id); + } + KeyType::Unknown +} diff --git a/vp_prelude/src/token.rs b/vp_prelude/src/token.rs new file mode 100644 index 0000000000..6670386c4a --- /dev/null +++ b/vp_prelude/src/token.rs @@ -0,0 +1,61 @@ +//! A fungible token validity predicate. + +use std::collections::BTreeSet; + +use namada::types::address::{Address, InternalAddress}; +use namada::types::storage::Key; +/// Vp imports and functions. +use namada::types::storage::KeySeg; +use namada::types::token; +pub use namada::types::token::*; + +use super::*; + +/// A token validity predicate. +pub fn vp( + ctx: &Ctx, + token: &Address, + keys_changed: &BTreeSet, + verifiers: &BTreeSet
, +) -> VpResult { + let mut change: Change = 0; + for key in keys_changed.iter() { + match token::is_balance_key(token, key) { + None => { + // Unknown changes to this address space are disallowed, but + // unknown changes anywhere else are permitted + if key.segments.get(0) == Some(&token.to_db_key()) { + return reject(); + } + } + Some(owner) => { + // accumulate the change + let pre: Amount = match owner { + Address::Internal(InternalAddress::IbcMint) => { + Amount::max() + } + Address::Internal(InternalAddress::IbcBurn) => { + Amount::default() + } + _ => ctx.read_pre(key)?.unwrap_or_default(), + }; + let post: Amount = match owner { + Address::Internal(InternalAddress::IbcMint) => { + ctx.read_temp(key)?.unwrap_or_else(Amount::max) + } + Address::Internal(InternalAddress::IbcBurn) => { + ctx.read_temp(key)?.unwrap_or_default() + } + _ => ctx.read_post(key)?.unwrap_or_default(), + }; + let this_change = post.change() - pre.change(); + change += this_change; + // make sure that the spender approved the transaction + if this_change < 0 && !verifiers.contains(owner) { + return reject(); + } + } + } + } + Ok(change == 0) +} From 71820f6acfa6939469177982fba61b15d82bb7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 20 Jul 2022 21:02:42 +0200 Subject: [PATCH 06/81] tests: update for VM API changes --- Cargo.lock | 3 +- tests/Cargo.toml | 3 +- tests/src/native_vp/mod.rs | 56 ++-- tests/src/native_vp/pos.rs | 137 ++++---- tests/src/vm_host_env/ibc.rs | 174 +++++----- tests/src/vm_host_env/mod.rs | 609 +++++++++++++++-------------------- tests/src/vm_host_env/tx.rs | 13 +- tests/src/vm_host_env/vp.rs | 14 +- 8 files changed, 460 insertions(+), 549 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76df02e9b0..b7fce8b64d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4018,7 +4018,8 @@ dependencies = [ "libp2p", "namada", "namada_apps", - "namada_vm_env", + "namada_tx_prelude", + "namada_vp_prelude", "pretty_assertions", "proptest", "prost 0.9.0", diff --git a/tests/Cargo.toml b/tests/Cargo.toml index ed453ad450..cc437b3686 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -13,7 +13,8 @@ wasm-runtime = ["namada/wasm-runtime"] [dependencies] namada = {path = "../shared", features = ["testing", "ibc-mocks"]} -namada_vm_env = {path = "../vm_env"} +namada_vp_prelude = {path = "../vp_prelude"} +namada_tx_prelude = {path = "../tx_prelude"} chrono = "0.4.19" concat-idents = "1.1.2" prost = "0.9.0" diff --git a/tests/src/native_vp/mod.rs b/tests/src/native_vp/mod.rs index be450a7086..808299a86d 100644 --- a/tests/src/native_vp/mod.rs +++ b/tests/src/native_vp/mod.rs @@ -1,47 +1,38 @@ mod pos; +use std::collections::BTreeSet; + use namada::ledger::native_vp::{Ctx, NativeVp}; use namada::ledger::storage::mockdb::MockDB; use namada::ledger::storage::Sha256Hasher; -use namada::vm::wasm::compilation_cache; -use namada::vm::wasm::compilation_cache::common::Cache; -use namada::vm::{wasm, WasmCacheRwAccess}; -use tempfile::TempDir; +use namada::types::address::Address; +use namada::types::storage; +use namada::vm::WasmCacheRwAccess; use crate::tx::TestTxEnv; type NativeVpCtx<'a> = Ctx<'a, MockDB, Sha256Hasher, WasmCacheRwAccess>; -type VpCache = Cache; #[derive(Debug)] pub struct TestNativeVpEnv { - pub vp_cache_dir: TempDir, - pub vp_wasm_cache: VpCache, pub tx_env: TestTxEnv, + pub address: Address, + pub verifiers: BTreeSet
, + pub keys_changed: BTreeSet, } impl TestNativeVpEnv { - pub fn new(tx_env: TestTxEnv) -> Self { - let (vp_wasm_cache, vp_cache_dir) = - wasm::compilation_cache::common::testing::cache(); - - Self { - vp_cache_dir, - vp_wasm_cache, - tx_env, - } - } -} + pub fn from_tx_env(tx_env: TestTxEnv, address: Address) -> Self { + // Find the tx verifiers and keys_changes the same way as protocol would + let verifiers = tx_env.get_verifiers(); -impl Default for TestNativeVpEnv { - fn default() -> Self { - let (vp_wasm_cache, vp_cache_dir) = - wasm::compilation_cache::common::testing::cache(); + let keys_changed = tx_env.all_touched_storage_keys(); Self { - vp_cache_dir, - vp_wasm_cache, - tx_env: TestTxEnv::default(), + address, + tx_env, + verifiers, + keys_changed, } } } @@ -51,20 +42,10 @@ impl TestNativeVpEnv { pub fn validate_tx<'a, T>( &'a self, init_native_vp: impl Fn(NativeVpCtx<'a>) -> T, - // The function is applied on the `tx_data` when called - mut apply_tx: impl FnMut(&[u8]), ) -> Result::Error> where T: NativeVp, { - let tx_data = self.tx_env.tx.data.as_ref().cloned().unwrap_or_default(); - apply_tx(&tx_data); - - // Find the tx verifiers and keys_changes the same way as protocol would - let verifiers = self.tx_env.get_verifiers(); - - let keys_changed = self.tx_env.all_touched_storage_keys(); - let ctx = Ctx { iterators: Default::default(), gas_meter: Default::default(), @@ -72,10 +53,13 @@ impl TestNativeVpEnv { write_log: &self.tx_env.write_log, tx: &self.tx_env.tx, vp_wasm_cache: self.tx_env.vp_wasm_cache.clone(), + address: &self.address, + keys_changed: &self.keys_changed, + verifiers: &self.verifiers, }; let tx_data = self.tx_env.tx.data.as_ref().cloned().unwrap_or_default(); let native_vp = init_native_vp(ctx); - native_vp.validate_tx(&tx_data, &keys_changed, &verifiers) + native_vp.validate_tx(&tx_data, &self.keys_changed, &self.verifiers) } } diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index be1844c6cd..1700be2264 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -105,10 +105,10 @@ mod tests { use namada::ledger::pos::namada_proof_of_stake::PosBase; use namada::ledger::pos::PosParams; use namada::types::storage::Epoch; - use namada::types::token; - use namada_vm_env::proof_of_stake::parameters::testing::arb_pos_params; - use namada_vm_env::proof_of_stake::{staking_token_address, PosVP}; - use namada_vm_env::tx_prelude::Address; + use namada::types::{address, token}; + use namada_tx_prelude::proof_of_stake::parameters::testing::arb_pos_params; + use namada_tx_prelude::proof_of_stake::{staking_token_address, PosVP}; + use namada_tx_prelude::Address; use proptest::prelude::*; use proptest::prop_state_machine; use proptest::state_machine::{AbstractStateMachine, StateMachineTest}; @@ -410,10 +410,13 @@ mod tests { fn validate_transitions(&self) { // Use the tx_env to run PoS VP let tx_env = tx_host_env::take(); - let vp_env = TestNativeVpEnv::new(tx_env); - let result = vp_env.validate_tx(PosVP::new, |_tx_data| {}); + + let vp_env = TestNativeVpEnv::from_tx_env(tx_env, address::POS); + let result = vp_env.validate_tx(PosVP::new); + // Put the tx_env back before checking the result tx_host_env::set(vp_env.tx_env); + let result = result.expect("Validation of valid changes must not fail!"); @@ -534,24 +537,25 @@ pub mod testing { use derivative::Derivative; use itertools::Either; use namada::ledger::pos::namada_proof_of_stake::btree_set::BTreeSetShims; + use namada::ledger::tx_env::TxEnv; use namada::types::key::common::PublicKey; use namada::types::key::RefTo; use namada::types::storage::Epoch; use namada::types::{address, key, token}; - use namada_vm_env::proof_of_stake::epoched::{ + use namada_tx_prelude::proof_of_stake::epoched::{ DynEpochOffset, Epoched, EpochedDelta, }; - use namada_vm_env::proof_of_stake::types::{ + use namada_tx_prelude::proof_of_stake::types::{ Bond, Unbond, ValidatorState, VotingPower, VotingPowerDelta, WeightedValidator, }; - use namada_vm_env::proof_of_stake::{ + use namada_tx_prelude::proof_of_stake::{ staking_token_address, BondId, Bonds, PosParams, Unbonds, }; - use namada_vm_env::tx_prelude::{Address, PoS}; + use namada_tx_prelude::Address; use proptest::prelude::*; - use crate::tx::tx_host_env; + use crate::tx::{self, tx_host_env}; #[derive(Clone, Debug, Default)] pub struct TestValidator { @@ -783,8 +787,8 @@ pub mod testing { /// the VP. pub fn apply(self, is_current_tx_valid: bool) { // Read the PoS parameters - use namada_vm_env::tx_prelude::PosRead; - let params = PoS.read_pos_params(); + use namada_tx_prelude::PosRead; + let params = tx::ctx().read_pos_params().unwrap(); let current_epoch = tx_host_env::with(|env| { // Reset the gas meter on each change, so that we never run @@ -811,7 +815,7 @@ pub mod testing { params: &PosParams, current_epoch: Epoch, ) -> PosStorageChanges { - use namada_vm_env::tx_prelude::PosRead; + use namada_tx_prelude::PosRead; match self { ValidPosAction::InitValidator(addr) => { @@ -869,8 +873,10 @@ pub mod testing { // Read the validator's current total deltas (this may be // updated by previous transition(s) within the same // transaction via write log) - let validator_total_deltas = - PoS.read_validator_total_deltas(&validator).unwrap(); + let validator_total_deltas = tx::ctx() + .read_validator_total_deltas(&validator) + .unwrap() + .unwrap(); let total_delta = validator_total_deltas .get_at_offset(current_epoch, offset, params) .unwrap_or_default(); @@ -1007,8 +1013,10 @@ pub mod testing { // Read the validator's current total deltas (this may be // updated by previous transition(s) within the same // transaction via write log) - let validator_total_deltas_cur = - PoS.read_validator_total_deltas(&validator).unwrap(); + let validator_total_deltas_cur = tx::ctx() + .read_validator_total_deltas(&validator) + .unwrap() + .unwrap(); let total_delta_cur = validator_total_deltas_cur .get_at_offset(current_epoch, offset, params) .unwrap_or_default(); @@ -1073,10 +1081,12 @@ pub mod testing { changes } ValidPosAction::Withdraw { owner, validator } => { - let unbonds = PoS.read_unbond(&BondId { - source: owner.clone(), - validator: validator.clone(), - }); + let unbonds = tx::ctx() + .read_unbond(&BondId { + source: owner.clone(), + validator: validator.clone(), + }) + .unwrap(); let token_delta: i128 = unbonds .and_then(|unbonds| unbonds.get(current_epoch)) @@ -1108,7 +1118,7 @@ pub mod testing { // invalid changes is_current_tx_valid: bool, ) { - use namada_vm_env::tx_prelude::{PosRead, PosWrite}; + use namada_tx_prelude::{PosRead, PosWrite}; match change { PosStorageChange::SpawnAccount { address } => { @@ -1126,7 +1136,7 @@ pub mod testing { source: owner, validator, }; - let bonds = PoS.read_bond(&bond_id); + let bonds = tx::ctx().read_bond(&bond_id).unwrap(); let bonds = if delta >= 0 { let amount: u64 = delta.try_into().unwrap(); let amount: token::Amount = amount.into(); @@ -1190,7 +1200,7 @@ pub mod testing { ); bonds }; - PoS.write_bond(&bond_id, bonds); + tx::ctx().write_bond(&bond_id, bonds).unwrap(); } PosStorageChange::Unbond { owner, @@ -1202,8 +1212,8 @@ pub mod testing { source: owner, validator, }; - let bonds = PoS.read_bond(&bond_id).unwrap(); - let unbonds = PoS.read_unbond(&bond_id); + let bonds = tx::ctx().read_bond(&bond_id).unwrap().unwrap(); + let unbonds = tx::ctx().read_unbond(&bond_id).unwrap(); let amount: u64 = delta.try_into().unwrap(); let mut to_unbond: token::Amount = amount.into(); let mut value = Unbond { @@ -1260,10 +1270,11 @@ pub mod testing { } None => Unbonds::init(value, current_epoch, params), }; - PoS.write_unbond(&bond_id, unbonds); + tx::ctx().write_unbond(&bond_id, unbonds).unwrap(); } PosStorageChange::TotalVotingPower { vp_delta, offset } => { - let mut total_voting_powers = PoS.read_total_voting_power(); + let mut total_voting_powers = + tx::ctx().read_total_voting_power().unwrap(); let vp_delta: i64 = vp_delta.try_into().unwrap(); match offset { Either::Left(offset) => { @@ -1283,10 +1294,14 @@ pub mod testing { ); } } - PoS.write_total_voting_power(total_voting_powers) + tx::ctx() + .write_total_voting_power(total_voting_powers) + .unwrap() } PosStorageChange::ValidatorAddressRawHash { address } => { - PoS.write_validator_address_raw_hash(&address); + tx::ctx() + .write_validator_address_raw_hash(&address) + .unwrap(); } PosStorageChange::ValidatorSet { validator, @@ -1302,8 +1317,9 @@ pub mod testing { ); } PosStorageChange::ValidatorConsensusKey { validator, pk } => { - let consensus_key = PoS + let consensus_key = tx::ctx() .read_validator_consensus_key(&validator) + .unwrap() .map(|mut consensus_keys| { consensus_keys.set(pk.clone(), current_epoch, params); consensus_keys @@ -1311,21 +1327,26 @@ pub mod testing { .unwrap_or_else(|| { Epoched::init(pk, current_epoch, params) }); - PoS.write_validator_consensus_key(&validator, consensus_key); + tx::ctx() + .write_validator_consensus_key(&validator, consensus_key) + .unwrap(); } PosStorageChange::ValidatorStakingRewardsAddress { validator, address, } => { - PoS.write_validator_staking_reward_address(&validator, address); + tx::ctx() + .write_validator_staking_reward_address(&validator, address) + .unwrap(); } PosStorageChange::ValidatorTotalDeltas { validator, delta, offset, } => { - let total_deltas = PoS + let total_deltas = tx::ctx() .read_validator_total_deltas(&validator) + .unwrap() .map(|mut total_deltas| { total_deltas.add_at_offset( delta, @@ -1343,15 +1364,18 @@ pub mod testing { params, ) }); - PoS.write_validator_total_deltas(&validator, total_deltas); + tx::ctx() + .write_validator_total_deltas(&validator, total_deltas) + .unwrap(); } PosStorageChange::ValidatorVotingPower { validator, vp_delta: delta, offset, } => { - let voting_power = PoS + let voting_power = tx::ctx() .read_validator_voting_power(&validator) + .unwrap() .map(|mut voting_powers| { match offset { Either::Left(offset) => { @@ -1381,11 +1405,14 @@ pub mod testing { params, ) }); - PoS.write_validator_voting_power(&validator, voting_power); + tx::ctx() + .write_validator_voting_power(&validator, voting_power) + .unwrap(); } PosStorageChange::ValidatorState { validator, state } => { - let state = PoS + let state = tx::ctx() .read_validator_state(&validator) + .unwrap() .map(|mut states| { states.set(state, current_epoch, params); states @@ -1393,16 +1420,15 @@ pub mod testing { .unwrap_or_else(|| { Epoched::init_at_genesis(state, current_epoch) }); - PoS.write_validator_state(&validator, state); + tx::ctx().write_validator_state(&validator, state).unwrap(); } PosStorageChange::StakingTokenPosBalance { delta } => { let balance_key = token::balance_key( &staking_token_address(), - &::POS_ADDRESS, - ) - .to_string(); + &::POS_ADDRESS, + ); let mut balance: token::Amount = - tx_host_env::read(&balance_key).unwrap_or_default(); + tx::ctx().read(&balance_key).unwrap().unwrap_or_default(); if delta < 0 { let to_spend: u64 = (-delta).try_into().unwrap(); let to_spend: token::Amount = to_spend.into(); @@ -1412,16 +1438,17 @@ pub mod testing { let to_recv: token::Amount = to_recv.into(); balance.receive(&to_recv); } - tx_host_env::write(&balance_key, balance); + tx::ctx().write(&balance_key, balance).unwrap(); } PosStorageChange::WithdrawUnbond { owner, validator } => { let bond_id = BondId { source: owner, validator, }; - let mut unbonds = PoS.read_unbond(&bond_id).unwrap(); + let mut unbonds = + tx::ctx().read_unbond(&bond_id).unwrap().unwrap(); unbonds.delete_current(current_epoch, params); - PoS.write_unbond(&bond_id, unbonds); + tx::ctx().write_unbond(&bond_id, unbonds).unwrap(); } } } @@ -1433,12 +1460,12 @@ pub mod testing { current_epoch: Epoch, params: &PosParams, ) { - use namada_vm_env::tx_prelude::{PosRead, PosWrite}; + use namada_tx_prelude::{PosRead, PosWrite}; let validator_total_deltas = - PoS.read_validator_total_deltas(&validator); + tx::ctx().read_validator_total_deltas(&validator).unwrap(); // println!("Read validator set"); - let mut validator_set = PoS.read_validator_set(); + let mut validator_set = tx::ctx().read_validator_set().unwrap(); // println!("Read validator set: {:#?}", validator_set); validator_set.update_from_offset( |validator_set, epoch| { @@ -1545,7 +1572,7 @@ pub mod testing { params, ); // println!("Write validator set {:#?}", validator_set); - PoS.write_validator_set(validator_set); + tx::ctx().write_validator_set(validator_set).unwrap(); } pub fn arb_invalid_pos_action( @@ -1625,8 +1652,8 @@ pub mod testing { /// Apply an invalid PoS storage action. pub fn apply(self) { // Read the PoS parameters - use namada_vm_env::tx_prelude::PosRead; - let params = PoS.read_pos_params(); + use namada_tx_prelude::PosRead; + let params = tx::ctx().read_pos_params().unwrap(); for (epoch, changes) in self.changes { for change in changes { @@ -1641,9 +1668,9 @@ pub mod testing { params: &PosParams, current_epoch: Epoch, ) -> bool { - use namada_vm_env::tx_prelude::PosRead; + use namada_tx_prelude::PosRead; - let validator_sets = PoS.read_validator_set(); + let validator_sets = tx::ctx().read_validator_set().unwrap(); let validator_set = validator_sets .get_at_offset(current_epoch, DynEpochOffset::PipelineLen, params) .unwrap(); diff --git a/tests/src/vm_host_env/ibc.rs b/tests/src/vm_host_env/ibc.rs index 13e7bd3882..0f94214a35 100644 --- a/tests/src/vm_host_env/ibc.rs +++ b/tests/src/vm_host_env/ibc.rs @@ -1,5 +1,5 @@ use core::time::Duration; -use std::collections::{BTreeSet, HashMap}; +use std::collections::HashMap; use std::str::FromStr; use namada::ibc::applications::ics20_fungible_token_transfer::msgs::transfer::MsgTransfer; @@ -60,24 +60,22 @@ use namada::ledger::ibc::vp::{ use namada::ledger::native_vp::{Ctx, NativeVp}; use namada::ledger::storage::mockdb::MockDB; use namada::ledger::storage::Sha256Hasher; +use namada::ledger::tx_env::TxEnv; use namada::proto::Tx; use namada::tendermint_proto::Protobuf; use namada::types::address::{self, Address, InternalAddress}; use namada::types::ibc::data::FungibleTokenPacketData; -use namada::types::ibc::IbcEvent; -use namada::types::storage::{BlockHash, BlockHeight, Key}; -use namada::types::time::Rfc3339String; +use namada::types::storage::{self, BlockHash, BlockHeight}; use namada::types::token::{self, Amount}; use namada::vm::{wasm, WasmCacheRwAccess}; -use tempfile::TempDir; -use crate::tx::*; +use crate::tx::{self, *}; const VP_ALWAYS_TRUE_WASM: &str = "../wasm_for_tests/vp_always_true.wasm"; +const ADDRESS: Address = Address::Internal(InternalAddress::Ibc); pub struct TestIbcVp<'a> { pub ibc: Ibc<'a, MockDB, Sha256Hasher, WasmCacheRwAccess>, - pub keys_changed: BTreeSet, } impl<'a> TestIbcVp<'a> { @@ -85,14 +83,16 @@ impl<'a> TestIbcVp<'a> { &self, tx_data: &[u8], ) -> std::result::Result { - self.ibc - .validate_tx(tx_data, &self.keys_changed, &BTreeSet::new()) + self.ibc.validate_tx( + tx_data, + self.ibc.ctx.keys_changed, + self.ibc.ctx.verifiers, + ) } } pub struct TestIbcTokenVp<'a> { pub token: IbcToken<'a, MockDB, Sha256Hasher, WasmCacheRwAccess>, - pub keys_changed: BTreeSet, } impl<'a> TestIbcTokenVp<'a> { @@ -100,82 +100,19 @@ impl<'a> TestIbcTokenVp<'a> { &self, tx_data: &[u8], ) -> std::result::Result { - self.token - .validate_tx(tx_data, &self.keys_changed, &BTreeSet::new()) + self.token.validate_tx( + tx_data, + self.token.ctx.keys_changed, + self.token.ctx.verifiers, + ) } } -pub struct TestIbcActions; - -impl IbcActions for TestIbcActions { - /// Read IBC-related data - fn read_ibc_data(&self, key: &Key) -> Option> { - tx_host_env::read_bytes(key.to_string()) - } - - /// Write IBC-related data - fn write_ibc_data(&self, key: &Key, data: impl AsRef<[u8]>) { - tx_host_env::write_bytes(key.to_string(), data) - } - - /// Delete IBC-related data - fn delete_ibc_data(&self, key: &Key) { - tx_host_env::delete(key.to_string()) - } - - /// Emit an IBC event - fn emit_ibc_event(&self, event: IbcEvent) { - tx_host_env::emit_ibc_event(&event) - } - - fn transfer_token( - &self, - src: &Address, - dest: &Address, - token: &Address, - amount: Amount, - ) { - let src_key = token::balance_key(token, src); - let dest_key = token::balance_key(token, dest); - let src_bal: Option = tx_host_env::read(&src_key.to_string()); - let mut src_bal = src_bal.unwrap_or_else(|| match src { - Address::Internal(InternalAddress::IbcMint) => Amount::max(), - _ => unreachable!(), - }); - src_bal.spend(&amount); - let mut dest_bal: Amount = - tx_host_env::read(&dest_key.to_string()).unwrap_or_default(); - dest_bal.receive(&amount); - match src { - Address::Internal(InternalAddress::IbcMint) => { - tx_host_env::write_temp(&src_key.to_string(), src_bal) - } - Address::Internal(InternalAddress::IbcBurn) => unreachable!(), - _ => tx_host_env::write(&src_key.to_string(), src_bal), - } - match dest { - Address::Internal(InternalAddress::IbcMint) => unreachable!(), - Address::Internal(InternalAddress::IbcBurn) => { - tx_host_env::write_temp(&dest_key.to_string(), dest_bal) - } - _ => tx_host_env::write(&dest_key.to_string(), dest_bal), - } - } - - fn get_height(&self) -> BlockHeight { - tx_host_env::get_block_height() - } - - fn get_header_time(&self) -> Rfc3339String { - tx_host_env::get_block_time() - } -} - -/// Initialize IBC VP by running a transaction. -pub fn init_ibc_vp_from_tx<'a>( +/// Validate an IBC transaction with IBC VP. +pub fn validate_ibc_vp_from_tx<'a>( tx_env: &'a TestTxEnv, tx: &'a Tx, -) -> (TestIbcVp<'a>, TempDir) { +) -> std::result::Result { let (verifiers, keys_changed) = tx_env .write_log .verifiers_and_changed_keys(&tx_env.verifiers); @@ -186,27 +123,30 @@ pub fn init_ibc_vp_from_tx<'a>( addr, verifiers ); } - let (vp_wasm_cache, vp_cache_dir) = + let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); let ctx = Ctx::new( + &ADDRESS, &tx_env.storage, &tx_env.write_log, tx, VpGasMeter::new(0), + &keys_changed, + &verifiers, vp_wasm_cache, ); let ibc = Ibc { ctx }; - (TestIbcVp { ibc, keys_changed }, vp_cache_dir) + TestIbcVp { ibc }.validate(tx.data.as_ref().unwrap()) } -/// Initialize the native token VP for the given address -pub fn init_token_vp_from_tx<'a>( +/// Validate the native token VP for the given address +pub fn validate_token_vp_from_tx<'a>( tx_env: &'a TestTxEnv, tx: &'a Tx, addr: &Address, -) -> (TestIbcTokenVp<'a>, TempDir) { +) -> std::result::Result { let (verifiers, keys_changed) = tx_env .write_log .verifiers_and_changed_keys(&tx_env.verifiers); @@ -217,26 +157,57 @@ pub fn init_token_vp_from_tx<'a>( addr, verifiers ); } - let (vp_wasm_cache, vp_cache_dir) = + let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); let ctx = Ctx::new( + &ADDRESS, &tx_env.storage, &tx_env.write_log, tx, VpGasMeter::new(0), + &keys_changed, + &verifiers, vp_wasm_cache, ); let token = IbcToken { ctx }; - ( - TestIbcTokenVp { - token, - keys_changed, - }, - vp_cache_dir, - ) -} + TestIbcTokenVp { token }.validate(tx.data.as_ref().unwrap()) +} + +// /// Initialize the native token VP for the given address +// pub fn init_token_vp_from_tx<'a>( +// tx_env: &'a TestTxEnv, +// tx: &'a Tx, +// addr: &Address, +// ) -> (TestIbcTokenVp<'a>, TempDir) { +// let (verifiers, keys_changed) = tx_env +// .write_log +// .verifiers_and_changed_keys(&tx_env.verifiers); +// if !verifiers.contains(addr) { +// panic!( +// "The given token address {} isn't part of the tx verifiers set: \ +// {:#?}", +// addr, verifiers +// ); +// } +// let (vp_wasm_cache, vp_cache_dir) = +// wasm::compilation_cache::common::testing::cache(); + +// let ctx = Ctx::new( +// &ADDRESS, +// &tx_env.storage, +// &tx_env.write_log, +// tx, +// VpGasMeter::new(0), +// &keys_changed, +// &verifiers, +// vp_wasm_cache, +// ); +// let token = IbcToken { ctx }; + +// (TestIbcTokenVp { token }, vp_cache_dir) +// } /// Initialize the test storage. Requires initialized [`tx_host_env::ENV`]. pub fn init_storage() -> (Address, Address) { @@ -251,17 +222,18 @@ pub fn init_storage() -> (Address, Address) { // initialize a token let code = std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); - let token = tx_host_env::init_account(code.clone()); + let token = tx::ctx().init_account(code.clone()).unwrap(); // initialize an account - let account = tx_host_env::init_account(code); + let account = tx::ctx().init_account(code).unwrap(); let key = token::balance_key(&token, &account); let init_bal = Amount::from(1_000_000_000u64); - tx_host_env::write(key.to_string(), init_bal); + tx::ctx().write(&key, init_bal).unwrap(); (token, account) } -pub fn prepare_client() -> (ClientId, AnyClientState, HashMap>) { +pub fn prepare_client() +-> (ClientId, AnyClientState, HashMap>) { let mut writes = HashMap::new(); let msg = msg_create_client(); @@ -292,7 +264,7 @@ pub fn prepare_client() -> (ClientId, AnyClientState, HashMap>) { pub fn prepare_opened_connection( client_id: &ClientId, -) -> (ConnectionId, HashMap>) { +) -> (ConnectionId, HashMap>) { let mut writes = HashMap::new(); let conn_id = connection_id(0); @@ -313,7 +285,7 @@ pub fn prepare_opened_connection( pub fn prepare_opened_channel( conn_id: &ConnectionId, is_ordered: bool, -) -> (PortId, ChannelId, HashMap>) { +) -> (PortId, ChannelId, HashMap>) { let mut writes = HashMap::new(); // port diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index ce547520f7..d539289533 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -26,6 +26,7 @@ mod tests { use namada::ledger::ibc::vp::{ get_dummy_header as tm_dummy_header, Error as IbcError, }; + use namada::ledger::tx_env::TxEnv; use namada::proto::{SignedTxData, Tx}; use namada::tendermint_proto::Protobuf; use namada::types::key::*; @@ -33,16 +34,14 @@ mod tests { use namada::types::time::DateTimeUtc; use namada::types::token::{self, Amount}; use namada::types::{address, key}; - use namada_vm_env::tx_prelude::{ - BorshDeserialize, BorshSerialize, KeyValIterator, - }; - use namada_vm_env::vp_prelude::{PostKeyValIterator, PreKeyValIterator}; + use namada_tx_prelude::{BorshDeserialize, BorshSerialize}; + use namada_vp_prelude::VpEnv; use prost::Message; use test_log::test; - use super::ibc; - use super::tx::*; - use super::vp::*; + use super::{ibc, tx, vp}; + use crate::tx::{tx_host_env, TestTxEnv}; + use crate::vp::{vp_host_env, TestVpEnv}; // paths to the WASMs used for tests const VP_ALWAYS_TRUE_WASM: &str = "../wasm_for_tests/vp_always_true.wasm"; @@ -53,8 +52,8 @@ mod tests { // The environment must be initialized first tx_host_env::init(); - let key = "key"; - let read_value: Option = tx_host_env::read(key); + let key = storage::Key::parse("key").unwrap(); + let read_value: Option = tx::ctx().read(&key).unwrap(); assert_eq!( None, read_value, "Trying to read a key that doesn't exists shouldn't find any value" @@ -62,9 +61,9 @@ mod tests { // Write some value let value = "test".repeat(4); - tx_host_env::write(key, value.clone()); + tx::ctx().write(&key, value.clone()).unwrap(); - let read_value: Option = tx_host_env::read(key); + let read_value: Option = tx::ctx().read(&key).unwrap(); assert_eq!( Some(value), read_value, @@ -73,8 +72,8 @@ mod tests { ); let value = vec![1_u8; 1000]; - tx_host_env::write(key, value.clone()); - let read_value: Option> = tx_host_env::read(key); + tx::ctx().write(&key, value.clone()).unwrap(); + let read_value: Option> = tx::ctx().read(&key).unwrap(); assert_eq!( Some(value), read_value, @@ -87,18 +86,18 @@ mod tests { // The environment must be initialized first tx_host_env::init(); - let key = "key"; + let key = storage::Key::parse("key").unwrap(); assert!( - !tx_host_env::has_key(key), + !tx::ctx().has_key(&key).unwrap(), "Before a key-value is written, its key shouldn't be found" ); // Write some value let value = "test".to_string(); - tx_host_env::write(key, value); + tx::ctx().write(&key, value).unwrap(); assert!( - tx_host_env::has_key(key), + tx::ctx().has_key(&key).unwrap(), "After a key-value has been written, its key should be found" ); } @@ -112,28 +111,28 @@ mod tests { tx_host_env::set(env); // Trying to delete a key that doesn't exists should be a no-op - let key = "key"; - tx_host_env::delete(key); + let key = storage::Key::parse("key").unwrap(); + tx::ctx().delete(&key).unwrap(); let value = "test".to_string(); - tx_host_env::write(key, value); + tx::ctx().write(&key, value).unwrap(); assert!( - tx_host_env::has_key(key), + tx::ctx().has_key(&key).unwrap(), "After a key-value has been written, its key should be found" ); // Then delete it - tx_host_env::delete(key); + tx::ctx().delete(&key).unwrap(); assert!( - !tx_host_env::has_key(key), + !tx::ctx().has_key(&key).unwrap(), "After a key has been deleted, its key shouldn't be found" ); // Trying to delete a validity predicate should fail - let key = storage::Key::validity_predicate(&test_account).to_string(); + let key = storage::Key::validity_predicate(&test_account); assert!( - panic::catch_unwind(|| { tx_host_env::delete(key) }) + panic::catch_unwind(|| { tx::ctx().delete(&key).unwrap() }) .err() .map(|a| a.downcast_ref::().cloned().unwrap()) .unwrap() @@ -146,10 +145,10 @@ mod tests { // The environment must be initialized first tx_host_env::init(); - let iter: KeyValIterator> = tx_host_env::iter_prefix("empty"); - assert_eq!( - iter.count(), - 0, + let empty_key = storage::Key::parse("empty").unwrap(); + let mut iter = tx::ctx().iter_prefix(&empty_key).unwrap(); + assert!( + tx::ctx().iter_next(&mut iter).unwrap().is_none(), "Trying to iter a prefix that doesn't have any matching keys \ should yield an empty iterator." ); @@ -166,8 +165,14 @@ mod tests { }); // Then try to iterate over their prefix - let iter: KeyValIterator = - tx_host_env::iter_prefix(prefix.to_string()); + let iter = tx::ctx().iter_prefix(&prefix).unwrap(); + let iter = itertools::unfold(iter, |iter| { + if let Ok(Some((key, value))) = tx::ctx().iter_next(iter) { + let decoded_value = i32::try_from_slice(&value[..]).unwrap(); + return Some((key, decoded_value)); + } + None + }); let expected = (0..10).map(|i| (format!("{}/{}", prefix, i), i)); itertools::assert_equal(iter.sorted(), expected.sorted()); } @@ -182,7 +187,7 @@ mod tests { "pre-condition" ); let verifier = address::testing::established_address_1(); - tx_host_env::insert_verifier(&verifier); + tx::ctx().insert_verifier(&verifier).unwrap(); assert!( tx_host_env::with(|env| env.verifiers.contains(&verifier)), "The verifier should have been inserted" @@ -201,7 +206,7 @@ mod tests { tx_host_env::init(); let code = vec![]; - tx_host_env::init_account(code); + tx::ctx().init_account(code).unwrap(); } #[test] @@ -211,7 +216,7 @@ mod tests { let code = std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); - tx_host_env::init_account(code); + tx::ctx().init_account(code).unwrap(); } #[test] @@ -220,19 +225,19 @@ mod tests { tx_host_env::init(); assert_eq!( - tx_host_env::get_chain_id(), + tx::ctx().get_chain_id().unwrap(), tx_host_env::with(|env| env.storage.get_chain_id().0) ); assert_eq!( - tx_host_env::get_block_height(), + tx::ctx().get_block_height().unwrap(), tx_host_env::with(|env| env.storage.get_block_height().0) ); assert_eq!( - tx_host_env::get_block_hash(), + tx::ctx().get_block_hash().unwrap(), tx_host_env::with(|env| env.storage.get_block_hash().0) ); assert_eq!( - tx_host_env::get_block_epoch(), + tx::ctx().get_block_epoch().unwrap(), tx_host_env::with(|env| env.storage.get_current_epoch().0) ); } @@ -252,9 +257,9 @@ mod tests { env.write_log.write(&key, value_raw.clone()).unwrap() }); - let read_pre_value: Option = vp_host_env::read_pre(key_raw); + let read_pre_value: Option = vp::CTX.read_pre(&key).unwrap(); assert_eq!(None, read_pre_value); - let read_post_value: Option = vp_host_env::read_post(key_raw); + let read_post_value: Option = vp::CTX.read_post(&key).unwrap(); assert_eq!(Some(value), read_post_value); } @@ -268,7 +273,6 @@ mod tests { // Write some value to storage let existing_key = addr_key.join(&Key::parse("existing_key_raw").unwrap()); - let existing_key_raw = existing_key.to_string(); let existing_value = vec![2_u8; 1000]; // Values written to storage have to be encoded with Borsh let existing_value_encoded = existing_value.try_to_vec().unwrap(); @@ -280,25 +284,24 @@ mod tests { // In a transaction, write override the existing key's value and add // another key-value let override_value = "override".to_string(); - let new_key = - addr_key.join(&Key::parse("new_key").unwrap()).to_string(); + let new_key = addr_key.join(&Key::parse("new_key").unwrap()); let new_value = "vp".repeat(4); // Initialize the VP environment via a transaction vp_host_env::init_from_tx(addr, tx_env, |_addr| { // Override the existing key - tx_host_env::write(&existing_key_raw, &override_value); + tx::ctx().write(&existing_key, &override_value).unwrap(); // Write the new key-value - tx_host_env::write(&new_key, new_value.clone()); + tx::ctx().write(&new_key, new_value.clone()).unwrap(); }); assert!( - vp_host_env::has_key_pre(&existing_key_raw), + vp::CTX.has_key_pre(&existing_key).unwrap(), "The existing key before transaction should be found" ); let pre_existing_value: Option> = - vp_host_env::read_pre(&existing_key_raw); + vp::CTX.read_pre(&existing_key).unwrap(); assert_eq!( Some(existing_value), pre_existing_value, @@ -307,10 +310,11 @@ mod tests { ); assert!( - !vp_host_env::has_key_pre(&new_key), + !vp::CTX.has_key_pre(&new_key).unwrap(), "The new key before transaction shouldn't be found" ); - let pre_new_value: Option> = vp_host_env::read_pre(&new_key); + let pre_new_value: Option> = + vp::CTX.read_pre(&new_key).unwrap(); assert_eq!( None, pre_new_value, "The new value read from state before transaction shouldn't yet \ @@ -318,11 +322,11 @@ mod tests { ); assert!( - vp_host_env::has_key_post(&existing_key_raw), + vp::CTX.has_key_post(&existing_key).unwrap(), "The existing key after transaction should still be found" ); let post_existing_value: Option = - vp_host_env::read_post(&existing_key_raw); + vp::CTX.read_post(&existing_key).unwrap(); assert_eq!( Some(override_value), post_existing_value, @@ -331,10 +335,11 @@ mod tests { ); assert!( - vp_host_env::has_key_post(&new_key), + vp::CTX.has_key_post(&new_key).unwrap(), "The new key after transaction should be found" ); - let post_new_value: Option = vp_host_env::read_post(&new_key); + let post_new_value: Option = + vp::CTX.read_post(&new_key).unwrap(); assert_eq!( Some(new_value), post_new_value, @@ -362,26 +367,37 @@ mod tests { // In a transaction, write override the existing key's value and add // another key-value let existing_key = prefix.join(&Key::parse(5.to_string()).unwrap()); - let existing_key_raw = existing_key.to_string(); let new_key = prefix.join(&Key::parse(11.to_string()).unwrap()); - let new_key_raw = new_key.to_string(); // Initialize the VP environment via a transaction vp_host_env::init_from_tx(addr, tx_env, |_addr| { // Override one of the existing keys - tx_host_env::write(&existing_key_raw, 100_i32); + tx::ctx().write(&existing_key, 100_i32).unwrap(); // Write the new key-value under the same prefix - tx_host_env::write(&new_key_raw, 11.try_to_vec().unwrap()); + tx::ctx().write(&new_key, 11_i32).unwrap(); }); - let iter_pre: PreKeyValIterator = - vp_host_env::iter_prefix_pre(prefix.to_string()); + let iter_pre = vp::CTX.iter_prefix(&prefix).unwrap(); + let iter_pre = itertools::unfold(iter_pre, |iter| { + if let Ok(Some((key, value))) = vp::CTX.iter_pre_next(iter) { + if let Ok(decoded_value) = i32::try_from_slice(&value[..]) { + return Some((key, decoded_value)); + } + } + None + }); let expected_pre = (0..10).map(|i| (format!("{}/{}", prefix, i), i)); itertools::assert_equal(iter_pre.sorted(), expected_pre.sorted()); - let iter_post: PostKeyValIterator = - vp_host_env::iter_prefix_post(prefix.to_string()); + let iter_post = vp::CTX.iter_prefix(&prefix).unwrap(); + let iter_post = itertools::unfold(iter_post, |iter| { + if let Ok(Some((key, value))) = vp::CTX.iter_post_next(iter) { + let decoded_value = i32::try_from_slice(&value[..]).unwrap(); + return Some((key, decoded_value)); + } + None + }); let expected_post = (0..10).map(|i| { let val = if i == 5 { 100 } else { i }; (format!("{}/{}", prefix, i), val) @@ -421,13 +437,21 @@ mod tests { .expect("decoding signed data we just signed") }); assert_eq!(&signed_tx_data.data, data); - assert!(vp_host_env::verify_tx_signature(&pk, &signed_tx_data.sig)); + assert!( + vp::CTX + .verify_tx_signature(&pk, &signed_tx_data.sig) + .unwrap() + ); let other_keypair = key::testing::keypair_2(); - assert!(!vp_host_env::verify_tx_signature( - &other_keypair.ref_to(), - &signed_tx_data.sig - )); + assert!( + !vp::CTX + .verify_tx_signature( + &other_keypair.ref_to(), + &signed_tx_data.sig + ) + .unwrap() + ); } } @@ -437,19 +461,19 @@ mod tests { vp_host_env::init(); assert_eq!( - vp_host_env::get_chain_id(), + vp::CTX.get_chain_id().unwrap(), vp_host_env::with(|env| env.storage.get_chain_id().0) ); assert_eq!( - vp_host_env::get_block_height(), + vp::CTX.get_block_height().unwrap(), vp_host_env::with(|env| env.storage.get_block_height().0) ); assert_eq!( - vp_host_env::get_block_hash(), + vp::CTX.get_block_hash().unwrap(), vp_host_env::with(|env| env.storage.get_block_hash().0) ); assert_eq!( - vp_host_env::get_block_epoch(), + vp::CTX.get_block_epoch().unwrap(), vp_host_env::with(|env| env.storage.get_current_epoch().0) ); } @@ -462,14 +486,14 @@ mod tests { // evaluating without any code should fail let empty_code = vec![]; let input_data = vec![]; - let result = vp_host_env::eval(empty_code, input_data); + let result = vp::CTX.eval(empty_code, input_data).unwrap(); assert!(!result); // evaluating the VP template which always returns `true` should pass let code = std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); let input_data = vec![]; - let result = vp_host_env::eval(code, input_data); + let result = vp::CTX.eval(code, input_data).unwrap(); assert!(result); // evaluating the VP template which always returns `false` shouldn't @@ -477,7 +501,7 @@ mod tests { let code = std::fs::read(VP_ALWAYS_FALSE_WASM).expect("cannot load wasm"); let input_data = vec![]; - let result = vp_host_env::eval(code, input_data); + let result = vp::CTX.eval(code, input_data).unwrap(); assert!(!result); } @@ -503,25 +527,25 @@ mod tests { .sign(&key::testing::keypair_1()); // get and increment the connection counter let counter_key = ibc::client_counter_key(); - let counter = ibc::TestIbcActions + let counter = tx::ctx() .get_and_inc_counter(&counter_key) .expect("getting the counter failed"); let client_id = ibc::client_id(msg.client_state.client_type(), counter) .expect("invalid client ID"); // only insert a client type - let client_type_key = ibc::client_type_key(&client_id).to_string(); - tx_host_env::write( - &client_type_key, - msg.client_state.client_type().as_str().as_bytes(), - ); + let client_type_key = ibc::client_type_key(&client_id); + tx::ctx() + .write( + &client_type_key, + msg.client_state.client_type().as_str().as_bytes(), + ) + .unwrap(); // Check should fail due to no client state let mut env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); assert!(matches!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect_err("validation succeeded unexpectedly"), + result.expect_err("validation succeeded unexpectedly"), IbcError::ClientError(_), )); // drop the transaction @@ -540,18 +564,14 @@ mod tests { .sign(&key::testing::keypair_1()); // create a client with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("creating a client failed"); // Check let mut env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); // Commit env.commit_tx_and_block(); @@ -582,27 +602,28 @@ mod tests { let same_client_state = old_data.client_state.clone(); let height = same_client_state.latest_height(); let same_consensus_state = old_data.consensus_state; - let client_state_key = ibc::client_state_key(&client_id).to_string(); - tx_host_env::write_bytes( - &client_state_key, - same_client_state.encode_vec().unwrap(), - ); - let consensus_state_key = - ibc::consensus_state_key(&client_id, height).to_string(); - tx_host_env::write( - &consensus_state_key, - same_consensus_state.encode_vec().unwrap(), - ); + let client_state_key = ibc::client_state_key(&client_id); + tx::ctx() + .write_bytes( + &client_state_key, + same_client_state.encode_vec().unwrap(), + ) + .unwrap(); + let consensus_state_key = ibc::consensus_state_key(&client_id, height); + tx::ctx() + .write( + &consensus_state_key, + same_consensus_state.encode_vec().unwrap(), + ) + .unwrap(); let event = ibc::make_update_client_event(&client_id, &msg); - tx_host_env::emit_ibc_event(&event.try_into().unwrap()); + TxEnv::emit_ibc_event(tx::ctx(), &event.try_into().unwrap()).unwrap(); // Check should fail due to the invalid updating let mut env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); assert!(matches!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect_err("validation succeeded unexpectedly"), + result.expect_err("validation succeeded unexpectedly"), IbcError::ClientError(_), )); // drop the transaction @@ -620,18 +641,14 @@ mod tests { } .sign(&key::testing::keypair_1()); // update the client with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("updating the client failed"); // Check let mut env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); // Commit env.commit_tx_and_block(); @@ -653,18 +670,14 @@ mod tests { } .sign(&key::testing::keypair_1()); // upgrade the client with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("upgrading the client failed"); // Check let env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); } #[test] @@ -696,25 +709,25 @@ mod tests { .sign(&key::testing::keypair_1()); // get and increment the connection counter let counter_key = ibc::connection_counter_key(); - let counter = ibc::TestIbcActions + let counter = tx::ctx() .get_and_inc_counter(&counter_key) .expect("getting the counter failed"); // insert a new opened connection let conn_id = ibc::connection_id(counter); - let conn_key = ibc::connection_key(&conn_id).to_string(); + let conn_key = ibc::connection_key(&conn_id); let mut connection = ibc::init_connection(&msg); ibc::open_connection(&mut connection); - tx_host_env::write_bytes(&conn_key, connection.encode_vec().unwrap()); + tx::ctx() + .write_bytes(&conn_key, connection.encode_vec().unwrap()) + .unwrap(); let event = ibc::make_open_init_connection_event(&conn_id, &msg); - tx_host_env::emit_ibc_event(&event.try_into().unwrap()); + TxEnv::emit_ibc_event(tx::ctx(), &event.try_into().unwrap()).unwrap(); // Check should fail due to directly opening a connection let mut env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); assert!(matches!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect_err("validation succeeded unexpectedly"), + result.expect_err("validation succeeded unexpectedly"), IbcError::ConnectionError(_), )); // drop the transaction @@ -732,18 +745,14 @@ mod tests { } .sign(&key::testing::keypair_1()); // init a connection with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("creating a connection failed"); // Check let mut env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); // Commit env.commit_tx_and_block(); @@ -762,18 +771,14 @@ mod tests { } .sign(&key::testing::keypair_1()); // open the connection with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("opening the connection failed"); // Check let env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); } #[test] @@ -802,18 +807,14 @@ mod tests { } .sign(&key::testing::keypair_1()); // open try a connection with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("creating a connection failed"); // Check let mut env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); // Commit env.commit_tx_and_block(); @@ -833,18 +834,14 @@ mod tests { } .sign(&key::testing::keypair_1()); // open the connection with the mssage - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("opening the connection failed"); // Check let env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); } #[test] @@ -880,27 +877,24 @@ mod tests { // not bind a port // get and increment the channel counter let counter_key = ibc::channel_counter_key(); - let counter = ibc::TestIbcActions + let counter = tx::ctx() .get_and_inc_counter(&counter_key) .expect("getting the counter failed"); // channel let channel_id = ibc::channel_id(counter); let port_channel_id = ibc::port_channel_id(port_id, channel_id.clone()); - let channel_key = ibc::channel_key(&port_channel_id).to_string(); - tx_host_env::write_bytes( - &channel_key, - msg.channel.encode_vec().unwrap(), - ); + let channel_key = ibc::channel_key(&port_channel_id); + tx::ctx() + .write_bytes(&channel_key, msg.channel.encode_vec().unwrap()) + .unwrap(); let event = ibc::make_open_init_channel_event(&channel_id, &msg); - tx_host_env::emit_ibc_event(&event.try_into().unwrap()); + TxEnv::emit_ibc_event(tx::ctx(), &event.try_into().unwrap()).unwrap(); // Check should fail due to no port binding let mut env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); assert!(matches!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect_err("validation succeeded unexpectedly"), + result.expect_err("validation succeeded unexpectedly"), IbcError::ChannelError(_), )); // drop the transaction @@ -922,32 +916,32 @@ mod tests { } .sign(&key::testing::keypair_1()); // bind a port - ibc::TestIbcActions + tx::ctx() .bind_port(&port_id) .expect("binding the port failed"); // get and increment the channel counter let counter_key = ibc::channel_counter_key(); - let counter = ibc::TestIbcActions + let counter = tx::ctx() .get_and_inc_counter(&counter_key) .expect("getting the counter failed"); // insert a opened channel let channel_id = ibc::channel_id(counter); let port_channel_id = ibc::port_channel_id(port_id, channel_id.clone()); - let channel_key = ibc::channel_key(&port_channel_id).to_string(); + let channel_key = ibc::channel_key(&port_channel_id); let mut channel = msg.channel.clone(); ibc::open_channel(&mut channel); - tx_host_env::write_bytes(&channel_key, channel.encode_vec().unwrap()); + tx::ctx() + .write_bytes(&channel_key, channel.encode_vec().unwrap()) + .unwrap(); let event = ibc::make_open_init_channel_event(&channel_id, &msg); - tx_host_env::emit_ibc_event(&event.try_into().unwrap()); + TxEnv::emit_ibc_event(tx::ctx(), &event.try_into().unwrap()).unwrap(); // Check should fail due to directly opening a channel let mut env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); assert!(matches!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect_err("validation succeeded unexpectedly"), + result.expect_err("validation succeeded unexpectedly"), IbcError::ChannelError(_), )); // drop the transaction @@ -966,18 +960,14 @@ mod tests { } .sign(&key::testing::keypair_1()); // init a channel with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("creating a channel failed"); // Check let mut env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); // Commit env.commit_tx_and_block(); @@ -994,18 +984,14 @@ mod tests { } .sign(&key::testing::keypair_1()); // open the channle with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("opening the channel failed"); // Check let env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); } #[test] @@ -1036,18 +1022,14 @@ mod tests { } .sign(&key::testing::keypair_1()); // try open a channel with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("creating a channel failed"); // Check let mut env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); // Commit env.commit_tx_and_block(); @@ -1065,18 +1047,14 @@ mod tests { } .sign(&key::testing::keypair_1()); // open a channel with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("opening the channel failed"); // Check let env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); } #[test] @@ -1109,18 +1087,14 @@ mod tests { } .sign(&key::testing::keypair_1()); // close the channel with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("closing the channel failed"); // Check let env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); } #[test] @@ -1154,18 +1128,14 @@ mod tests { .sign(&key::testing::keypair_1()); // close the channel with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("closing the channel failed"); // Check let env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); } #[test] @@ -1202,18 +1172,14 @@ mod tests { } .sign(&key::testing::keypair_1()); // send the token and a packet with the data - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("sending a packet failed"); // Check let mut env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); // Check if the token was escrowed let escrow = address::Address::Internal( address::InternalAddress::ibc_escrow_address( @@ -1221,12 +1187,9 @@ mod tests { msg.source_channel.to_string(), ), ); - let (token_vp, _) = ibc::init_token_vp_from_tx(&env, &tx, &escrow); - assert!( - token_vp - .validate(tx.data.as_ref().unwrap()) - .expect("token validation failed unexpectedly") - ); + let token_vp_result = + ibc::validate_token_vp_from_tx(&env, &tx, &escrow); + assert!(token_vp_result.expect("token validation failed unexpectedly")); // Commit env.commit_tx_and_block(); @@ -1246,18 +1209,14 @@ mod tests { } .sign(&key::testing::keypair_1()); // ack the packet with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("the packet ack failed"); // Check let env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); } #[test] @@ -1292,27 +1251,19 @@ mod tests { } .sign(&key::testing::keypair_1()); // send the token and a packet with the data - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("sending a packet failed"); // Check let env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); // Check if the token was burned let burn = address::Address::Internal(address::InternalAddress::IbcBurn); - let (token_vp, _) = ibc::init_token_vp_from_tx(&env, &tx, &burn); - assert!( - token_vp - .validate(tx.data.as_ref().unwrap()) - .expect("token validation failed unexpectedly") - ); + let result = ibc::validate_token_vp_from_tx(&env, &tx, &burn); + assert!(result.expect("token validation failed unexpectedly")); } #[test] @@ -1354,27 +1305,19 @@ mod tests { } .sign(&key::testing::keypair_1()); // receive a packet with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("receiving a packet failed"); // Check let env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); // Check if the token was minted let mint = address::Address::Internal(address::InternalAddress::IbcMint); - let (token_vp, _) = ibc::init_token_vp_from_tx(&env, &tx, &mint); - assert!( - token_vp - .validate(tx.data.as_ref().unwrap()) - .expect("token validation failed unexpectedly") - ); + let result = ibc::validate_token_vp_from_tx(&env, &tx, &mint); + assert!(result.expect("token validation failed unexpectedly")); } #[test] @@ -1436,25 +1379,17 @@ mod tests { } .sign(&key::testing::keypair_1()); // receive a packet with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("receiving a packet failed"); // Check let env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); // Check if the token was unescrowed - let (token_vp, _) = ibc::init_token_vp_from_tx(&env, &tx, &escrow); - assert!( - token_vp - .validate(tx.data.as_ref().unwrap()) - .expect("token validation failed unexpectedly") - ); + let result = ibc::validate_token_vp_from_tx(&env, &tx, &escrow); + assert!(result.expect("token validation failed unexpectedly")); } #[test] @@ -1491,20 +1426,16 @@ mod tests { } .sign(&key::testing::keypair_1()); // send a packet with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("sending a packet failed"); // the transaction does something before senging a packet // Check let mut env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); // Commit env.commit_tx_and_block(); @@ -1524,20 +1455,16 @@ mod tests { } .sign(&key::testing::keypair_1()); // ack the packet with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("the packet ack failed"); // the transaction does something after the ack // Check let env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); } #[test] @@ -1579,20 +1506,16 @@ mod tests { } .sign(&key::testing::keypair_1()); // receive a packet with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("receiving a packet failed"); // the transaction does something according to the packet // Check let env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); } #[test] @@ -1624,8 +1547,8 @@ mod tests { .encode(&mut tx_data) .expect("encoding failed"); // send a packet with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("sending apacket failed"); // Commit @@ -1646,18 +1569,14 @@ mod tests { .sign(&key::testing::keypair_1()); // close the channel with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("closing the channel failed"); // Check let env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); // Check if the token was refunded let escrow = address::Address::Internal( address::InternalAddress::ibc_escrow_address( @@ -1665,12 +1584,8 @@ mod tests { packet.source_channel.to_string(), ), ); - let (token_vp, _) = ibc::init_token_vp_from_tx(&env, &tx, &escrow); - assert!( - token_vp - .validate(tx.data.as_ref().unwrap()) - .expect("token validation failed unexpectedly") - ); + let result = ibc::validate_token_vp_from_tx(&env, &tx, &escrow); + assert!(result.expect("token validation failed unexpectedly")); } #[test] @@ -1701,8 +1616,8 @@ mod tests { .encode(&mut tx_data) .expect("encoding failed"); // send a packet with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("sending a packet failed"); // Commit @@ -1723,18 +1638,14 @@ mod tests { .sign(&key::testing::keypair_1()); // close the channel with the message - ibc::TestIbcActions - .dispatch(&tx_data) + tx::ctx() + .dispatch_ibc_action(&tx_data) .expect("closing the channel failed"); // Check let env = tx_host_env::take(); - let (ibc_vp, _) = ibc::init_ibc_vp_from_tx(&env, &tx); - assert!( - ibc_vp - .validate(tx.data.as_ref().unwrap()) - .expect("validation failed unexpectedly") - ); + let result = ibc::validate_ibc_vp_from_tx(&env, &tx); + assert!(result.expect("validation failed unexpectedly")); // Check if the token was refunded let escrow = address::Address::Internal( address::InternalAddress::ibc_escrow_address( @@ -1742,11 +1653,7 @@ mod tests { packet.source_channel.to_string(), ), ); - let (token_vp, _) = ibc::init_token_vp_from_tx(&env, &tx, &escrow); - assert!( - token_vp - .validate(tx.data.as_ref().unwrap()) - .expect("token validation failed unexpectedly") - ); + let result = ibc::validate_token_vp_from_tx(&env, &tx, &escrow); + assert!(result.expect("token validation failed unexpectedly")); } } diff --git a/tests/src/vm_host_env/tx.rs b/tests/src/vm_host_env/tx.rs index 3a684e8382..346cb6bdd4 100644 --- a/tests/src/vm_host_env/tx.rs +++ b/tests/src/vm_host_env/tx.rs @@ -15,16 +15,25 @@ use namada::types::{key, token}; use namada::vm::prefix_iter::PrefixIterators; use namada::vm::wasm::{self, TxCache, VpCache}; use namada::vm::{self, WasmCacheRwAccess}; -use namada_vm_env::tx_prelude::BorshSerialize; +use namada_tx_prelude::{BorshSerialize, Ctx}; use tempfile::TempDir; +/// Tx execution context provides access to host env functions +static mut CTX: Ctx = unsafe { Ctx::new() }; + +/// Tx execution context provides access to host env functions +pub fn ctx() -> &'static mut Ctx { + unsafe { &mut CTX } +} + /// This module combines the native host function implementations from /// `native_tx_host_env` with the functions exposed to the tx wasm /// that will call to the native functions, instead of interfacing via a /// wasm runtime. It can be used for host environment integration tests. pub mod tx_host_env { - pub use namada_vm_env::tx_prelude::*; + pub use namada_tx_prelude::*; + pub use super::ctx; pub use super::native_tx_host_env::*; } diff --git a/tests/src/vm_host_env/vp.rs b/tests/src/vm_host_env/vp.rs index 61b87e1b3b..d849a11487 100644 --- a/tests/src/vm_host_env/vp.rs +++ b/tests/src/vm_host_env/vp.rs @@ -10,17 +10,27 @@ use namada::types::storage::{self, Key}; use namada::vm::prefix_iter::PrefixIterators; use namada::vm::wasm::{self, VpCache}; use namada::vm::{self, WasmCacheRwAccess}; +use namada_vp_prelude::Ctx; use tempfile::TempDir; use crate::tx::{tx_host_env, TestTxEnv}; +/// VP execution context provides access to host env functions +pub static CTX: Ctx = unsafe { Ctx::new() }; + +/// VP execution context provides access to host env functions +pub fn ctx() -> &'static Ctx { + &CTX +} + /// This module combines the native host function implementations from /// `native_vp_host_env` with the functions exposed to the vp wasm /// that will call to the native functions, instead of interfacing via a /// wasm runtime. It can be used for host environment integration tests. pub mod vp_host_env { - pub use namada_vm_env::vp_prelude::*; + pub use namada_vp_prelude::*; + pub use super::ctx; pub use super::native_vp_host_env::*; } @@ -160,7 +170,7 @@ mod native_vp_host_env { /// Initialize the VP host environment in [`ENV`] by running a transaction. /// The transaction is expected to modify the storage sub-space of the given /// address `addr` or to add it to the set of verifiers using - /// [`tx_host_env::insert_verifier`]. + /// `ctx.insert_verifier`. pub fn init_from_tx( addr: Address, mut tx_env: TestTxEnv, From d7d0ef14ab15ac052abdbf8ef81bd0a385725b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 20 Jul 2022 21:03:18 +0200 Subject: [PATCH 07/81] wasm: update for VM API changes --- tests/src/vm_host_env/ibc.rs | 4 +- wasm/tx_template/Cargo.lock | 19 +- wasm/tx_template/src/lib.rs | 5 +- wasm/vp_template/Cargo.lock | 21 +- wasm/vp_template/src/lib.rs | 12 +- wasm/wasm_source/Cargo.lock | 13 +- wasm/wasm_source/src/tx_bond.rs | 8 +- wasm/wasm_source/src/tx_from_intent.rs | 16 +- wasm/wasm_source/src/tx_ibc.rs | 4 +- wasm/wasm_source/src/tx_init_account.rs | 7 +- wasm/wasm_source/src/tx_init_nft.rs | 5 +- wasm/wasm_source/src/tx_init_proposal.rs | 4 +- wasm/wasm_source/src/tx_init_validator.rs | 5 +- wasm/wasm_source/src/tx_mint_nft.rs | 4 +- wasm/wasm_source/src/tx_transfer.rs | 4 +- wasm/wasm_source/src/tx_unbond.rs | 12 +- wasm/wasm_source/src/tx_update_vp.rs | 4 +- wasm/wasm_source/src/tx_vote_proposal.rs | 4 +- wasm/wasm_source/src/tx_withdraw.rs | 6 +- wasm/wasm_source/src/vp_nft.rs | 389 +++++++++++++--------- wasm/wasm_source/src/vp_testnet_faucet.rs | 94 ++++-- wasm/wasm_source/src/vp_token.rs | 27 +- wasm/wasm_source/src/vp_user.rs | 225 +++++++++---- wasm_for_tests/wasm_source/Cargo.lock | 14 +- wasm_for_tests/wasm_source/Cargo.toml | 1 - wasm_for_tests/wasm_source/src/lib.rs | 90 ++--- 26 files changed, 620 insertions(+), 377 deletions(-) diff --git a/tests/src/vm_host_env/ibc.rs b/tests/src/vm_host_env/ibc.rs index 0f94214a35..a838f37819 100644 --- a/tests/src/vm_host_env/ibc.rs +++ b/tests/src/vm_host_env/ibc.rs @@ -112,7 +112,7 @@ impl<'a> TestIbcTokenVp<'a> { pub fn validate_ibc_vp_from_tx<'a>( tx_env: &'a TestTxEnv, tx: &'a Tx, -) -> std::result::Result { +) -> std::result::Result { let (verifiers, keys_changed) = tx_env .write_log .verifiers_and_changed_keys(&tx_env.verifiers); @@ -146,7 +146,7 @@ pub fn validate_token_vp_from_tx<'a>( tx_env: &'a TestTxEnv, tx: &'a Tx, addr: &Address, -) -> std::result::Result { +) -> std::result::Result { let (verifiers, keys_changed) = tx_env .write_log .verifiers_and_changed_keys(&tx_env.verifiers); diff --git a/wasm/tx_template/Cargo.lock b/wasm/tx_template/Cargo.lock index bf9d222920..08a4aa8b12 100644 --- a/wasm/tx_template/Cargo.lock +++ b/wasm/tx_template/Cargo.lock @@ -1421,7 +1421,8 @@ dependencies = [ "concat-idents", "derivative", "namada", - "namada_vm_env", + "namada_tx_prelude", + "namada_vp_prelude", "prost", "serde_json", "sha2 0.9.9", @@ -1435,8 +1436,12 @@ dependencies = [ name = "namada_tx_prelude" version = "0.7.0" dependencies = [ + "borsh", + "namada", + "namada_macros", "namada_vm_env", "sha2 0.10.2", + "thiserror", ] [[package]] @@ -1444,9 +1449,19 @@ name = "namada_vm_env" version = "0.7.0" dependencies = [ "borsh", - "hex", + "namada", +] + +[[package]] +name = "namada_vp_prelude" +version = "0.7.0" +dependencies = [ + "borsh", "namada", "namada_macros", + "namada_vm_env", + "sha2 0.10.2", + "thiserror", ] [[package]] diff --git a/wasm/tx_template/src/lib.rs b/wasm/tx_template/src/lib.rs index f507e90bed..473984aa31 100644 --- a/wasm/tx_template/src/lib.rs +++ b/wasm/tx_template/src/lib.rs @@ -1,8 +1,9 @@ use namada_tx_prelude::*; #[transaction] -fn apply_tx(tx_data: Vec) { +fn apply_tx(_ctx: &mut Ctx, tx_data: Vec) -> TxResult { log_string(format!("apply_tx called with data: {:#?}", tx_data)); + Ok(()) } #[cfg(test)] @@ -19,7 +20,7 @@ mod tests { tx_host_env::init(); let tx_data = vec![]; - apply_tx(tx_data); + apply_tx(ctx(), tx_data).unwrap(); let env = tx_host_env::take(); assert!(env.all_touched_storage_keys().is_empty()); diff --git a/wasm/vp_template/Cargo.lock b/wasm/vp_template/Cargo.lock index 98f6c7f71c..91059c4434 100644 --- a/wasm/vp_template/Cargo.lock +++ b/wasm/vp_template/Cargo.lock @@ -1421,7 +1421,8 @@ dependencies = [ "concat-idents", "derivative", "namada", - "namada_vm_env", + "namada_tx_prelude", + "namada_vp_prelude", "prost", "serde_json", "sha2 0.9.9", @@ -1432,21 +1433,35 @@ dependencies = [ ] [[package]] -name = "namada_vm_env" +name = "namada_tx_prelude" version = "0.7.0" dependencies = [ "borsh", - "hex", "namada", "namada_macros", + "namada_vm_env", + "sha2 0.10.2", + "thiserror", +] + +[[package]] +name = "namada_vm_env" +version = "0.7.0" +dependencies = [ + "borsh", + "namada", ] [[package]] name = "namada_vp_prelude" version = "0.7.0" dependencies = [ + "borsh", + "namada", + "namada_macros", "namada_vm_env", "sha2 0.10.2", + "thiserror", ] [[package]] diff --git a/wasm/vp_template/src/lib.rs b/wasm/vp_template/src/lib.rs index 7918072266..35cdabd1c5 100644 --- a/wasm/vp_template/src/lib.rs +++ b/wasm/vp_template/src/lib.rs @@ -2,25 +2,25 @@ use namada_vp_prelude::*; #[validity_predicate] fn validate_tx( + ctx: &Ctx, tx_data: Vec, addr: Address, keys_changed: BTreeSet, verifiers: BTreeSet
, -) -> bool { +) -> VpResult { log_string(format!( "validate_tx called with addr: {}, key_changed: {:#?}, tx_data: \ {:#?}, verifiers: {:?}", addr, keys_changed, tx_data, verifiers )); - for key in keys_changed.iter() { - let key = key.to_string(); - let pre: Option = read_pre(&key); - let post: Option = read_post(&key); + for key in keys_changed { + let pre: Option = ctx.read_pre(&key)?; + let post: Option = ctx.read_post(&key)?; log_string(format!( "validate_tx key: {}, pre: {:#?}, post: {:#?}", key, pre, post, )); } - true + accept() } diff --git a/wasm/wasm_source/Cargo.lock b/wasm/wasm_source/Cargo.lock index b7185cc110..19a50588dc 100644 --- a/wasm/wasm_source/Cargo.lock +++ b/wasm/wasm_source/Cargo.lock @@ -1421,7 +1421,8 @@ dependencies = [ "concat-idents", "derivative", "namada", - "namada_vm_env", + "namada_tx_prelude", + "namada_vp_prelude", "prost", "serde_json", "sha2 0.9.9", @@ -1435,8 +1436,12 @@ dependencies = [ name = "namada_tx_prelude" version = "0.7.0" dependencies = [ + "borsh", + "namada", + "namada_macros", "namada_vm_env", "sha2 0.10.2", + "thiserror", ] [[package]] @@ -1444,17 +1449,19 @@ name = "namada_vm_env" version = "0.7.0" dependencies = [ "borsh", - "hex", "namada", - "namada_macros", ] [[package]] name = "namada_vp_prelude" version = "0.7.0" dependencies = [ + "borsh", + "namada", + "namada_macros", "namada_vm_env", "sha2 0.10.2", + "thiserror", ] [[package]] diff --git a/wasm/wasm_source/src/tx_bond.rs b/wasm/wasm_source/src/tx_bond.rs index 9a5309f927..0d9d390226 100644 --- a/wasm/wasm_source/src/tx_bond.rs +++ b/wasm/wasm_source/src/tx_bond.rs @@ -1,19 +1,19 @@ //! A tx for a PoS bond that stakes tokens via a self-bond or delegation. -use namada_tx_prelude::proof_of_stake::bond_tokens; use namada_tx_prelude::*; #[transaction] -fn apply_tx(tx_data: Vec) { +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); let bond = transaction::pos::Bond::try_from_slice(&signed.data.unwrap()[..]) .unwrap(); if let Err(err) = - bond_tokens(bond.source.as_ref(), &bond.validator, bond.amount) + ctx.bond_tokens(bond.source.as_ref(), &bond.validator, bond.amount) { - debug_log!("Bond failed with: {}", err); + debug_log!("Unbonding failed with: {}", err); panic!() } + Ok(()) } diff --git a/wasm/wasm_source/src/tx_from_intent.rs b/wasm/wasm_source/src/tx_from_intent.rs index e39963fae7..9b5afb5ad0 100644 --- a/wasm/wasm_source/src/tx_from_intent.rs +++ b/wasm/wasm_source/src/tx_from_intent.rs @@ -5,7 +5,7 @@ use namada_tx_prelude::*; #[transaction] -fn apply_tx(tx_data: Vec) { +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); let tx_data = @@ -14,7 +14,7 @@ fn apply_tx(tx_data: Vec) { let tx_data = tx_data.unwrap(); // make sure that the matchmaker has to validate this tx - insert_verifier(&tx_data.source); + ctx.insert_verifier(&tx_data.source)?; for token::Transfer { source, @@ -23,13 +23,11 @@ fn apply_tx(tx_data: Vec) { amount, } in tx_data.matches.transfers { - token::transfer(&source, &target, &token, amount); + token::transfer(ctx, &source, &target, &token, amount)?; } - tx_data - .matches - .exchanges - .values() - .into_iter() - .for_each(intent::invalidate_exchange); + for intent in tx_data.matches.exchanges.values() { + intent::invalidate_exchange(ctx, intent)?; + } + Ok(()) } diff --git a/wasm/wasm_source/src/tx_ibc.rs b/wasm/wasm_source/src/tx_ibc.rs index e38aa2f856..188ec1ae8d 100644 --- a/wasm/wasm_source/src/tx_ibc.rs +++ b/wasm/wasm_source/src/tx_ibc.rs @@ -6,7 +6,7 @@ use namada_tx_prelude::*; #[transaction] -fn apply_tx(tx_data: Vec) { +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); - Ibc.dispatch(&signed.data.unwrap()).unwrap() + ctx.dispatch_ibc_action(&signed.data.unwrap()) } diff --git a/wasm/wasm_source/src/tx_init_account.rs b/wasm/wasm_source/src/tx_init_account.rs index e976c38941..9d187d3c48 100644 --- a/wasm/wasm_source/src/tx_init_account.rs +++ b/wasm/wasm_source/src/tx_init_account.rs @@ -4,14 +4,15 @@ use namada_tx_prelude::*; #[transaction] -fn apply_tx(tx_data: Vec) { +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); let tx_data = transaction::InitAccount::try_from_slice(&signed.data.unwrap()[..]) .unwrap(); debug_log!("apply_tx called to init a new established account"); - let address = init_account(&tx_data.vp_code); + let address = ctx.init_account(&tx_data.vp_code)?; let pk_key = key::pk_key(&address); - write(&pk_key.to_string(), &tx_data.public_key); + ctx.write(&pk_key, &tx_data.public_key)?; + Ok(()) } diff --git a/wasm/wasm_source/src/tx_init_nft.rs b/wasm/wasm_source/src/tx_init_nft.rs index e26d656b57..ee0db9310d 100644 --- a/wasm/wasm_source/src/tx_init_nft.rs +++ b/wasm/wasm_source/src/tx_init_nft.rs @@ -3,12 +3,13 @@ use namada_tx_prelude::*; #[transaction] -fn apply_tx(tx_data: Vec) { +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); let tx_data = transaction::nft::CreateNft::try_from_slice(&signed.data.unwrap()[..]) .unwrap(); log_string("apply_tx called to create a new NFT"); - nft::init_nft(tx_data); + let _address = nft::init_nft(ctx, tx_data)?; + Ok(()) } diff --git a/wasm/wasm_source/src/tx_init_proposal.rs b/wasm/wasm_source/src/tx_init_proposal.rs index 3cb1c3d5de..48bd0fa0f5 100644 --- a/wasm/wasm_source/src/tx_init_proposal.rs +++ b/wasm/wasm_source/src/tx_init_proposal.rs @@ -3,7 +3,7 @@ use namada_tx_prelude::*; #[transaction] -fn apply_tx(tx_data: Vec) { +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); let tx_data = transaction::governance::InitProposalData::try_from_slice( &signed.data.unwrap()[..], @@ -11,5 +11,5 @@ fn apply_tx(tx_data: Vec) { .unwrap(); log_string("apply_tx called to create a new governance proposal"); - governance::init_proposal(tx_data); + governance::init_proposal(ctx, tx_data) } diff --git a/wasm/wasm_source/src/tx_init_validator.rs b/wasm/wasm_source/src/tx_init_validator.rs index 79dfedad56..bae45fc533 100644 --- a/wasm/wasm_source/src/tx_init_validator.rs +++ b/wasm/wasm_source/src/tx_init_validator.rs @@ -5,14 +5,14 @@ use namada_tx_prelude::transaction::InitValidator; use namada_tx_prelude::*; #[transaction] -fn apply_tx(tx_data: Vec) { +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); let init_validator = InitValidator::try_from_slice(&signed.data.unwrap()[..]).unwrap(); debug_log!("apply_tx called to init a new validator account"); // Register the validator in PoS - match proof_of_stake::init_validator(init_validator) { + match ctx.init_validator(init_validator) { Ok((validator_address, staking_reward_address)) => { debug_log!( "Created validator {} and staking reward account {}", @@ -25,4 +25,5 @@ fn apply_tx(tx_data: Vec) { panic!() } } + Ok(()) } diff --git a/wasm/wasm_source/src/tx_mint_nft.rs b/wasm/wasm_source/src/tx_mint_nft.rs index 692155432c..73c915d123 100644 --- a/wasm/wasm_source/src/tx_mint_nft.rs +++ b/wasm/wasm_source/src/tx_mint_nft.rs @@ -3,12 +3,12 @@ use namada_tx_prelude::*; #[transaction] -fn apply_tx(tx_data: Vec) { +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); let tx_data = transaction::nft::MintNft::try_from_slice(&signed.data.unwrap()[..]) .unwrap(); log_string("apply_tx called to mint a new NFT tokens"); - nft::mint_tokens(tx_data); + nft::mint_tokens(ctx, tx_data) } diff --git a/wasm/wasm_source/src/tx_transfer.rs b/wasm/wasm_source/src/tx_transfer.rs index f0ab0ad2d0..e1dabcd851 100644 --- a/wasm/wasm_source/src/tx_transfer.rs +++ b/wasm/wasm_source/src/tx_transfer.rs @@ -5,7 +5,7 @@ use namada_tx_prelude::*; #[transaction] -fn apply_tx(tx_data: Vec) { +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); let transfer = token::Transfer::try_from_slice(&signed.data.unwrap()[..]).unwrap(); @@ -16,5 +16,5 @@ fn apply_tx(tx_data: Vec) { token, amount, } = transfer; - token::transfer(&source, &target, &token, amount) + token::transfer(ctx, &source, &target, &token, amount) } diff --git a/wasm/wasm_source/src/tx_unbond.rs b/wasm/wasm_source/src/tx_unbond.rs index 5d2662ed5c..c3eec798a5 100644 --- a/wasm/wasm_source/src/tx_unbond.rs +++ b/wasm/wasm_source/src/tx_unbond.rs @@ -1,20 +1,22 @@ //! A tx for a PoS unbond that removes staked tokens from a self-bond or a //! delegation to be withdrawn in or after unbonding epoch. -use namada_tx_prelude::proof_of_stake::unbond_tokens; use namada_tx_prelude::*; #[transaction] -fn apply_tx(tx_data: Vec) { +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); let unbond = transaction::pos::Unbond::try_from_slice(&signed.data.unwrap()[..]) .unwrap(); - if let Err(err) = - unbond_tokens(unbond.source.as_ref(), &unbond.validator, unbond.amount) - { + if let Err(err) = ctx.unbond_tokens( + unbond.source.as_ref(), + &unbond.validator, + unbond.amount, + ) { debug_log!("Unbonding failed with: {}", err); panic!() } + Ok(()) } diff --git a/wasm/wasm_source/src/tx_update_vp.rs b/wasm/wasm_source/src/tx_update_vp.rs index 4b68f11170..d6fdb16a2e 100644 --- a/wasm/wasm_source/src/tx_update_vp.rs +++ b/wasm/wasm_source/src/tx_update_vp.rs @@ -5,11 +5,11 @@ use namada_tx_prelude::*; #[transaction] -fn apply_tx(tx_data: Vec) { +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); let update_vp = transaction::UpdateVp::try_from_slice(&signed.data.unwrap()[..]) .unwrap(); debug_log!("update VP for: {:#?}", update_vp.addr); - update_validity_predicate(&update_vp.addr, update_vp.vp_code) + ctx.update_validity_predicate(&update_vp.addr, update_vp.vp_code) } diff --git a/wasm/wasm_source/src/tx_vote_proposal.rs b/wasm/wasm_source/src/tx_vote_proposal.rs index cae8c4ef33..30173d1b34 100644 --- a/wasm/wasm_source/src/tx_vote_proposal.rs +++ b/wasm/wasm_source/src/tx_vote_proposal.rs @@ -3,7 +3,7 @@ use namada_tx_prelude::*; #[transaction] -fn apply_tx(tx_data: Vec) { +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); let tx_data = transaction::governance::VoteProposalData::try_from_slice( &signed.data.unwrap()[..], @@ -11,5 +11,5 @@ fn apply_tx(tx_data: Vec) { .unwrap(); log_string("apply_tx called to vote a governance proposal"); - governance::vote_proposal(tx_data); + governance::vote_proposal(ctx, tx_data) } diff --git a/wasm/wasm_source/src/tx_withdraw.rs b/wasm/wasm_source/src/tx_withdraw.rs index 27bd984a66..adab55ca5a 100644 --- a/wasm/wasm_source/src/tx_withdraw.rs +++ b/wasm/wasm_source/src/tx_withdraw.rs @@ -1,17 +1,16 @@ //! A tx for a PoS unbond that removes staked tokens from a self-bond or a //! delegation to be withdrawn in or after unbonding epoch. -use namada_tx_prelude::proof_of_stake::withdraw_tokens; use namada_tx_prelude::*; #[transaction] -fn apply_tx(tx_data: Vec) { +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); let withdraw = transaction::pos::Withdraw::try_from_slice(&signed.data.unwrap()[..]) .unwrap(); - match withdraw_tokens(withdraw.source.as_ref(), &withdraw.validator) { + match ctx.withdraw_tokens(withdraw.source.as_ref(), &withdraw.validator) { Ok(slashed) => { debug_log!("Withdrawal slashed for {}", slashed); } @@ -20,4 +19,5 @@ fn apply_tx(tx_data: Vec) { panic!() } } + Ok(()) } diff --git a/wasm/wasm_source/src/vp_nft.rs b/wasm/wasm_source/src/vp_nft.rs index f1e6dd587b..4ab7f232bd 100644 --- a/wasm/wasm_source/src/vp_nft.rs +++ b/wasm/wasm_source/src/vp_nft.rs @@ -4,33 +4,36 @@ use namada_vp_prelude::*; #[validity_predicate] fn validate_tx( + ctx: &Ctx, tx_data: Vec, addr: Address, keys_changed: BTreeSet, verifiers: BTreeSet
, -) -> bool { +) -> VpResult { log_string(format!( "validate_tx called with token addr: {}, key_changed: {:#?}, \ verifiers: {:?}", addr, keys_changed, verifiers )); - if !is_tx_whitelisted() { - return false; + if !is_tx_whitelisted(ctx)? { + return reject(); } - let vp_check = - keys_changed - .iter() - .all(|key| match key.is_validity_predicate() { - Some(_) => { - let vp: Vec = read_bytes_post(key.to_string()).unwrap(); - is_vp_whitelisted(&vp) + let vp_check = keys_changed.iter().all(|key| { + if key.is_validity_predicate().is_some() { + match ctx.read_bytes_post(key) { + Ok(Some(vp)) => { + matches!(is_vp_whitelisted(ctx, &vp), Ok(true)) } - None => true, - }); - - vp_check && nft::vp(tx_data, &addr, &keys_changed, &verifiers) + _ => false, + } + } else { + true + } + }); + + Ok(vp_check && nft::vp(ctx, tx_data, &addr, &keys_changed, &verifiers)?) } #[cfg(test)] @@ -38,8 +41,9 @@ mod tests { use namada::types::nft::{self, NftToken}; use namada::types::transaction::nft::{CreateNft, MintNft}; use namada_tests::log::test; - use namada_tests::tx::{tx_host_env, TestTxEnv}; + use namada_tests::tx::{self, tx_host_env, TestTxEnv}; use namada_tests::vp::*; + use namada_tx_prelude::TxEnv; use super::*; @@ -59,21 +63,25 @@ mod tests { std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); tx_host_env::set(tx_env); - let nft_address = tx_host_env::nft::init_nft(CreateNft { - tag: "v1".to_string(), - creator: nft_creator.clone(), - vp_code, - keys: vec![], - opt_keys: vec![], - tokens: vec![], - }); + let nft_address = tx_host_env::nft::init_nft( + tx::ctx(), + CreateNft { + tag: "v1".to_string(), + creator: nft_creator.clone(), + vp_code, + keys: vec![], + opt_keys: vec![], + tokens: vec![], + }, + ) + .unwrap(); let mut tx_env = tx_host_env::take(); tx_env.write_log.commit_tx(); vp_host_env::init_from_tx(nft_address.clone(), tx_env, |address| { // Apply transfer in a transaction - tx_host_env::insert_verifier(address) + tx::ctx().insert_verifier(address).unwrap() }); let vp_env = vp_host_env::take(); @@ -82,7 +90,10 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= vp_env.get_verifiers(); vp_host_env::set(vp_env); - assert!(validate_tx(tx_data, nft_address, keys_changed, verifiers)); + assert!( + validate_tx(&CTX, tx_data, nft_address, keys_changed, verifiers) + .unwrap() + ); } /// Test that you can create an nft without tokens @@ -98,26 +109,34 @@ mod tests { std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); tx_host_env::set(tx_env); - let nft_address = tx_host_env::nft::init_nft(CreateNft { - tag: "v1".to_string(), - creator: nft_creator.clone(), - vp_code, - keys: vec![], - opt_keys: vec![], - tokens: vec![], - }); + let nft_address = tx_host_env::nft::init_nft( + tx::ctx(), + CreateNft { + tag: "v1".to_string(), + creator: nft_creator.clone(), + vp_code, + keys: vec![], + opt_keys: vec![], + tokens: vec![], + }, + ) + .unwrap(); let mut tx_env = tx_host_env::take(); tx_env.write_log.commit_tx(); vp_host_env::init_from_tx(nft_address.clone(), tx_env, |address| { // Apply transfer in a transaction - tx_host_env::nft::mint_tokens(MintNft { - address: nft_address.clone(), - tokens: vec![], - creator: nft_creator.clone(), - }); - tx_host_env::insert_verifier(address) + tx_host_env::nft::mint_tokens( + tx::ctx(), + MintNft { + address: nft_address.clone(), + tokens: vec![], + creator: nft_creator.clone(), + }, + ) + .unwrap(); + tx::ctx().insert_verifier(address).unwrap() }); let vp_env = vp_host_env::take(); @@ -127,7 +146,10 @@ mod tests { let verifiers: BTreeSet
= vp_env.get_verifiers(); vp_host_env::set(vp_env); - assert!(validate_tx(tx_data, nft_address, keys_changed, verifiers)); + assert!( + validate_tx(&CTX, tx_data, nft_address, keys_changed, verifiers) + .unwrap() + ); } /// Test that you can create an nft with tokens @@ -144,34 +166,42 @@ mod tests { std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); tx_host_env::set(tx_env); - let nft_address = tx_host_env::nft::init_nft(CreateNft { - tag: "v1".to_string(), - creator: nft_creator.clone(), - vp_code, - keys: vec![], - opt_keys: vec![], - tokens: vec![], - }); + let nft_address = tx_host_env::nft::init_nft( + tx::ctx(), + CreateNft { + tag: "v1".to_string(), + creator: nft_creator.clone(), + vp_code, + keys: vec![], + opt_keys: vec![], + tokens: vec![], + }, + ) + .unwrap(); let mut tx_env = tx_host_env::take(); tx_env.commit_tx_and_block(); vp_host_env::init_from_tx(nft_address.clone(), tx_env, |_| { // Apply transfer in a transaction - tx_host_env::nft::mint_tokens(MintNft { - address: nft_address.clone(), - creator: nft_creator.clone(), - tokens: vec![NftToken { - id: 1, - values: vec![], - opt_values: vec![], - metadata: "".to_string(), - approvals: vec![], - current_owner: Some(nft_token_owner.clone()), - past_owners: vec![], - burnt: false, - }], - }); + tx_host_env::nft::mint_tokens( + tx::ctx(), + MintNft { + address: nft_address.clone(), + creator: nft_creator.clone(), + tokens: vec![NftToken { + id: 1, + values: vec![], + opt_values: vec![], + metadata: "".to_string(), + approvals: vec![], + current_owner: Some(nft_token_owner.clone()), + past_owners: vec![], + burnt: false, + }], + }, + ) + .unwrap(); }); let vp_env = vp_host_env::take(); @@ -181,7 +211,10 @@ mod tests { let verifiers: BTreeSet
= vp_env.get_verifiers(); vp_host_env::set(vp_env); - assert!(validate_tx(tx_data, nft_address, keys_changed, verifiers)); + assert!( + validate_tx(&CTX, tx_data, nft_address, keys_changed, verifiers) + .unwrap() + ); } /// Test that only owner can mint new tokens @@ -198,34 +231,42 @@ mod tests { std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); tx_host_env::set(tx_env); - let nft_address = tx_host_env::nft::init_nft(CreateNft { - tag: "v1".to_string(), - creator: nft_creator.clone(), - vp_code, - keys: vec![], - opt_keys: vec![], - tokens: vec![], - }); + let nft_address = tx_host_env::nft::init_nft( + tx::ctx(), + CreateNft { + tag: "v1".to_string(), + creator: nft_creator.clone(), + vp_code, + keys: vec![], + opt_keys: vec![], + tokens: vec![], + }, + ) + .unwrap(); let mut tx_env = tx_host_env::take(); tx_env.commit_tx_and_block(); vp_host_env::init_from_tx(nft_address.clone(), tx_env, |_| { // Apply transfer in a transaction - tx_host_env::nft::mint_tokens(MintNft { - address: nft_address.clone(), - creator: nft_token_owner.clone(), - tokens: vec![NftToken { - id: 1, - values: vec![], - opt_values: vec![], - metadata: "".to_string(), - approvals: vec![], - current_owner: Some(nft_token_owner.clone()), - past_owners: vec![], - burnt: false, - }], - }); + tx_host_env::nft::mint_tokens( + tx::ctx(), + MintNft { + address: nft_address.clone(), + creator: nft_token_owner.clone(), + tokens: vec![NftToken { + id: 1, + values: vec![], + opt_values: vec![], + metadata: "".to_string(), + approvals: vec![], + current_owner: Some(nft_token_owner.clone()), + past_owners: vec![], + burnt: false, + }], + }, + ) + .unwrap(); }); let vp_env = vp_host_env::take(); @@ -235,7 +276,10 @@ mod tests { let verifiers: BTreeSet
= vp_env.get_verifiers(); vp_host_env::set(vp_env); - assert!(!validate_tx(tx_data, nft_address, keys_changed, verifiers)); + assert!( + !validate_tx(&CTX, tx_data, nft_address, keys_changed, verifiers) + .unwrap() + ); } /// Test that an approval can add another approval @@ -259,45 +303,54 @@ mod tests { std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); tx_host_env::set(tx_env); - let nft_address = tx_host_env::nft::init_nft(CreateNft { - tag: "v1".to_string(), - creator: nft_creator.clone(), - vp_code, - keys: vec![], - opt_keys: vec![], - tokens: vec![], - }); + let nft_address = tx_host_env::nft::init_nft( + tx::ctx(), + CreateNft { + tag: "v1".to_string(), + creator: nft_creator.clone(), + vp_code, + keys: vec![], + opt_keys: vec![], + tokens: vec![], + }, + ) + .unwrap(); let mut tx_env = tx_host_env::take(); tx_env.commit_tx_and_block(); tx_host_env::set(tx_env); - tx_host_env::nft::mint_tokens(MintNft { - address: nft_address.clone(), - creator: nft_creator.clone(), - tokens: vec![NftToken { - id: 1, - values: vec![], - opt_values: vec![], - metadata: "".to_string(), - approvals: vec![nft_token_approval.clone()], - current_owner: None, - past_owners: vec![], - burnt: false, - }], - }); + tx_host_env::nft::mint_tokens( + tx::ctx(), + MintNft { + address: nft_address.clone(), + creator: nft_creator.clone(), + tokens: vec![NftToken { + id: 1, + values: vec![], + opt_values: vec![], + metadata: "".to_string(), + approvals: vec![nft_token_approval.clone()], + current_owner: None, + past_owners: vec![], + burnt: false, + }], + }, + ) + .unwrap(); let mut tx_env = tx_host_env::take(); tx_env.commit_tx_and_block(); vp_host_env::init_from_tx(nft_address.clone(), tx_env, |_| { - let approval_key = - nft::get_token_approval_key(&nft_address, "1").to_string(); - tx_host_env::write( - approval_key, - [&nft_token_approval_2, &nft_token_approval], - ); - tx_host_env::insert_verifier(&nft_token_approval); + let approval_key = nft::get_token_approval_key(&nft_address, "1"); + tx::ctx() + .write( + &approval_key, + [&nft_token_approval_2, &nft_token_approval], + ) + .unwrap(); + tx::ctx().insert_verifier(&nft_token_approval).unwrap(); }); let vp_env = vp_host_env::take(); @@ -307,7 +360,10 @@ mod tests { let verifiers: BTreeSet
= vp_env.get_verifiers(); vp_host_env::set(vp_env); - assert!(validate_tx(tx_data, nft_address, keys_changed, verifiers)); + assert!( + validate_tx(&CTX, tx_data, nft_address, keys_changed, verifiers) + .unwrap() + ); } /// Test that an approval can add another approval @@ -331,45 +387,54 @@ mod tests { std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); tx_host_env::set(tx_env); - let nft_address = tx_host_env::nft::init_nft(CreateNft { - tag: "v1".to_string(), - creator: nft_creator.clone(), - vp_code, - keys: vec![], - opt_keys: vec![], - tokens: vec![], - }); + let nft_address = tx_host_env::nft::init_nft( + tx::ctx(), + CreateNft { + tag: "v1".to_string(), + creator: nft_creator.clone(), + vp_code, + keys: vec![], + opt_keys: vec![], + tokens: vec![], + }, + ) + .unwrap(); let mut tx_env = tx_host_env::take(); tx_env.commit_tx_and_block(); tx_host_env::set(tx_env); - tx_host_env::nft::mint_tokens(MintNft { - address: nft_address.clone(), - creator: nft_creator.clone(), - tokens: vec![NftToken { - id: 1, - values: vec![], - opt_values: vec![], - metadata: "".to_string(), - approvals: vec![nft_token_approval.clone()], - current_owner: None, - past_owners: vec![], - burnt: false, - }], - }); + tx_host_env::nft::mint_tokens( + tx::ctx(), + MintNft { + address: nft_address.clone(), + creator: nft_creator.clone(), + tokens: vec![NftToken { + id: 1, + values: vec![], + opt_values: vec![], + metadata: "".to_string(), + approvals: vec![nft_token_approval.clone()], + current_owner: None, + past_owners: vec![], + burnt: false, + }], + }, + ) + .unwrap(); let mut tx_env = tx_host_env::take(); tx_env.commit_tx_and_block(); vp_host_env::init_from_tx(nft_address.clone(), tx_env, |_| { - let approval_key = - nft::get_token_approval_key(&nft_address, "1").to_string(); - tx_host_env::write( - approval_key, - [&nft_token_approval_2, &nft_token_approval], - ); - tx_host_env::insert_verifier(&nft_token_approval_2); + let approval_key = nft::get_token_approval_key(&nft_address, "1"); + tx::ctx() + .write( + &approval_key, + [&nft_token_approval_2, &nft_token_approval], + ) + .unwrap(); + tx::ctx().insert_verifier(&nft_token_approval_2).unwrap(); }); let vp_env = vp_host_env::take(); @@ -379,7 +444,10 @@ mod tests { let verifiers: BTreeSet
= vp_env.get_verifiers(); vp_host_env::set(vp_env); - assert!(!validate_tx(tx_data, nft_address, keys_changed, verifiers)); + assert!( + !validate_tx(&CTX, tx_data, nft_address, keys_changed, verifiers) + .unwrap() + ); } /// Test nft address cannot be changed @@ -396,21 +464,25 @@ mod tests { std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); tx_host_env::set(tx_env); - let nft_address = tx_host_env::nft::init_nft(CreateNft { - tag: "v1".to_string(), - creator: nft_owner.clone(), - vp_code, - keys: vec![], - opt_keys: vec![], - tokens: vec![], - }); + let nft_address = tx_host_env::nft::init_nft( + tx::ctx(), + CreateNft { + tag: "v1".to_string(), + creator: nft_owner.clone(), + vp_code, + keys: vec![], + opt_keys: vec![], + tokens: vec![], + }, + ) + .unwrap(); let mut tx_env = tx_host_env::take(); tx_env.commit_tx_and_block(); vp_host_env::init_from_tx(nft_address.clone(), tx_env, |_| { - let creator_key = nft::get_creator_key(&nft_address).to_string(); - tx_host_env::write(creator_key, &another_address); + let creator_key = nft::get_creator_key(&nft_address); + tx::ctx().write(&creator_key, &another_address).unwrap(); }); let vp_env = vp_host_env::take(); @@ -420,6 +492,9 @@ mod tests { let verifiers: BTreeSet
= vp_env.get_verifiers(); vp_host_env::set(vp_env); - assert!(!validate_tx(tx_data, nft_address, keys_changed, verifiers)); + assert!( + !validate_tx(&CTX, tx_data, nft_address, keys_changed, verifiers) + .unwrap() + ); } } diff --git a/wasm/wasm_source/src/vp_testnet_faucet.rs b/wasm/wasm_source/src/vp_testnet_faucet.rs index 553288926e..a3a5630ab7 100644 --- a/wasm/wasm_source/src/vp_testnet_faucet.rs +++ b/wasm/wasm_source/src/vp_testnet_faucet.rs @@ -13,11 +13,12 @@ pub const MAX_FREE_DEBIT: i128 = 1_000_000_000; // in micro units #[validity_predicate] fn validate_tx( + ctx: &Ctx, tx_data: Vec, addr: Address, keys_changed: BTreeSet, verifiers: BTreeSet
, -) -> bool { +) -> VpResult { debug_log!( "vp_testnet_faucet called with user addr: {}, key_changed: {:?}, \ verifiers: {:?}", @@ -31,26 +32,31 @@ fn validate_tx( let valid_sig = Lazy::new(|| match &*signed_tx_data { Ok(signed_tx_data) => { - let pk = key::get(&addr); + let pk = key::get(ctx, &addr); match pk { - Some(pk) => verify_tx_signature(&pk, &signed_tx_data.sig), - None => false, + Ok(Some(pk)) => { + matches!( + ctx.verify_tx_signature(&pk, &signed_tx_data.sig), + Ok(true) + ) + } + _ => false, } } _ => false, }); - if !is_tx_whitelisted() { - return false; + if !is_tx_whitelisted(ctx)? { + return reject(); } for key in keys_changed.iter() { let is_valid = if let Some(owner) = token::is_any_token_balance_key(key) { if owner == &addr { - let key = key.to_string(); - let pre: token::Amount = read_pre(&key).unwrap_or_default(); - let post: token::Amount = read_post(&key).unwrap_or_default(); + let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default(); + let post: token::Amount = + ctx.read_post(key)?.unwrap_or_default(); let change = post.change() - pre.change(); // Debit over `MAX_FREE_DEBIT` has to signed, credit doesn't change >= -MAX_FREE_DEBIT || change >= 0 || *valid_sig @@ -59,18 +65,17 @@ fn validate_tx( true } } else if let Some(owner) = key.is_validity_predicate() { - let key = key.to_string(); - let has_post: bool = has_key_post(&key); + let has_post: bool = ctx.has_key_post(key)?; if owner == &addr { if has_post { - let vp: Vec = read_bytes_post(&key).unwrap(); - return *valid_sig && is_vp_whitelisted(&vp); + let vp: Vec = ctx.read_bytes_post(key)?.unwrap(); + return Ok(*valid_sig && is_vp_whitelisted(ctx, &vp)?); } else { - return false; + return reject(); } } else { - let vp: Vec = read_bytes_post(&key).unwrap(); - return is_vp_whitelisted(&vp); + let vp: Vec = ctx.read_bytes_post(key)?.unwrap(); + return is_vp_whitelisted(ctx, &vp); } } else { // Allow any other key change if authorized by a signature @@ -78,10 +83,10 @@ fn validate_tx( }; if !is_valid { debug_log!("key {} modification failed vp", key); - return false; + return reject(); } } - true + accept() } #[cfg(test)] @@ -89,9 +94,10 @@ mod tests { use address::testing::arb_non_internal_address; // Use this as `#[test]` annotation to enable logging use namada_tests::log::test; - use namada_tests::tx::{tx_host_env, TestTxEnv}; + use namada_tests::tx::{self, tx_host_env, TestTxEnv}; use namada_tests::vp::vp_host_env::storage::Key; use namada_tests::vp::*; + use namada_tx_prelude::TxEnv; use namada_vp_prelude::key::RefTo; use proptest::prelude::*; use storage::testing::arb_account_storage_key_no_vp; @@ -112,7 +118,9 @@ mod tests { // The VP env must be initialized before calling `validate_tx` vp_host_env::init(); - assert!(validate_tx(tx_data, addr, keys_changed, verifiers)); + assert!( + validate_tx(&CTX, tx_data, addr, keys_changed, verifiers).unwrap() + ); } /// Test that a credit transfer is accepted. @@ -136,7 +144,14 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction - tx_host_env::token::transfer(&source, address, &token, amount); + tx_host_env::token::transfer( + tx_host_env::ctx(), + &source, + address, + &token, + amount, + ) + .unwrap(); }); let vp_env = vp_host_env::take(); @@ -145,7 +160,10 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!( + validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); } /// Test that a validity predicate update without a valid signature is @@ -165,7 +183,9 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Update VP in a transaction - tx_host_env::update_validity_predicate(address, &vp_code); + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); }); let vp_env = vp_host_env::take(); @@ -174,7 +194,10 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(!validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!( + !validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); } /// Test that a validity predicate update with a valid signature is @@ -198,7 +221,9 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Update VP in a transaction - tx_host_env::update_validity_predicate(address, &vp_code); + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); }); let mut vp_env = vp_host_env::take(); @@ -210,7 +235,10 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!( + validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); } prop_compose! { @@ -251,7 +279,7 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction - tx_host_env::token::transfer(address, &target, &token, amount); + tx_host_env::token::transfer(tx::ctx(), address, &target, &token, amount).unwrap(); }); let vp_env = vp_host_env::take(); @@ -260,7 +288,7 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(!validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!(!validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers).unwrap()); } /// Test that a debit of less than or equal to [`MAX_FREE_DEBIT`] tokens without a valid signature is accepted. @@ -284,7 +312,7 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction - tx_host_env::token::transfer(address, &target, &token, amount); + tx_host_env::token::transfer(tx::ctx(), address, &target, &token, amount).unwrap(); }); let vp_env = vp_host_env::take(); @@ -293,7 +321,7 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!(validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers).unwrap()); } /// Test that a signed tx that performs arbitrary storage writes or @@ -321,9 +349,9 @@ mod tests { vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { // Write or delete some data in the transaction if let Some(value) = &storage_value { - tx_host_env::write(storage_key.to_string(), value); + tx::ctx().write(&storage_key, value).unwrap(); } else { - tx_host_env::delete(storage_key.to_string()); + tx::ctx().delete(&storage_key).unwrap(); } }); @@ -336,7 +364,7 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!(validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers).unwrap()); } } } diff --git a/wasm/wasm_source/src/vp_token.rs b/wasm/wasm_source/src/vp_token.rs index 60513ce808..b9d3de8f7d 100644 --- a/wasm/wasm_source/src/vp_token.rs +++ b/wasm/wasm_source/src/vp_token.rs @@ -5,11 +5,12 @@ use namada_vp_prelude::*; #[validity_predicate] fn validate_tx( + ctx: &Ctx, _tx_data: Vec, addr: Address, keys_changed: BTreeSet, verifiers: BTreeSet
, -) -> bool { +) -> VpResult { debug_log!( "validate_tx called with token addr: {}, key_changed: {:?}, \ verifiers: {:?}", @@ -18,20 +19,18 @@ fn validate_tx( verifiers ); - if !is_tx_whitelisted() { - return false; + if !is_tx_whitelisted(ctx)? { + return reject(); } - let vp_check = - keys_changed - .iter() - .all(|key| match key.is_validity_predicate() { - Some(_) => { - let vp: Vec = read_bytes_post(key.to_string()).unwrap(); - is_vp_whitelisted(&vp) - } - None => true, - }); + for key in keys_changed.iter() { + if key.is_validity_predicate().is_some() { + let vp: Vec = ctx.read_bytes_post(key)?.unwrap(); + if !is_vp_whitelisted(ctx, &vp)? { + return reject(); + } + } + } - vp_check && token::vp(&addr, &keys_changed, &verifiers) + token::vp(ctx, &addr, &keys_changed, &verifiers) } diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index a222a344ef..3b6ddcf65f 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -57,11 +57,12 @@ impl<'a> From<&'a storage::Key> for KeyType<'a> { #[validity_predicate] fn validate_tx( + ctx: &Ctx, tx_data: Vec, addr: Address, keys_changed: BTreeSet, verifiers: BTreeSet
, -) -> bool { +) -> VpResult { debug_log!( "vp_user called with user addr: {}, key_changed: {:?}, verifiers: {:?}", addr, @@ -74,22 +75,32 @@ fn validate_tx( let valid_sig = Lazy::new(|| match &*signed_tx_data { Ok(signed_tx_data) => { - let pk = key::get(&addr); + let pk = key::get(ctx, &addr); match pk { - Some(pk) => verify_tx_signature(&pk, &signed_tx_data.sig), - None => false, + Ok(Some(pk)) => { + matches!( + ctx.verify_tx_signature(&pk, &signed_tx_data.sig), + Ok(true) + ) + } + _ => false, } } _ => false, }); let valid_intent = Lazy::new(|| match &*signed_tx_data { - Ok(signed_tx_data) => check_intent_transfers(&addr, signed_tx_data), + Ok(signed_tx_data) => { + matches!( + check_intent_transfers(ctx, &addr, signed_tx_data), + Ok(true) + ) + } _ => false, }); - if !is_tx_whitelisted() { - return false; + if !is_tx_whitelisted(ctx)? { + return reject(); } for key in keys_changed.iter() { @@ -97,10 +108,10 @@ fn validate_tx( let is_valid = match key_type { KeyType::Token(owner) => { if owner == &addr { - let key = key.to_string(); - let pre: token::Amount = read_pre(&key).unwrap_or_default(); + let pre: token::Amount = + ctx.read_pre(key)?.unwrap_or_default(); let post: token::Amount = - read_post(&key).unwrap_or_default(); + ctx.read_post(key)?.unwrap_or_default(); let change = post.change() - pre.change(); // debit has to signed, credit doesn't let valid = change >= 0 || *valid_sig || *valid_intent; @@ -150,11 +161,10 @@ fn validate_tx( } KeyType::InvalidIntentSet(owner) => { if owner == &addr { - let key = key.to_string(); let pre: HashSet = - read_pre(&key).unwrap_or_default(); + ctx.read_pre(key)?.unwrap_or_default(); let post: HashSet = - read_post(&key).unwrap_or_default(); + ctx.read_post(key)?.unwrap_or_default(); // A new invalid intent must have been added pre.len() + 1 == post.len() } else { @@ -184,18 +194,17 @@ fn validate_tx( } } KeyType::Vp(owner) => { - let key = key.to_string(); - let has_post: bool = has_key_post(&key); + let has_post: bool = ctx.has_key_post(key)?; if owner == &addr { if has_post { - let vp: Vec = read_bytes_post(&key).unwrap(); - return *valid_sig && is_vp_whitelisted(&vp); + let vp: Vec = ctx.read_bytes_post(key)?.unwrap(); + *valid_sig && is_vp_whitelisted(ctx, &vp)? } else { - return false; + false } } else { - let vp: Vec = read_bytes_post(&key).unwrap(); - return is_vp_whitelisted(&vp); + let vp: Vec = ctx.read_bytes_post(key)?.unwrap(); + is_vp_whitelisted(ctx, &vp)? } } KeyType::Unknown => { @@ -211,24 +220,25 @@ fn validate_tx( }; if !is_valid { debug_log!("key {} modification failed vp", key); - return false; + return reject(); } } - true + accept() } fn check_intent_transfers( + ctx: &Ctx, addr: &Address, signed_tx_data: &SignedTxData, -) -> bool { +) -> EnvResult { if let Some((raw_intent_transfers, exchange, intent)) = try_decode_intent(addr, signed_tx_data) { log_string("check intent"); - return check_intent(addr, exchange, intent, raw_intent_transfers); + return check_intent(ctx, addr, exchange, intent, raw_intent_transfers); } - false + reject() } fn try_decode_intent( @@ -259,25 +269,26 @@ fn try_decode_intent( } fn check_intent( + ctx: &Ctx, addr: &Address, exchange: namada_vp_prelude::Signed, intent: namada_vp_prelude::Signed, raw_intent_transfers: Vec, -) -> bool { +) -> EnvResult { // verify signature - let pk = key::get(addr); + let pk = key::get(ctx, addr)?; if let Some(pk) = pk { if intent.verify(&pk).is_err() { log_string("invalid sig"); - return false; + return reject(); } } else { - return false; + return reject(); } // verify the intent have not been already used - if !intent::vp_exchange(&exchange) { - return false; + if !intent::vp_exchange(ctx, &exchange)? { + return reject(); } // verify the intent is fulfilled @@ -294,10 +305,10 @@ fn check_intent( debug_log!("vp is: {}", vp.is_some()); if let Some(code) = vp { - let eval_result = eval(code.to_vec(), raw_intent_transfers); + let eval_result = ctx.eval(code.to_vec(), raw_intent_transfers)?; debug_log!("eval result: {}", eval_result); if !eval_result { - return false; + return reject(); } } @@ -310,18 +321,19 @@ fn check_intent( rate_min.0 ); - let token_sell_key = token::balance_key(token_sell, addr).to_string(); + let token_sell_key = token::balance_key(token_sell, addr); let mut sell_difference: token::Amount = - read_pre(&token_sell_key).unwrap_or_default(); + ctx.read_pre(&token_sell_key)?.unwrap_or_default(); let sell_post: token::Amount = - read_post(token_sell_key).unwrap_or_default(); + ctx.read_post(&token_sell_key)?.unwrap_or_default(); sell_difference.spend(&sell_post); - let token_buy_key = token::balance_key(token_buy, addr).to_string(); - let buy_pre: token::Amount = read_pre(&token_buy_key).unwrap_or_default(); + let token_buy_key = token::balance_key(token_buy, addr); + let buy_pre: token::Amount = + ctx.read_pre(&token_buy_key)?.unwrap_or_default(); let mut buy_difference: token::Amount = - read_post(token_buy_key).unwrap_or_default(); + ctx.read_post(&token_buy_key)?.unwrap_or_default(); buy_difference.spend(&buy_pre); @@ -354,9 +366,9 @@ fn check_intent( min_buy.change(), buy_diff / sell_diff ); - false + reject() } else { - true + accept() } } @@ -365,9 +377,10 @@ mod tests { use address::testing::arb_non_internal_address; // Use this as `#[test]` annotation to enable logging use namada_tests::log::test; - use namada_tests::tx::{tx_host_env, TestTxEnv}; + use namada_tests::tx::{self, tx_host_env, TestTxEnv}; use namada_tests::vp::vp_host_env::storage::Key; use namada_tests::vp::*; + use namada_tx_prelude::TxEnv; use namada_vp_prelude::key::RefTo; use proptest::prelude::*; use storage::testing::arb_account_storage_key_no_vp; @@ -388,7 +401,9 @@ mod tests { // The VP env must be initialized before calling `validate_tx` vp_host_env::init(); - assert!(validate_tx(tx_data, addr, keys_changed, verifiers)); + assert!( + validate_tx(&CTX, tx_data, addr, keys_changed, verifiers).unwrap() + ); } /// Test that a credit transfer is accepted. @@ -412,7 +427,14 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction - tx_host_env::token::transfer(&source, address, &token, amount); + tx_host_env::token::transfer( + tx::ctx(), + &source, + address, + &token, + amount, + ) + .unwrap(); }); let vp_env = vp_host_env::take(); @@ -421,7 +443,10 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!( + validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); } /// Test that a debit transfer without a valid signature is rejected. @@ -445,7 +470,14 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction - tx_host_env::token::transfer(address, &target, &token, amount); + tx_host_env::token::transfer( + tx::ctx(), + address, + &target, + &token, + amount, + ) + .unwrap(); }); let vp_env = vp_host_env::take(); @@ -454,7 +486,10 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(!validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!( + !validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); } /// Test that a debit transfer with a valid signature is accepted. @@ -482,7 +517,14 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction - tx_host_env::token::transfer(address, &target, &token, amount); + tx_host_env::token::transfer( + tx::ctx(), + address, + &target, + &token, + amount, + ) + .unwrap(); }); let mut vp_env = vp_host_env::take(); @@ -494,7 +536,10 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!( + validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); } /// Test that a transfer on with accounts other than self is accepted. @@ -518,9 +563,16 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { - tx_host_env::insert_verifier(address); + tx::ctx().insert_verifier(address).unwrap(); // Apply transfer in a transaction - tx_host_env::token::transfer(&source, &target, &token, amount); + tx_host_env::token::transfer( + tx::ctx(), + &source, + &target, + &token, + amount, + ) + .unwrap(); }); let vp_env = vp_host_env::take(); @@ -529,7 +581,10 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!( + validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); } prop_compose! { @@ -569,9 +624,9 @@ mod tests { vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { // Write or delete some data in the transaction if let Some(value) = &storage_value { - tx_host_env::write(storage_key.to_string(), value); + tx::ctx().write(&storage_key, value).unwrap(); } else { - tx_host_env::delete(storage_key.to_string()); + tx::ctx().delete(&storage_key).unwrap(); } }); @@ -581,7 +636,7 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(!validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!(!validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers).unwrap()); } } @@ -611,9 +666,9 @@ mod tests { vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { // Write or delete some data in the transaction if let Some(value) = &storage_value { - tx_host_env::write(storage_key.to_string(), value); + tx::ctx().write(&storage_key, value).unwrap(); } else { - tx_host_env::delete(storage_key.to_string()); + tx::ctx().delete(&storage_key).unwrap(); } }); @@ -626,7 +681,7 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!(validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers).unwrap()); } } @@ -647,7 +702,9 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Update VP in a transaction - tx_host_env::update_validity_predicate(address, &vp_code); + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); }); let vp_env = vp_host_env::take(); @@ -656,7 +713,10 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(!validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!( + !validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); } /// Test that a validity predicate update with a valid signature is @@ -681,7 +741,9 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Update VP in a transaction - tx_host_env::update_validity_predicate(address, &vp_code); + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); }); let mut vp_env = vp_host_env::take(); @@ -693,7 +755,10 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!( + validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); } /// Test that a validity predicate update is rejected if not whitelisted @@ -717,7 +782,9 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Update VP in a transaction - tx_host_env::update_validity_predicate(address, &vp_code); + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); }); let mut vp_env = vp_host_env::take(); @@ -729,7 +796,10 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(!validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!( + !validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); } /// Test that a validity predicate update is accepted if whitelisted @@ -755,7 +825,9 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Update VP in a transaction - tx_host_env::update_validity_predicate(address, &vp_code); + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); }); let mut vp_env = vp_host_env::take(); @@ -767,7 +839,10 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!( + validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); } /// Test that a tx is rejected if not whitelisted @@ -797,7 +872,9 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Update VP in a transaction - tx_host_env::update_validity_predicate(address, &vp_code); + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); }); let mut vp_env = vp_host_env::take(); @@ -809,7 +886,10 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(!validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!( + !validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); } #[test] @@ -834,7 +914,9 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Update VP in a transaction - tx_host_env::update_validity_predicate(address, &vp_code); + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); }); let mut vp_env = vp_host_env::take(); @@ -846,6 +928,9 @@ mod tests { vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); - assert!(validate_tx(tx_data, vp_owner, keys_changed, verifiers)); + assert!( + validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); } } diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 39f70c1787..3e7bbbe365 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1432,7 +1432,8 @@ dependencies = [ "concat-idents", "derivative", "namada", - "namada_vm_env", + "namada_tx_prelude", + "namada_vp_prelude", "prost", "serde_json", "sha2 0.9.9", @@ -1446,8 +1447,12 @@ dependencies = [ name = "namada_tx_prelude" version = "0.7.0" dependencies = [ + "borsh", + "namada", + "namada_macros", "namada_vm_env", "sha2 0.10.2", + "thiserror", ] [[package]] @@ -1455,17 +1460,19 @@ name = "namada_vm_env" version = "0.7.0" dependencies = [ "borsh", - "hex", "namada", - "namada_macros", ] [[package]] name = "namada_vp_prelude" version = "0.7.0" dependencies = [ + "borsh", + "namada", + "namada_macros", "namada_vm_env", "sha2 0.10.2", + "thiserror", ] [[package]] @@ -1476,7 +1483,6 @@ dependencies = [ "getrandom", "namada_tests", "namada_tx_prelude", - "namada_vm_env", "namada_vp_prelude", "wee_alloc", ] diff --git a/wasm_for_tests/wasm_source/Cargo.toml b/wasm_for_tests/wasm_source/Cargo.toml index cdd56aaf89..8f3cc9cc36 100644 --- a/wasm_for_tests/wasm_source/Cargo.toml +++ b/wasm_for_tests/wasm_source/Cargo.toml @@ -26,7 +26,6 @@ tx_proposal_code = [] [dependencies] namada_tx_prelude = {path = "../../tx_prelude"} -namada_vm_env = {path = "../../vm_env"} namada_vp_prelude = {path = "../../vp_prelude"} borsh = "0.9.1" wee_alloc = "0.4.5" diff --git a/wasm_for_tests/wasm_source/src/lib.rs b/wasm_for_tests/wasm_source/src/lib.rs index 674fb2a2d9..215a967846 100644 --- a/wasm_for_tests/wasm_source/src/lib.rs +++ b/wasm_for_tests/wasm_source/src/lib.rs @@ -1,66 +1,71 @@ /// A tx that doesn't do anything. #[cfg(feature = "tx_no_op")] pub mod main { - use namada_vm_env::tx_prelude::*; + use namada_tx_prelude::*; #[transaction] - fn apply_tx(_tx_data: Vec) {} + fn apply_tx(_ctx: &mut Ctx, _tx_data: Vec) -> TxResult { + Ok(()) + } } /// A tx that allocates a memory of size given from the `tx_data: usize`. #[cfg(feature = "tx_memory_limit")] pub mod main { - use namada_vm_env::tx_prelude::*; + use namada_tx_prelude::*; #[transaction] - fn apply_tx(tx_data: Vec) { + fn apply_tx(_ctx: &mut Ctx, tx_data: Vec) -> TxResult { let len = usize::try_from_slice(&tx_data[..]).unwrap(); log_string(format!("allocate len {}", len)); let bytes: Vec = vec![6_u8; len]; // use the variable to prevent it from compiler optimizing it away log_string(format!("{:?}", &bytes[..8])); + Ok(()) } } /// A tx to be used as proposal_code #[cfg(feature = "tx_proposal_code")] pub mod main { - use namada_vm_env::tx_prelude::*; + use namada_tx_prelude::*; #[transaction] - fn apply_tx(_tx_data: Vec) { + fn apply_tx(ctx: &mut Ctx, _tx_data: Vec) -> TxResult { // governance - let target_key = storage::get_min_proposal_grace_epoch_key(); - write(&target_key.to_string(), 9_u64); + let target_key = gov_storage::get_min_proposal_grace_epoch_key(); + ctx.write(&target_key, 9_u64)?; // treasury let target_key = treasury_storage::get_max_transferable_fund_key(); - write(&target_key.to_string(), token::Amount::whole(20_000)); + ctx.write(&target_key, token::Amount::whole(20_000))?; // parameters let target_key = parameters_storage::get_tx_whitelist_storage_key(); - write(&target_key.to_string(), vec!["hash"]); + ctx.write(&target_key, vec!["hash"])?; + Ok(()) } } /// A tx that attempts to read the given key from storage. #[cfg(feature = "tx_read_storage_key")] pub mod main { - use namada_vm_env::tx_prelude::*; + use namada_tx_prelude::*; #[transaction] - fn apply_tx(tx_data: Vec) { + fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { // Allocates a memory of size given from the `tx_data (usize)` - let key = Key::try_from_slice(&tx_data[..]).unwrap(); + let key = storage::Key::try_from_slice(&tx_data[..]).unwrap(); log_string(format!("key {}", key)); - let _result: Vec = read(key.to_string()).unwrap(); + let _result: Vec = ctx.read(&key)?.unwrap(); + Ok(()) } } /// A tx that attempts to write arbitrary data to the given key #[cfg(feature = "tx_write_storage_key")] pub mod main { - use namada_vm_env::tx_prelude::*; + use namada_tx_prelude::*; const TX_NAME: &str = "tx_write"; @@ -81,7 +86,7 @@ pub mod main { } #[transaction] - fn apply_tx(tx_data: Vec) { + fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = match SignedTxData::try_from_slice(&tx_data[..]) { Ok(signed) => { log("got signed data"); @@ -100,15 +105,15 @@ pub mod main { }; let key = match String::from_utf8(data) { Ok(key) => { + let key = storage::Key::parse(key).unwrap(); log(&format!("parsed key from data: {}", key)); key } Err(error) => fatal("getting key", error), }; - let val: Option> = read(key.as_str()); + let val: Option = ctx.read(&key)?; match val { Some(val) => { - let val = String::from_utf8(val).unwrap(); log(&format!("preexisting val is {}", val)); } None => { @@ -119,7 +124,7 @@ pub mod main { "attempting to write new value {} to key {}", ARBITRARY_VALUE, key )); - write(key.as_str(), ARBITRARY_VALUE); + ctx.write(&key, ARBITRARY_VALUE) } } @@ -128,10 +133,10 @@ pub mod main { /// token's VP. #[cfg(feature = "tx_mint_tokens")] pub mod main { - use namada_vm_env::tx_prelude::*; + use namada_tx_prelude::*; #[transaction] - fn apply_tx(tx_data: Vec) { + fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); let transfer = token::Transfer::try_from_slice(&signed.data.unwrap()[..]).unwrap(); @@ -144,41 +149,43 @@ pub mod main { } = transfer; let target_key = token::balance_key(&token, &target); let mut target_bal: token::Amount = - read(&target_key.to_string()).unwrap_or_default(); + ctx.read(&target_key)?.unwrap_or_default(); target_bal.receive(&amount); - write(&target_key.to_string(), target_bal); + ctx.write(&target_key, target_bal) } } /// A VP that always returns `true`. #[cfg(feature = "vp_always_true")] pub mod main { - use namada_vm_env::vp_prelude::*; + use namada_vp_prelude::*; #[validity_predicate] fn validate_tx( + _ctx: &Ctx, _tx_data: Vec, _addr: Address, _keys_changed: BTreeSet, _verifiers: BTreeSet
, - ) -> bool { - true + ) -> VpResult { + accept() } } /// A VP that always returns `false`. #[cfg(feature = "vp_always_false")] pub mod main { - use namada_vm_env::vp_prelude::*; + use namada_vp_prelude::*; #[validity_predicate] fn validate_tx( + _ctx: &Ctx, _tx_data: Vec, _addr: Address, _keys_changed: BTreeSet, _verifiers: BTreeSet
, - ) -> bool { - false + ) -> VpResult { + reject() } } @@ -186,19 +193,20 @@ pub mod main { /// of `eval`. #[cfg(feature = "vp_eval")] pub mod main { - use namada_vm_env::vp_prelude::*; + use namada_vp_prelude::*; #[validity_predicate] fn validate_tx( + ctx: &Ctx, tx_data: Vec, _addr: Address, _keys_changed: BTreeSet, _verifiers: BTreeSet
, - ) -> bool { + ) -> VpResult { use validity_predicate::EvalVp; let EvalVp { vp_code, input }: EvalVp = EvalVp::try_from_slice(&tx_data[..]).unwrap(); - eval(vp_code, input) + ctx.eval(vp_code, input) } } @@ -206,21 +214,22 @@ pub mod main { // Returns `true`, if the allocation is within memory limits. #[cfg(feature = "vp_memory_limit")] pub mod main { - use namada_vm_env::vp_prelude::*; + use namada_vp_prelude::*; #[validity_predicate] fn validate_tx( + _ctx: &Ctx, tx_data: Vec, _addr: Address, _keys_changed: BTreeSet, _verifiers: BTreeSet
, - ) -> bool { + ) -> VpResult { let len = usize::try_from_slice(&tx_data[..]).unwrap(); log_string(format!("allocate len {}", len)); let bytes: Vec = vec![6_u8; len]; // use the variable to prevent it from compiler optimizing it away log_string(format!("{:?}", &bytes[..8])); - true + accept() } } @@ -228,19 +237,20 @@ pub mod main { /// execution). Returns `true`, if the allocation is within memory limits. #[cfg(feature = "vp_read_storage_key")] pub mod main { - use namada_vm_env::vp_prelude::*; + use namada_vp_prelude::*; #[validity_predicate] fn validate_tx( + ctx: &Ctx, tx_data: Vec, _addr: Address, _keys_changed: BTreeSet, _verifiers: BTreeSet
, - ) -> bool { + ) -> VpResult { // Allocates a memory of size given from the `tx_data (usize)` - let key = Key::try_from_slice(&tx_data[..]).unwrap(); + let key = storage::Key::try_from_slice(&tx_data[..]).unwrap(); log_string(format!("key {}", key)); - let _result: Vec = read_pre(key.to_string()).unwrap(); - true + let _result: Vec = ctx.read_pre(&key)?.unwrap(); + accept() } } From bf604a4c3a7b9b6084d20284c4df2495f212feaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 20 Jul 2022 21:03:53 +0200 Subject: [PATCH 08/81] macros: add error handling to transaction and validity_predicate macros --- macros/src/lib.rs | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index afa49c66ab..33e729dca4 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -15,7 +15,10 @@ use syn::{parse_macro_input, DeriveInput, ItemFn}; /// This macro expects a function with signature: /// /// ```compiler_fail -/// fn apply_tx(tx_data: Vec) +/// fn apply_tx( +/// ctx: &mut Ctx, +/// tx_data: Vec +/// ) -> TxResult /// ``` #[proc_macro_attribute] pub fn transaction(_attr: TokenStream, input: TokenStream) -> TokenStream { @@ -38,7 +41,19 @@ pub fn transaction(_attr: TokenStream, input: TokenStream) -> TokenStream { ) }; let tx_data = slice.to_vec(); - #ident(tx_data); + + // The context on WASM side is only provided by the VM once its + // being executed (in here it's implicit). But because we want to + // have interface consistent with the VP interface, in which the + // context is explicit, in here we're just using an empty `Ctx` + // to "fake" it. + let mut ctx = unsafe { namada_tx_prelude::Ctx::new() }; + + if let Err(err) = #ident(&mut ctx, tx_data) { + namada_tx_prelude::debug_log!("Transaction error: {}", err); + // crash the transaction to abort + panic!(); + } } }; TokenStream::from(gen) @@ -50,11 +65,12 @@ pub fn transaction(_attr: TokenStream, input: TokenStream) -> TokenStream { /// /// ```compiler_fail /// fn validate_tx( +/// ctx: &Ctx, /// tx_data: Vec, /// addr: Address, /// keys_changed: BTreeSet, /// verifiers: BTreeSet
-/// ) -> bool +/// ) -> VpResult /// ``` #[proc_macro_attribute] pub fn validity_predicate( @@ -74,7 +90,6 @@ pub fn validity_predicate( #[no_mangle] extern "C" fn _validate_tx( // VP's account's address - // TODO Should the address be on demand (a call to host function?) addr_ptr: u64, addr_len: u64, tx_data_ptr: u64, @@ -113,11 +128,22 @@ pub fn validity_predicate( }; let verifiers: BTreeSet
= BTreeSet::try_from_slice(slice).unwrap(); + // The context on WASM side is only provided by the VM once its + // being executed (in here it's implicit). But because we want to + // have interface identical with the native VPs, in which the + // context is explicit, in here we're just using an empty `Ctx` + // to "fake" it. + let ctx = unsafe { namada_vp_prelude::Ctx::new() }; + // run validation with the concrete type(s) - if #ident(tx_data, addr, keys_changed, verifiers) { - 1 - } else { - 0 + match #ident(&ctx, tx_data, addr, keys_changed, verifiers) + { + Ok(true) => 1, + Ok(false) => 0, + Err(err) => { + namada_vp_prelude::debug_log!("Validity predicate error: {}", err); + 0 + }, } } }; From d868424682a67517b53cbda42bc9c1ed6c97d7e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 21 Jul 2022 12:44:30 +0200 Subject: [PATCH 09/81] wasm: improve error handling --- wasm/wasm_source/src/tx_bond.rs | 17 ++++++----------- wasm/wasm_source/src/tx_from_intent.rs | 11 +++++------ wasm/wasm_source/src/tx_ibc.rs | 6 ++++-- wasm/wasm_source/src/tx_init_account.rs | 9 +++++---- wasm/wasm_source/src/tx_init_nft.rs | 9 +++++---- wasm/wasm_source/src/tx_init_proposal.rs | 11 ++++++----- wasm/wasm_source/src/tx_init_validator.rs | 8 +++++--- wasm/wasm_source/src/tx_mint_nft.rs | 9 +++++---- wasm/wasm_source/src/tx_transfer.rs | 8 +++++--- wasm/wasm_source/src/tx_unbond.rs | 19 ++++++------------- wasm/wasm_source/src/tx_update_vp.rs | 11 +++++++---- wasm/wasm_source/src/tx_vote_proposal.rs | 14 ++++++++------ wasm/wasm_source/src/tx_withdraw.rs | 21 +++++++++------------ wasm_for_tests/wasm_source/src/lib.rs | 12 ++++-------- 14 files changed, 80 insertions(+), 85 deletions(-) diff --git a/wasm/wasm_source/src/tx_bond.rs b/wasm/wasm_source/src/tx_bond.rs index 0d9d390226..9b04e2a939 100644 --- a/wasm/wasm_source/src/tx_bond.rs +++ b/wasm/wasm_source/src/tx_bond.rs @@ -4,16 +4,11 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); - let bond = - transaction::pos::Bond::try_from_slice(&signed.data.unwrap()[..]) - .unwrap(); + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .err_msg("failed to decode SignedTxData")?; + let data = signed.data.ok_or_err_msg("Missing data")?; + let bond = transaction::pos::Bond::try_from_slice(&data[..]) + .err_msg("failed to decode Bond")?; - if let Err(err) = - ctx.bond_tokens(bond.source.as_ref(), &bond.validator, bond.amount) - { - debug_log!("Unbonding failed with: {}", err); - panic!() - } - Ok(()) + ctx.bond_tokens(bond.source.as_ref(), &bond.validator, bond.amount) } diff --git a/wasm/wasm_source/src/tx_from_intent.rs b/wasm/wasm_source/src/tx_from_intent.rs index 9b5afb5ad0..a299070393 100644 --- a/wasm/wasm_source/src/tx_from_intent.rs +++ b/wasm/wasm_source/src/tx_from_intent.rs @@ -6,12 +6,11 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); - - let tx_data = - intent::IntentTransfers::try_from_slice(&signed.data.unwrap()[..]); - - let tx_data = tx_data.unwrap(); + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .err_msg("failed to decode SignedTxData")?; + let data = signed.data.ok_or_err_msg("Missing data")?; + let tx_data = intent::IntentTransfers::try_from_slice(&data[..]) + .err_msg("failed to decode IntentTransfers")?; // make sure that the matchmaker has to validate this tx ctx.insert_verifier(&tx_data.source)?; diff --git a/wasm/wasm_source/src/tx_ibc.rs b/wasm/wasm_source/src/tx_ibc.rs index 188ec1ae8d..08b3c60d60 100644 --- a/wasm/wasm_source/src/tx_ibc.rs +++ b/wasm/wasm_source/src/tx_ibc.rs @@ -7,6 +7,8 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); - ctx.dispatch_ibc_action(&signed.data.unwrap()) + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .err_msg("failed to decode SignedTxData")?; + let data = signed.data.ok_or_err_msg("Missing data")?; + ctx.dispatch_ibc_action(&data) } diff --git a/wasm/wasm_source/src/tx_init_account.rs b/wasm/wasm_source/src/tx_init_account.rs index 9d187d3c48..05789751b2 100644 --- a/wasm/wasm_source/src/tx_init_account.rs +++ b/wasm/wasm_source/src/tx_init_account.rs @@ -5,10 +5,11 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); - let tx_data = - transaction::InitAccount::try_from_slice(&signed.data.unwrap()[..]) - .unwrap(); + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .err_msg("failed to decode SignedTxData")?; + let data = signed.data.ok_or_err_msg("Missing data")?; + let tx_data = transaction::InitAccount::try_from_slice(&data[..]) + .err_msg("failed to decode InitAccount")?; debug_log!("apply_tx called to init a new established account"); let address = ctx.init_account(&tx_data.vp_code)?; diff --git a/wasm/wasm_source/src/tx_init_nft.rs b/wasm/wasm_source/src/tx_init_nft.rs index ee0db9310d..ace54fc161 100644 --- a/wasm/wasm_source/src/tx_init_nft.rs +++ b/wasm/wasm_source/src/tx_init_nft.rs @@ -4,10 +4,11 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); - let tx_data = - transaction::nft::CreateNft::try_from_slice(&signed.data.unwrap()[..]) - .unwrap(); + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .err_msg("failed to decode SignedTxData")?; + let data = signed.data.ok_or_err_msg("Missing data")?; + let tx_data = transaction::nft::CreateNft::try_from_slice(&data[..]) + .err_msg("failed to decode CreateNft")?; log_string("apply_tx called to create a new NFT"); let _address = nft::init_nft(ctx, tx_data)?; diff --git a/wasm/wasm_source/src/tx_init_proposal.rs b/wasm/wasm_source/src/tx_init_proposal.rs index 48bd0fa0f5..728d7613ae 100644 --- a/wasm/wasm_source/src/tx_init_proposal.rs +++ b/wasm/wasm_source/src/tx_init_proposal.rs @@ -4,11 +4,12 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); - let tx_data = transaction::governance::InitProposalData::try_from_slice( - &signed.data.unwrap()[..], - ) - .unwrap(); + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .err_msg("failed to decode SignedTxData")?; + let data = signed.data.ok_or_err_msg("Missing data")?; + let tx_data = + transaction::governance::InitProposalData::try_from_slice(&data[..]) + .err_msg("failed to decode InitProposalData")?; log_string("apply_tx called to create a new governance proposal"); governance::init_proposal(ctx, tx_data) diff --git a/wasm/wasm_source/src/tx_init_validator.rs b/wasm/wasm_source/src/tx_init_validator.rs index bae45fc533..2bfed44fac 100644 --- a/wasm/wasm_source/src/tx_init_validator.rs +++ b/wasm/wasm_source/src/tx_init_validator.rs @@ -6,9 +6,11 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); - let init_validator = - InitValidator::try_from_slice(&signed.data.unwrap()[..]).unwrap(); + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .err_msg("failed to decode SignedTxData")?; + let data = signed.data.ok_or_err_msg("Missing data")?; + let init_validator = InitValidator::try_from_slice(&data[..]) + .err_msg("failed to decode InitValidator")?; debug_log!("apply_tx called to init a new validator account"); // Register the validator in PoS diff --git a/wasm/wasm_source/src/tx_mint_nft.rs b/wasm/wasm_source/src/tx_mint_nft.rs index 73c915d123..f132b74158 100644 --- a/wasm/wasm_source/src/tx_mint_nft.rs +++ b/wasm/wasm_source/src/tx_mint_nft.rs @@ -4,10 +4,11 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); - let tx_data = - transaction::nft::MintNft::try_from_slice(&signed.data.unwrap()[..]) - .unwrap(); + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .err_msg("failed to decode SignedTxData")?; + let data = signed.data.ok_or_err_msg("Missing data")?; + let tx_data = transaction::nft::MintNft::try_from_slice(&data[..]) + .err_msg("failed to decode MintNft")?; log_string("apply_tx called to mint a new NFT tokens"); nft::mint_tokens(ctx, tx_data) diff --git a/wasm/wasm_source/src/tx_transfer.rs b/wasm/wasm_source/src/tx_transfer.rs index e1dabcd851..5731612888 100644 --- a/wasm/wasm_source/src/tx_transfer.rs +++ b/wasm/wasm_source/src/tx_transfer.rs @@ -6,9 +6,11 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); - let transfer = - token::Transfer::try_from_slice(&signed.data.unwrap()[..]).unwrap(); + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .err_msg("failed to decode SignedTxData")?; + let data = signed.data.ok_or_err_msg("Missing data")?; + let transfer = token::Transfer::try_from_slice(&data[..]) + .err_msg("failed to decode token::Transfer")?; debug_log!("apply_tx called with transfer: {:#?}", transfer); let token::Transfer { source, diff --git a/wasm/wasm_source/src/tx_unbond.rs b/wasm/wasm_source/src/tx_unbond.rs index c3eec798a5..c5ffc1ab6e 100644 --- a/wasm/wasm_source/src/tx_unbond.rs +++ b/wasm/wasm_source/src/tx_unbond.rs @@ -5,18 +5,11 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); - let unbond = - transaction::pos::Unbond::try_from_slice(&signed.data.unwrap()[..]) - .unwrap(); + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .err_msg("failed to decode SignedTxData")?; + let data = signed.data.ok_or_err_msg("Missing data")?; + let unbond = transaction::pos::Unbond::try_from_slice(&data[..]) + .err_msg("failed to decode Unbond")?; - if let Err(err) = ctx.unbond_tokens( - unbond.source.as_ref(), - &unbond.validator, - unbond.amount, - ) { - debug_log!("Unbonding failed with: {}", err); - panic!() - } - Ok(()) + ctx.unbond_tokens(unbond.source.as_ref(), &unbond.validator, unbond.amount) } diff --git a/wasm/wasm_source/src/tx_update_vp.rs b/wasm/wasm_source/src/tx_update_vp.rs index d6fdb16a2e..d0c41d3bd9 100644 --- a/wasm/wasm_source/src/tx_update_vp.rs +++ b/wasm/wasm_source/src/tx_update_vp.rs @@ -6,10 +6,13 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); - let update_vp = - transaction::UpdateVp::try_from_slice(&signed.data.unwrap()[..]) - .unwrap(); + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .err_msg("failed to decode SignedTxData")?; + let data = signed.data.ok_or_err_msg("Missing data")?; + let update_vp = transaction::UpdateVp::try_from_slice(&data[..]) + .err_msg("failed to decode UpdateVp")?; + debug_log!("update VP for: {:#?}", update_vp.addr); + ctx.update_validity_predicate(&update_vp.addr, update_vp.vp_code) } diff --git a/wasm/wasm_source/src/tx_vote_proposal.rs b/wasm/wasm_source/src/tx_vote_proposal.rs index 30173d1b34..614e4a9fa1 100644 --- a/wasm/wasm_source/src/tx_vote_proposal.rs +++ b/wasm/wasm_source/src/tx_vote_proposal.rs @@ -4,12 +4,14 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); - let tx_data = transaction::governance::VoteProposalData::try_from_slice( - &signed.data.unwrap()[..], - ) - .unwrap(); - log_string("apply_tx called to vote a governance proposal"); + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .err_msg("failed to decode SignedTxData")?; + let data = signed.data.ok_or_err_msg("Missing data")?; + let tx_data = + transaction::governance::VoteProposalData::try_from_slice(&data[..]) + .err_msg("failed to decode VoteProposalData")?; + + debug_log!("apply_tx called to vote a governance proposal"); governance::vote_proposal(ctx, tx_data) } diff --git a/wasm/wasm_source/src/tx_withdraw.rs b/wasm/wasm_source/src/tx_withdraw.rs index adab55ca5a..bcb64b4af0 100644 --- a/wasm/wasm_source/src/tx_withdraw.rs +++ b/wasm/wasm_source/src/tx_withdraw.rs @@ -5,19 +5,16 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); - let withdraw = - transaction::pos::Withdraw::try_from_slice(&signed.data.unwrap()[..]) - .unwrap(); + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .err_msg("failed to decode SignedTxData")?; + let data = signed.data.ok_or_err_msg("Missing data")?; + let withdraw = transaction::pos::Withdraw::try_from_slice(&data[..]) + .err_msg("failed to decode Withdraw")?; - match ctx.withdraw_tokens(withdraw.source.as_ref(), &withdraw.validator) { - Ok(slashed) => { - debug_log!("Withdrawal slashed for {}", slashed); - } - Err(err) => { - debug_log!("Withdrawal failed with: {}", err); - panic!() - } + let slashed = + ctx.withdraw_tokens(withdraw.source.as_ref(), &withdraw.validator)?; + if slashed != token::Amount::default() { + debug_log!("Withdrawal slashed for {}", slashed); } Ok(()) } diff --git a/wasm_for_tests/wasm_source/src/lib.rs b/wasm_for_tests/wasm_source/src/lib.rs index 215a967846..0e47437704 100644 --- a/wasm_for_tests/wasm_source/src/lib.rs +++ b/wasm_for_tests/wasm_source/src/lib.rs @@ -87,13 +87,8 @@ pub mod main { #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = match SignedTxData::try_from_slice(&tx_data[..]) { - Ok(signed) => { - log("got signed data"); - signed - } - Err(error) => fatal("getting signed data", error), - }; + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .err_msg("failed to decode SignedTxData")?; let data = match signed.data { Some(data) => { log(&format!("got data ({} bytes)", data.len())); @@ -137,7 +132,8 @@ pub mod main { #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]).unwrap(); + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .err_msg("failed to decode SignedTxData")?; let transfer = token::Transfer::try_from_slice(&signed.data.unwrap()[..]).unwrap(); log_string(format!("apply_tx called to mint tokens: {:#?}", transfer)); From 221e9f2544cf2420b90bc78fd69328959103638d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 21 Jul 2022 15:55:09 +0200 Subject: [PATCH 10/81] changelog: add #1093 --- .../unreleased/improvements/1093-unify-native-and-wasm-vp.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/unreleased/improvements/1093-unify-native-and-wasm-vp.md diff --git a/.changelog/unreleased/improvements/1093-unify-native-and-wasm-vp.md b/.changelog/unreleased/improvements/1093-unify-native-and-wasm-vp.md new file mode 100644 index 0000000000..e39308413f --- /dev/null +++ b/.changelog/unreleased/improvements/1093-unify-native-and-wasm-vp.md @@ -0,0 +1,3 @@ +- Added WASM transaction and validity predicate `Ctx` with methods for host + environment functions to unify the interface of native VPs and WASM VPs under + `trait VpEnv` ([#1093](https://github.com/anoma/anoma/pull/1093)) \ No newline at end of file From 8bce9152fdb14d927b9756646d9e8633712a9bc3 Mon Sep 17 00:00:00 2001 From: Tomas Zemanovic Date: Mon, 25 Jul 2022 16:08:26 +0200 Subject: [PATCH 11/81] Update shared/src/ledger/vp_env.rs --- shared/src/ledger/vp_env.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/shared/src/ledger/vp_env.rs b/shared/src/ledger/vp_env.rs index 1a77c38312..e2ffd72e13 100644 --- a/shared/src/ledger/vp_env.rs +++ b/shared/src/ledger/vp_env.rs @@ -21,11 +21,7 @@ pub trait VpEnv { /// Storage read prefix iterator type PrefixIter; - /// Host functions possible error. - /// - /// In a native VP this may be out-of-gas error, however, because WASM VP is - /// sandboxed and gas accounting is injected by and handled in the host, - /// in WASM VP this error is [`std::convert::Infallible`]. + /// Host functions possible errors, extensible with custom user errors. type Error; /// Storage read prior state Borsh encoded value (before tx execution). It From affa0b550ac045a6f5d1f3cc1cec9f4f24f2884e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 11 Aug 2022 15:56:07 +0200 Subject: [PATCH 12/81] wasm_for_tests: make --- wasm_for_tests/tx_memory_limit.wasm | Bin 135502 -> 135517 bytes wasm_for_tests/tx_mint_tokens.wasm | Bin 226185 -> 226159 bytes wasm_for_tests/tx_no_op.wasm | Bin 25027 -> 25030 bytes wasm_for_tests/tx_proposal_code.wasm | Bin 214371 -> 213719 bytes wasm_for_tests/tx_read_storage_key.wasm | Bin 152558 -> 152558 bytes wasm_for_tests/tx_write_storage_key.wasm | Bin 149083 -> 218648 bytes wasm_for_tests/vp_always_false.wasm | Bin 160359 -> 160766 bytes wasm_for_tests/vp_always_true.wasm | Bin 161066 -> 160766 bytes wasm_for_tests/vp_eval.wasm | Bin 161675 -> 160961 bytes wasm_for_tests/vp_memory_limit.wasm | Bin 163364 -> 162919 bytes wasm_for_tests/vp_read_storage_key.wasm | Bin 170912 -> 170873 bytes 11 files changed, 0 insertions(+), 0 deletions(-) diff --git a/wasm_for_tests/tx_memory_limit.wasm b/wasm_for_tests/tx_memory_limit.wasm index 88c8ef0ada51f31c8e0f25c2fa633b4ce20ad65b..c31821bdf45cc9da95e876de939171236563b048 100755 GIT binary patch delta 778 zcmY*XO-NKx6u#%a-*IN_F%FZ?KwY0?lRv04IQ|XhRVQKed6w>t)I$UcxA&T+cI^fzd1CdaJs}*+spX!h*e6abH4csr)Bt=O}h) zQ4#TV2HXr7H4;HQhKi`kO$llx>99oZ8+s z+*M&yB-?)1Pz%dYnh7z1S5NWkX;bDT<0V0nj-=tKcoJ!9)Sb@|Q+14s%Y}s8$Iz+f zoAapFCp1hCampp8i@%Yxpo>KG7`Vi>vMogi^%!ks3aVvqMJFYvywQ&Dvd2UExBo0} zEpF~UPl~SiB+S__;&0_6eEk^?puQG`Vkm&YfUjYME2y}Ux9{;fwcO&+40x>+vT&wY z^b+D0RjHx9kvVh35ssy9AMkfl=V4pe!xw6)h3Y9`Dw`U=Q!v#8wu}*#si*~qLdb4P z?tm;42BK&9mKaQ5qL2~G>Gu>)i;2ud2R>vT)m9NgI`GYnk5635O^vbfu^SBiCSu@U dLWswnANdO4v9qI^Ax3fybEVu`DF?4#{Rdr?&5Qs5 delta 766 zcmY*XTS!zv82Z`o3g7u0QCGa`thTtW!3Y*#IUbkTYY zNrpm@a515$iaLZnY*23zR9HR)8By3n5J*qbOEhQo#bM6(@&Dhq9!yeBE8306|nhMWndHKm=zKR7|3v z^)OnkV z7&7AND3iBjfWZa=99%!`DeqkrrG+68D;&?yht{<}4^}hF8xN!Q$HK=bqf8c;Qd}2v zWz{YQ0phU~j-5%dQg)6&T-5o~lI-XHlqSyxtWhfIe#KQJ&}A0|mYj6nRZeR7LG;%C zE@5g7fQ8&Q%6(%NcKg&5UQ>+ou7cEIbd*_?4T`78a}>!OSb2e1ZMg< z;R-Xw-_Qv(MJ&7z4Ux*>Y`9&wj0O^9hP@UZQJmaPH-4k02~5^2ix%yX5uCDLL|#)S z6UolYepi$$vZYe>FoiEk4_6uTm3i^BWtaVkyEP|FwsRZ{pi2sPER}KtxInDNuHknv z5}W8clOx8q;K{9Fu;PgS{U8g`%swV{@p^q~(xjyRUDvcSlbt6qxAi zIZq%ep7p#X5EB#evktAs@0ITafJXTxhK7bOBt{3=(7QMei5Hh&|k= Vd&G3YE3PDbH#3Q)Oafm%`49Xo&n5r> diff --git a/wasm_for_tests/tx_mint_tokens.wasm b/wasm_for_tests/tx_mint_tokens.wasm index c9a102773e28ced047f374606582082da3d839d0..d9c858690158ed1142676c5d1e90e1e37cf85e63 100755 GIT binary patch delta 67440 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 delta 67188 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 diff --git a/wasm_for_tests/tx_proposal_code.wasm b/wasm_for_tests/tx_proposal_code.wasm index 3c753377ed91ac3086bd2c55079414182be5aa0b..4f19c38cdb3b54224d2cebd493660c22186e1c39 100755 GIT binary patch delta 65786 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 X-t1!eVYBo|i}-S&oF?QE^1S~C@TqkL delta 65827 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 z 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 AsQ>@~ delta 33651 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 KH{+#Kmj45i)%)lG diff --git a/wasm_for_tests/tx_write_storage_key.wasm b/wasm_for_tests/tx_write_storage_key.wasm index 89b669f6af9c1826ece9718b94c1767b2b95d1f3..a7db45733eeb9ef5731e1d1a1e640d1d9ffb4b07 100755 GIT binary patch literal 218648 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�?n*8Vpa_=>(yHeK|a);3}E> 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 delta 44569 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 OzWtw0eLH;V&;BpQ@Uwyd delta 16941 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 KZ-&qQ;r|CUUg!w` diff --git a/wasm_for_tests/vp_eval.wasm b/wasm_for_tests/vp_eval.wasm index 15b9ad7d670bc22bb78a7dd5d3417a429f53272c..58dff40f048c99f93b6798bf1de26963f302c4ba 100755 GIT binary patch delta 17271 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 E|Lk=?sQ>@~ delta 17810 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 M@2`A0eB$5!f2HuVasU7T diff --git a/wasm_for_tests/vp_memory_limit.wasm b/wasm_for_tests/vp_memory_limit.wasm index 9d445b88fde2864b07ad9d1c7916123ee0c96f67..3e476d5ec4aa9dcad450d17a744e092d10a0ad1b 100755 GIT binary patch delta 18096 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 delta 18652 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 D&)3*0 delta 38737 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 Date: Thu, 11 Aug 2022 16:46:24 +0200 Subject: [PATCH 13/81] update wasm checksums --- wasm/checksums.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index 5d670022b7..0e7c2970e3 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,19 +1,19 @@ { - "tx_bond.wasm": "tx_bond.71acaf1ff3a4ac1ce6550c4ce594cf70d684213769f45641be5cdb4b7321184e.wasm", - "tx_from_intent.wasm": "tx_from_intent.e18907b1fb0e1c4e4946228c0ec57838a783a5d88c6b3129d44935386693cee8.wasm", - "tx_ibc.wasm": "tx_ibc.38ee97db63b765f1639ee24dd3e7f264139f146a56f7c06edafde63eaecedade.wasm", - "tx_init_account.wasm": "tx_init_account.6be05f8ca240d9e978dfe6a1566b3860e07fa3aee334003fc5d85a5a1ae99cf7.wasm", - "tx_init_nft.wasm": "tx_init_nft.5f710b641c38cbe7822c2359bf7a764e864e952f4660ccff9b58b97fbaa4b3a2.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.25666fc8fbf251bd4ad478cb968f1d50a8100a0a7aae0e3ee5e50e28304e8e3e.wasm", - "tx_init_validator.wasm": "tx_init_validator.2b1a4c947242ddddf94a491b8c571185f649913606bfa7d37f668ddc86fe1049.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.9af8eea9219db09161b98405c18ea3c82db03b982f69a66ce1d117e64a715c3c.wasm", - "tx_transfer.wasm": "tx_transfer.87e4fba9d2476388e0f2b1d4bc00c396ac7b8b7b38d5079735fe53bc5984b583.wasm", - "tx_unbond.wasm": "tx_unbond.61f2ac46e7680ec35aba367ec908ac63aa43d6765cd07da932aa9ea45d77be46.wasm", - "tx_update_vp.wasm": "tx_update_vp.00d828bdf510d92d971ca59015945c8c00f285a802a655451cc5ee87c5ee99bc.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.92ed628aca7856e2b0fe461c0a0a65c6f7c1f585ac542c10feceef0e4b9ce611.wasm", - "tx_withdraw.wasm": "tx_withdraw.fb54d108a6fe7c6db16e573110c4a9f52158ec993e04e5a06e1425b8dd676c54.wasm", - "vp_nft.wasm": "vp_nft.3421840146f8d2e6c3ce3a0d94bbb0dad6ef296c50e9d00744179d290f32745b.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.1b37ab6939280fbb54512918053362fae34e3db74f029ef31c9f08e767539afb.wasm", - "vp_token.wasm": "vp_token.08dcec2d3ecb0942d0a244bbd176f9a42424ec09544120db1b30c0d97ed7dbb1.wasm", - "vp_user.wasm": "vp_user.404f089f0c73fcf906f7d6059d74c6bab3e2e3d3a108394503bbc22588ff95b9.wasm" + "tx_bond.wasm": "tx_bond.d1d6c4f26fce547f8513a541824a4935edcd33493eff8b1b7a90d2ec93ff2e3e.wasm", + "tx_from_intent.wasm": "tx_from_intent.8c191f8c888143f1b839a4db9b6f0befbb088394785434da146b633f5f30b1b8.wasm", + "tx_ibc.wasm": "tx_ibc.0137abdd701591e3f12bee5dbf24aac5b616febbfd93cfdf40f68715493e46a0.wasm", + "tx_init_account.wasm": "tx_init_account.f9765f3fc3e93ddab2981d69196e896db20cd47bfd378df73a7d1f28bd18b917.wasm", + "tx_init_nft.wasm": "tx_init_nft.b268527f6b8d3aa99c07d50cb003a3be4de38dbbaa7ae5ee846b4e4bc43d12e9.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.8588577db37a9b55f6700b761a0dd39ffe16a69a818da875397e48074775eea9.wasm", + "tx_init_validator.wasm": "tx_init_validator.74f67dbceb2d3d07d4e722487887d151c88caa44d600ad9e2d9b316568430a0d.wasm", + "tx_mint_nft.wasm": "tx_mint_nft.b0cd25ce674765a2fc79a69b3863da66971317b7e0168a21a4bed41a3211a7a3.wasm", + "tx_transfer.wasm": "tx_transfer.71adf67c4b597f79ee85284eb144513ece312929c0c7629d2d3cffce1446b871.wasm", + "tx_unbond.wasm": "tx_unbond.549c267003e98a3807a67c95e790bfa8cd35ae5da1344cb776a3c67ed3e7df7a.wasm", + "tx_update_vp.wasm": "tx_update_vp.a304b3c70361cde5fda458ba5637d6825eb002198e73990a1c74351113b83d43.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.7c0b48866b9b31e913f338d0afaf41605f51c0cddf2e3a3f0d231e57e68c72f2.wasm", + "tx_withdraw.wasm": "tx_withdraw.34f64072aacd5b4fc3815a12999fbaf0dd64cd2c0772e302a634c3fbe7cfe7ac.wasm", + "vp_nft.wasm": "vp_nft.ea4754591552e715ffdb125b649b3d1bb4b45ca93c3c808a51fa5ccaa9787d5c.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.8a2c7be6d3e5afc4fec1e001c95f7134a37e64df661bc54e9fe04c6a38e166ce.wasm", + "vp_token.wasm": "vp_token.5e619a0471b160712711bf44de9db1950f12900d1405dbcca0a2cea41d16a8a1.wasm", + "vp_user.wasm": "vp_user.7667570c56cdadea9fa8eb303e31cbb4b6661406cb277dcb59a9293702f6dd91.wasm" } \ No newline at end of file From 18526da1a51ba4851d89862d9c9b5afb02b73ba2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 Aug 2022 17:20:07 +0000 Subject: [PATCH 14/81] [ci skip] wasm checksums update --- wasm/checksums.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index 0e7c2970e3..b8e98c6cf2 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,19 +1,19 @@ { - "tx_bond.wasm": "tx_bond.d1d6c4f26fce547f8513a541824a4935edcd33493eff8b1b7a90d2ec93ff2e3e.wasm", - "tx_from_intent.wasm": "tx_from_intent.8c191f8c888143f1b839a4db9b6f0befbb088394785434da146b633f5f30b1b8.wasm", - "tx_ibc.wasm": "tx_ibc.0137abdd701591e3f12bee5dbf24aac5b616febbfd93cfdf40f68715493e46a0.wasm", - "tx_init_account.wasm": "tx_init_account.f9765f3fc3e93ddab2981d69196e896db20cd47bfd378df73a7d1f28bd18b917.wasm", - "tx_init_nft.wasm": "tx_init_nft.b268527f6b8d3aa99c07d50cb003a3be4de38dbbaa7ae5ee846b4e4bc43d12e9.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.8588577db37a9b55f6700b761a0dd39ffe16a69a818da875397e48074775eea9.wasm", - "tx_init_validator.wasm": "tx_init_validator.74f67dbceb2d3d07d4e722487887d151c88caa44d600ad9e2d9b316568430a0d.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.b0cd25ce674765a2fc79a69b3863da66971317b7e0168a21a4bed41a3211a7a3.wasm", - "tx_transfer.wasm": "tx_transfer.71adf67c4b597f79ee85284eb144513ece312929c0c7629d2d3cffce1446b871.wasm", - "tx_unbond.wasm": "tx_unbond.549c267003e98a3807a67c95e790bfa8cd35ae5da1344cb776a3c67ed3e7df7a.wasm", + "tx_bond.wasm": "tx_bond.091bf7885488fe9d01643d448236a3c5a8496fc72b1992d98633fa9954a79505.wasm", + "tx_from_intent.wasm": "tx_from_intent.65511fd08da70dc102f8c9f2c4c96fa611d7d2c9b680bcd323da242b1ce6d563.wasm", + "tx_ibc.wasm": "tx_ibc.bfc87f17efbd799cfa73487b7e080f845a6004cdc5f9b26c74cee93587f9d3c4.wasm", + "tx_init_account.wasm": "tx_init_account.ca95fc31bafe84f9bccc4e01336d2d993f4652fef9ea3d0bcfc10edbd9ef30d8.wasm", + "tx_init_nft.wasm": "tx_init_nft.683188dc9eceaf55263d4021bb0fe6c733a74e79998370fc54994eff0d4fd061.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.c4612a75320aa5ceb23cd61da9b43cf89b15feadad9fc28886eadfdeac38bbca.wasm", + "tx_init_validator.wasm": "tx_init_validator.05b94e3de1ff997c830ec0393df6d51fdcbe0410b28e3b9b4cc585c7132141b7.wasm", + "tx_mint_nft.wasm": "tx_mint_nft.e29e4510c1bb9df5b98671ab2b5b5c728d21812924398e810f8d4a00885d9b98.wasm", + "tx_transfer.wasm": "tx_transfer.38739d844a43275e1e4bc18f79a225d1abece7bf302a49763073c1a5188f54d9.wasm", + "tx_unbond.wasm": "tx_unbond.c943d4a1759b2912dc1762e9bce55f16226db12b081afce912e3fa1a28459ba2.wasm", "tx_update_vp.wasm": "tx_update_vp.a304b3c70361cde5fda458ba5637d6825eb002198e73990a1c74351113b83d43.wasm", "tx_vote_proposal.wasm": "tx_vote_proposal.7c0b48866b9b31e913f338d0afaf41605f51c0cddf2e3a3f0d231e57e68c72f2.wasm", - "tx_withdraw.wasm": "tx_withdraw.34f64072aacd5b4fc3815a12999fbaf0dd64cd2c0772e302a634c3fbe7cfe7ac.wasm", + "tx_withdraw.wasm": "tx_withdraw.1ec566946f3178c7a7e639e56eb4d1e26ac24e4d5cb856efd1a81bbe59390a3e.wasm", "vp_nft.wasm": "vp_nft.ea4754591552e715ffdb125b649b3d1bb4b45ca93c3c808a51fa5ccaa9787d5c.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.8a2c7be6d3e5afc4fec1e001c95f7134a37e64df661bc54e9fe04c6a38e166ce.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.70588d919e1c3d7a9c2ec37c67c5587c835ab0b9b485b73cec25b9413c9ccfd8.wasm", "vp_token.wasm": "vp_token.5e619a0471b160712711bf44de9db1950f12900d1405dbcca0a2cea41d16a8a1.wasm", - "vp_user.wasm": "vp_user.7667570c56cdadea9fa8eb303e31cbb4b6661406cb277dcb59a9293702f6dd91.wasm" + "vp_user.wasm": "vp_user.3712b862e606effd77fc8c82058d960012ea5aef09580b8521de827484af27d1.wasm" } \ No newline at end of file From 9a135eb9396889eb33fd2239c7bcc60ff045017f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 12 Aug 2022 09:45:22 +0200 Subject: [PATCH 15/81] ledger: add StorageRead trait with extensible error type --- shared/src/ledger/mod.rs | 1 + shared/src/ledger/storage_api/error.rs | 68 ++++++++++++++++++++++++++ shared/src/ledger/storage_api/mod.rs | 53 ++++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 shared/src/ledger/storage_api/error.rs create mode 100644 shared/src/ledger/storage_api/mod.rs diff --git a/shared/src/ledger/mod.rs b/shared/src/ledger/mod.rs index 7dd7bc2d46..fefb32ac64 100644 --- a/shared/src/ledger/mod.rs +++ b/shared/src/ledger/mod.rs @@ -8,6 +8,7 @@ pub mod native_vp; pub mod parameters; pub mod pos; pub mod storage; +pub mod storage_api; pub mod treasury; pub mod tx_env; pub mod vp_env; diff --git a/shared/src/ledger/storage_api/error.rs b/shared/src/ledger/storage_api/error.rs new file mode 100644 index 0000000000..d01fbfd287 --- /dev/null +++ b/shared/src/ledger/storage_api/error.rs @@ -0,0 +1,68 @@ +//! Storage API error type, extensible with custom user errors and static string +//! messages. + +use thiserror::Error; + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Custom(CustomError), + #[error("{0}: {1}")] + CustomWithMessage(&'static str, CustomError), +} + +/// Result of a storage API call. +pub type Result = std::result::Result; + +/// Result extension to easily wrap custom errors into [`Error`]. +// This is separate from `ResultExt`, because the implementation requires +// different bounds for `T`. +pub trait ResultExt { + /// Convert a [`std::result::Result`] into storage_api [`Result`]. + fn into_storage_result(self) -> Result; + + /// Add a static message to a possible error in [`Result`]. + fn wrap_err(self, msg: &'static str) -> Result; +} + +impl ResultExt for std::result::Result +where + E: std::error::Error + Send + Sync + 'static, +{ + fn into_storage_result(self) -> Result { + self.map_err(Error::new) + } + + fn wrap_err(self, msg: &'static str) -> Result { + self.map_err(|err| Error::wrap(msg, err)) + } +} + +impl Error { + /// Create an [`enum@Error`] from another [`std::error::Error`]. + pub fn new(error: E) -> Self + where + E: Into>, + { + Self::Custom(CustomError(error.into())) + } + + /// Wrap another [`std::error::Error`] with a static message. + pub fn wrap(msg: &'static str, error: E) -> Self + where + E: Into>, + { + Self::CustomWithMessage(msg, CustomError(error.into())) + } +} + +/// A custom error +#[derive(Debug)] +pub struct CustomError(pub Box); + +impl std::fmt::Display for CustomError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} diff --git a/shared/src/ledger/storage_api/mod.rs b/shared/src/ledger/storage_api/mod.rs new file mode 100644 index 0000000000..0c0e4f4083 --- /dev/null +++ b/shared/src/ledger/storage_api/mod.rs @@ -0,0 +1,53 @@ +//! The common storage read trait is implemented in the storage, client RPC, tx +//! and VPs (both native and WASM). + +mod error; + +use borsh::BorshDeserialize; +pub use error::{CustomError, Error, Result, ResultExt}; + +use crate::types::storage::{self, BlockHash, BlockHeight, Epoch}; + +/// Common storage read interface +pub trait StorageRead { + /// Storage read prefix iterator + type PrefixIter; + + /// Storage read Borsh encoded value. It will try to read from the storage + /// and decode it if found. + fn read( + &self, + key: &storage::Key, + ) -> Result>; + + /// Storage read raw bytes. It will try to read from the storage. + fn read_bytes(&self, key: &storage::Key) -> Result>>; + + /// Storage `has_key` in. It will try to read from the storage. + fn has_key(&self, key: &storage::Key) -> Result; + + /// Storage prefix iterator. It will try to get an iterator from the + /// storage. + fn iter_prefix(&self, prefix: &storage::Key) -> Result; + + /// Storage prefix iterator for. It will try to read from the storage. + fn iter_next( + &self, + iter: &mut Self::PrefixIter, + ) -> Result)>>; + + /// Getting the chain ID. + fn get_chain_id(&self) -> Result; + + /// Getting the block height. The height is that of the block to which the + /// current transaction is being applied. + fn get_block_height(&self) -> Result; + + /// Getting the block hash. The height is that of the block to which the + /// current transaction is being applied. + fn get_block_hash(&self) -> Result; + + /// Getting the block epoch. The epoch is that of the block to which the + /// current transaction is being applied. + fn get_block_epoch(&self) -> Result; +} From 97c69475935bbab22cfefa5a90ce77b8f5241107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 12 Aug 2022 09:46:53 +0200 Subject: [PATCH 16/81] ledger/native_vp: implement StorageRead for pre and post state --- shared/src/ledger/native_vp.rs | 157 +++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/shared/src/ledger/native_vp.rs b/shared/src/ledger/native_vp.rs index 3893e847ed..fa30efb7ea 100644 --- a/shared/src/ledger/native_vp.rs +++ b/shared/src/ledger/native_vp.rs @@ -3,6 +3,7 @@ use std::cell::RefCell; use std::collections::BTreeSet; +use super::storage_api::{self, ResultExt, StorageRead}; pub use super::vp_env::VpEnv; use crate::ledger::gas::VpGasMeter; use crate::ledger::storage::write_log::WriteLog; @@ -72,6 +73,30 @@ where pub cache_access: std::marker::PhantomData, } +/// Read access to the prior storage (state before tx execution) via +/// [`trait@StorageRead`]. +#[derive(Debug)] +pub struct CtxPreStorageRead<'b, 'a: 'b, DB, H, CA> +where + DB: storage::DB + for<'iter> storage::DBIter<'iter>, + H: StorageHasher, + CA: WasmCacheAccess, +{ + ctx: &'b Ctx<'a, DB, H, CA>, +} + +/// Read access to the posterior storage (state after tx execution) via +/// [`trait@StorageRead`]. +#[derive(Debug)] +pub struct CtxPostStorageRead<'f, 'a: 'f, DB, H, CA> +where + DB: storage::DB + for<'iter> storage::DBIter<'iter>, + H: StorageHasher, + CA: WasmCacheAccess, +{ + ctx: &'f Ctx<'a, DB, H, CA>, +} + impl<'a, DB, H, CA> Ctx<'a, DB, H, CA> where DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, @@ -111,6 +136,138 @@ where pub fn add_gas(&self, used_gas: u64) -> Result<(), vp_env::RuntimeError> { vp_env::add_gas(&mut *self.gas_meter.borrow_mut(), used_gas) } + + /// Read access to the prior storage (state before tx execution) + /// via [`trait@StorageRead`]. + pub fn pre<'b>(&'b self) -> CtxPreStorageRead<'b, 'a, DB, H, CA> { + CtxPreStorageRead { ctx: self } + } + + /// Read access to the posterior storage (state after tx execution) + /// via [`trait@StorageRead`]. + pub fn post<'b>(&'b self) -> CtxPostStorageRead<'b, 'a, DB, H, CA> { + CtxPostStorageRead { ctx: self } + } +} + +impl<'f, 'a, DB, H, CA> StorageRead for CtxPreStorageRead<'f, 'a, DB, H, CA> +where + DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, + H: 'static + StorageHasher, + CA: 'static + WasmCacheAccess, +{ + type PrefixIter = >::PrefixIter; + + fn read( + &self, + key: &crate::types::storage::Key, + ) -> Result, storage_api::Error> { + self.ctx.read_pre(key).into_storage_result() + } + + fn read_bytes( + &self, + key: &crate::types::storage::Key, + ) -> Result>, storage_api::Error> { + self.ctx.read_bytes_pre(key).into_storage_result() + } + + fn has_key( + &self, + key: &crate::types::storage::Key, + ) -> Result { + self.ctx.has_key_pre(key).into_storage_result() + } + + fn iter_prefix( + &self, + prefix: &crate::types::storage::Key, + ) -> Result { + self.ctx.iter_prefix(prefix).into_storage_result() + } + + fn iter_next( + &self, + iter: &mut Self::PrefixIter, + ) -> Result)>, storage_api::Error> { + self.ctx.iter_pre_next(iter).into_storage_result() + } + + fn get_chain_id(&self) -> Result { + self.ctx.get_chain_id().into_storage_result() + } + + fn get_block_height(&self) -> Result { + self.ctx.get_block_height().into_storage_result() + } + + fn get_block_hash(&self) -> Result { + self.ctx.get_block_hash().into_storage_result() + } + + fn get_block_epoch(&self) -> Result { + self.ctx.get_block_epoch().into_storage_result() + } +} + +impl<'f, 'a, DB, H, CA> StorageRead for CtxPostStorageRead<'f, 'a, DB, H, CA> +where + DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, + H: 'static + StorageHasher, + CA: 'static + WasmCacheAccess, +{ + type PrefixIter = >::PrefixIter; + + fn read( + &self, + key: &crate::types::storage::Key, + ) -> Result, storage_api::Error> { + self.ctx.read_post(key).into_storage_result() + } + + fn read_bytes( + &self, + key: &crate::types::storage::Key, + ) -> Result>, storage_api::Error> { + self.ctx.read_bytes_post(key).into_storage_result() + } + + fn has_key( + &self, + key: &crate::types::storage::Key, + ) -> Result { + self.ctx.has_key_post(key).into_storage_result() + } + + fn iter_prefix( + &self, + prefix: &crate::types::storage::Key, + ) -> Result { + self.ctx.iter_prefix(prefix).into_storage_result() + } + + fn iter_next( + &self, + iter: &mut Self::PrefixIter, + ) -> Result)>, storage_api::Error> { + self.ctx.iter_post_next(iter).into_storage_result() + } + + fn get_chain_id(&self) -> Result { + self.ctx.get_chain_id().into_storage_result() + } + + fn get_block_height(&self) -> Result { + self.ctx.get_block_height().into_storage_result() + } + + fn get_block_hash(&self) -> Result { + self.ctx.get_block_hash().into_storage_result() + } + + fn get_block_epoch(&self) -> Result { + self.ctx.get_block_epoch().into_storage_result() + } } impl<'a, DB, H, CA> VpEnv for Ctx<'a, DB, H, CA> From 7552c1ee7d39b75c4e08d8b3aac1022663827c53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 12 Aug 2022 09:47:42 +0200 Subject: [PATCH 17/81] ledger/vp: impl StorageRead for WASM VPs pre and post state --- shared/src/ledger/vp_env.rs | 9 ++ vp_prelude/src/error.rs | 7 + vp_prelude/src/lib.rs | 290 ++++++++++++++++++++++++++++-------- 3 files changed, 247 insertions(+), 59 deletions(-) diff --git a/shared/src/ledger/vp_env.rs b/shared/src/ledger/vp_env.rs index e2ffd72e13..95e7c48782 100644 --- a/shared/src/ledger/vp_env.rs +++ b/shared/src/ledger/vp_env.rs @@ -7,6 +7,7 @@ use borsh::BorshDeserialize; use thiserror::Error; use super::gas::MIN_STORAGE_GAS; +use super::storage_api; use crate::ledger::gas; use crate::ledger::gas::VpGasMeter; use crate::ledger::storage::write_log::WriteLog; @@ -157,6 +158,8 @@ pub enum RuntimeError { ReadTemporaryValueError, #[error("Trying to read a permament value with read_temp")] ReadPermanentValueError, + #[error("Storage error: {0}")] + StorageApi(storage_api::Error), } /// VP environment function result @@ -460,3 +463,9 @@ where } Ok(None) } + +impl From for RuntimeError { + fn from(err: storage_api::Error) -> Self { + Self::StorageApi(err) + } +} diff --git a/vp_prelude/src/error.rs b/vp_prelude/src/error.rs index b34d954166..099ae2540a 100644 --- a/vp_prelude/src/error.rs +++ b/vp_prelude/src/error.rs @@ -5,6 +5,7 @@ //! avoiding `error[E0117]: only traits defined in the current crate can be //! implemented for arbitrary types` +use namada::ledger::storage_api; use thiserror::Error; #[allow(missing_docs)] @@ -101,3 +102,9 @@ impl std::fmt::Display for CustomError { self.0.fmt(f) } } + +impl From for Error { + fn from(err: storage_api::Error) -> Self { + Self::new(err) + } +} diff --git a/vp_prelude/src/lib.rs b/vp_prelude/src/lib.rs index c1be4465f1..37d1958aa9 100644 --- a/vp_prelude/src/lib.rs +++ b/vp_prelude/src/lib.rs @@ -22,6 +22,7 @@ use std::marker::PhantomData; pub use borsh::{BorshDeserialize, BorshSerialize}; pub use error::*; pub use namada::ledger::governance::storage as gov_storage; +pub use namada::ledger::storage_api::{self, StorageRead}; pub use namada::ledger::vp_env::VpEnv; pub use namada::ledger::{parameters, pos as proof_of_stake}; pub use namada::proto::{Signed, SignedTxData}; @@ -80,6 +81,7 @@ macro_rules! debug_log { }} } +#[derive(Debug)] pub struct Ctx(()); impl Ctx { @@ -104,6 +106,32 @@ impl Ctx { pub const unsafe fn new() -> Self { Self(()) } + + /// Read access to the prior storage (state before tx execution) + /// via [`trait@StorageRead`]. + pub fn pre(&self) -> CtxPreStorageRead<'_> { + CtxPreStorageRead { _ctx: self } + } + + /// Read access to the posterior storage (state after tx execution) + /// via [`trait@StorageRead`]. + pub fn post(&self) -> CtxPostStorageRead<'_> { + CtxPostStorageRead { _ctx: self } + } +} + +/// Read access to the prior storage (state before tx execution) via +/// [`trait@StorageRead`]. +#[derive(Debug)] +pub struct CtxPreStorageRead<'a> { + _ctx: &'a Ctx, +} + +/// Read access to the posterior storage (state after tx execution) via +/// [`trait@StorageRead`]. +#[derive(Debug)] +pub struct CtxPostStorageRead<'a> { + _ctx: &'a Ctx, } /// Validity predicate result @@ -130,42 +158,28 @@ impl VpEnv for Ctx { &self, key: &storage::Key, ) -> Result, Self::Error> { - let key = key.to_string(); - let read_result = - unsafe { anoma_vp_read_pre(key.as_ptr() as _, key.len() as _) }; - Ok(read_from_buffer(read_result, anoma_vp_result_buffer) - .and_then(|bytes| T::try_from_slice(&bytes[..]).ok())) + self.pre().read(key).into_env_result() } fn read_bytes_pre( &self, key: &storage::Key, ) -> Result>, Self::Error> { - let key = key.to_string(); - let read_result = - unsafe { anoma_vp_read_pre(key.as_ptr() as _, key.len() as _) }; - Ok(read_from_buffer(read_result, anoma_vp_result_buffer)) + self.pre().read_bytes(key).into_env_result() } fn read_post( &self, key: &storage::Key, ) -> Result, Self::Error> { - let key = key.to_string(); - let read_result = - unsafe { anoma_vp_read_post(key.as_ptr() as _, key.len() as _) }; - Ok(read_from_buffer(read_result, anoma_vp_result_buffer) - .and_then(|bytes| T::try_from_slice(&bytes[..]).ok())) + self.post().read(key).into_env_result() } fn read_bytes_post( &self, key: &storage::Key, ) -> Result>, Self::Error> { - let key = key.to_string(); - let read_result = - unsafe { anoma_vp_read_post(key.as_ptr() as _, key.len() as _) }; - Ok(read_from_buffer(read_result, anoma_vp_result_buffer)) + self.post().read_bytes(key).into_env_result() } fn read_temp( @@ -190,80 +204,50 @@ impl VpEnv for Ctx { } fn has_key_pre(&self, key: &storage::Key) -> Result { - let key = key.to_string(); - let found = - unsafe { anoma_vp_has_key_pre(key.as_ptr() as _, key.len() as _) }; - Ok(HostEnvResult::is_success(found)) + self.pre().has_key(key).into_env_result() } fn has_key_post(&self, key: &storage::Key) -> Result { - let key = key.to_string(); - let found = - unsafe { anoma_vp_has_key_post(key.as_ptr() as _, key.len() as _) }; - Ok(HostEnvResult::is_success(found)) + self.post().has_key(key).into_env_result() } fn get_chain_id(&self) -> Result { - let result = Vec::with_capacity(CHAIN_ID_LENGTH); - unsafe { - anoma_vp_get_chain_id(result.as_ptr() as _); - } - let slice = - unsafe { slice::from_raw_parts(result.as_ptr(), CHAIN_ID_LENGTH) }; - Ok(String::from_utf8(slice.to_vec()) - .expect("Cannot convert the ID string")) + // Both `CtxPreStorageRead` and `CtxPostStorageRead` have the same impl + self.pre().get_chain_id().into_env_result() } fn get_block_height(&self) -> Result { - Ok(BlockHeight(unsafe { anoma_vp_get_block_height() })) + self.pre().get_block_height().into_env_result() } fn get_block_hash(&self) -> Result { - let result = Vec::with_capacity(BLOCK_HASH_LENGTH); - unsafe { - anoma_vp_get_block_hash(result.as_ptr() as _); - } - let slice = unsafe { - slice::from_raw_parts(result.as_ptr(), BLOCK_HASH_LENGTH) - }; - Ok(BlockHash::try_from(slice).expect("Cannot convert the hash")) + self.pre().get_block_hash().into_env_result() } fn get_block_epoch(&self) -> Result { - Ok(Epoch(unsafe { anoma_vp_get_block_epoch() })) + self.pre().get_block_epoch().into_env_result() } fn iter_prefix( &self, prefix: &storage::Key, ) -> Result { - let prefix = prefix.to_string(); - let iter_id = unsafe { - anoma_vp_iter_prefix(prefix.as_ptr() as _, prefix.len() as _) - }; - Ok(KeyValIterator(iter_id, PhantomData)) + // Both `CtxPreStorageRead` and `CtxPostStorageRead` have the same impl + self.pre().iter_prefix(prefix).into_env_result() } fn iter_pre_next( &self, iter: &mut Self::PrefixIter, ) -> Result)>, Self::Error> { - let read_result = unsafe { anoma_vp_iter_pre_next(iter.0) }; - Ok(read_key_val_bytes_from_buffer( - read_result, - anoma_vp_result_buffer, - )) + self.pre().iter_next(iter).into_env_result() } fn iter_post_next( &self, iter: &mut Self::PrefixIter, ) -> Result)>, Self::Error> { - let read_result = unsafe { anoma_vp_iter_post_next(iter.0) }; - Ok(read_key_val_bytes_from_buffer( - read_result, - anoma_vp_result_buffer, - )) + self.post().iter_next(iter).into_env_result() } fn eval( @@ -310,3 +294,191 @@ impl VpEnv for Ctx { Ok(Hash::try_from(slice).expect("Cannot convert the hash")) } } + +impl StorageRead for CtxPreStorageRead<'_> { + type PrefixIter = KeyValIterator<(String, Vec)>; + + fn read( + &self, + key: &storage::Key, + ) -> Result, storage_api::Error> { + let bytes = self.read_bytes(key)?; + match bytes { + Some(bytes) => match T::try_from_slice(&bytes[..]) { + Ok(val) => Ok(Some(val)), + Err(err) => Err(storage_api::Error::new(err)), + }, + None => Ok(None), + } + } + + fn read_bytes( + &self, + key: &storage::Key, + ) -> Result>, storage_api::Error> { + let key = key.to_string(); + let read_result = + unsafe { anoma_vp_read_pre(key.as_ptr() as _, key.len() as _) }; + Ok(read_from_buffer(read_result, anoma_vp_result_buffer)) + } + + fn has_key(&self, key: &storage::Key) -> Result { + let key = key.to_string(); + let found = + unsafe { anoma_vp_has_key_pre(key.as_ptr() as _, key.len() as _) }; + Ok(HostEnvResult::is_success(found)) + } + + fn iter_prefix( + &self, + prefix: &storage::Key, + ) -> Result { + // Note that this is the same as `CtxPostStorageRead` + iter_prefix(prefix) + } + + fn iter_next( + &self, + iter: &mut Self::PrefixIter, + ) -> Result)>, storage_api::Error> { + let read_result = unsafe { anoma_vp_iter_pre_next(iter.0) }; + Ok(read_key_val_bytes_from_buffer( + read_result, + anoma_vp_result_buffer, + )) + } + + fn get_chain_id(&self) -> Result { + get_chain_id() + } + + fn get_block_height(&self) -> Result { + Ok(BlockHeight(unsafe { anoma_vp_get_block_height() })) + } + + fn get_block_hash(&self) -> Result { + let result = Vec::with_capacity(BLOCK_HASH_LENGTH); + unsafe { + anoma_vp_get_block_hash(result.as_ptr() as _); + } + let slice = unsafe { + slice::from_raw_parts(result.as_ptr(), BLOCK_HASH_LENGTH) + }; + Ok(BlockHash::try_from(slice).expect("Cannot convert the hash")) + } + + fn get_block_epoch(&self) -> Result { + Ok(Epoch(unsafe { anoma_vp_get_block_epoch() })) + } +} + +impl StorageRead for CtxPostStorageRead<'_> { + type PrefixIter = KeyValIterator<(String, Vec)>; + + fn read( + &self, + key: &storage::Key, + ) -> Result, storage_api::Error> { + let bytes = self.read_bytes(key)?; + match bytes { + Some(bytes) => match T::try_from_slice(&bytes[..]) { + Ok(val) => Ok(Some(val)), + Err(err) => Err(storage_api::Error::new(err)), + }, + None => Ok(None), + } + } + + fn read_bytes( + &self, + key: &storage::Key, + ) -> Result>, storage_api::Error> { + let key = key.to_string(); + let read_result = + unsafe { anoma_vp_read_post(key.as_ptr() as _, key.len() as _) }; + Ok(read_from_buffer(read_result, anoma_vp_result_buffer)) + } + + fn has_key(&self, key: &storage::Key) -> Result { + let key = key.to_string(); + let found = + unsafe { anoma_vp_has_key_post(key.as_ptr() as _, key.len() as _) }; + Ok(HostEnvResult::is_success(found)) + } + + fn iter_prefix( + &self, + prefix: &storage::Key, + ) -> Result { + // Note that this is the same as `CtxPreStorageRead` + iter_prefix(prefix) + } + + fn iter_next( + &self, + iter: &mut Self::PrefixIter, + ) -> Result)>, storage_api::Error> { + let read_result = unsafe { anoma_vp_iter_post_next(iter.0) }; + Ok(read_key_val_bytes_from_buffer( + read_result, + anoma_vp_result_buffer, + )) + } + + fn get_chain_id(&self) -> Result { + get_chain_id() + } + + fn get_block_height(&self) -> Result { + get_block_height() + } + + fn get_block_hash(&self) -> Result { + get_block_hash() + } + + fn get_block_epoch(&self) -> Result { + get_block_epoch() + } +} + +fn iter_prefix( + prefix: &storage::Key, +) -> Result)>, storage_api::Error> { + let prefix = prefix.to_string(); + let iter_id = unsafe { + anoma_vp_iter_prefix(prefix.as_ptr() as _, prefix.len() as _) + }; + Ok(KeyValIterator(iter_id, PhantomData)) +} + +fn get_chain_id() -> Result { + let result = Vec::with_capacity(CHAIN_ID_LENGTH); + unsafe { + anoma_vp_get_chain_id(result.as_ptr() as _); + } + let slice = + unsafe { slice::from_raw_parts(result.as_ptr(), CHAIN_ID_LENGTH) }; + Ok( + String::from_utf8(slice.to_vec()) + .expect("Cannot convert the ID string"), + ) +} + +fn get_block_height() -> Result { + Ok(BlockHeight(unsafe { anoma_vp_get_block_height() })) +} + +fn get_block_hash() -> Result { + let result = Vec::with_capacity(BLOCK_HASH_LENGTH); + unsafe { + anoma_vp_get_block_hash(result.as_ptr() as _); + } + let slice = + unsafe { slice::from_raw_parts(result.as_ptr(), BLOCK_HASH_LENGTH) }; + Ok(BlockHash::try_from(slice).expect("Cannot convert the hash")) +} + +fn get_block_epoch() -> Result { + Ok(Epoch(unsafe { anoma_vp_get_block_epoch() })) +} From 45f3f947968070d0826b46f12d795aedeb1d0981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 12 Aug 2022 09:49:20 +0200 Subject: [PATCH 18/81] ledger/storage: impl StorageRead for Storage --- shared/src/ledger/storage/mod.rs | 76 +++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/shared/src/ledger/storage/mod.rs b/shared/src/ledger/storage/mod.rs index 0b3d19a742..1f106bcf69 100644 --- a/shared/src/ledger/storage/mod.rs +++ b/shared/src/ledger/storage/mod.rs @@ -11,8 +11,9 @@ use core::fmt::Debug; use tendermint::merkle::proof::Proof; use thiserror::Error; -use super::parameters; use super::parameters::Parameters; +use super::storage_api::{ResultExt, StorageRead}; +use super::{parameters, storage_api}; use crate::ledger::gas::MIN_STORAGE_GAS; use crate::ledger::parameters::EpochDuration; use crate::ledger::storage::merkle_tree::{ @@ -684,6 +685,79 @@ where } } +// The `'iter` lifetime is needed for the associated type `PrefixIter`. +// Note that the `D: DBIter<'iter>` bound uses another higher-rank lifetime +// (see https://doc.rust-lang.org/nomicon/hrtb.html). +impl<'iter, D, H> StorageRead for &'iter Storage +where + D: DB + for<'iter_> DBIter<'iter_>, + H: StorageHasher, +{ + type PrefixIter = >::PrefixIter; + + fn read( + &self, + key: &crate::types::storage::Key, + ) -> std::result::Result, storage_api::Error> { + self.read_bytes(key) + .map(|maybe_value| { + maybe_value.and_then(|t| T::try_from_slice(&t[..]).ok()) + }) + .into_storage_result() + } + + fn read_bytes( + &self, + key: &crate::types::storage::Key, + ) -> std::result::Result>, storage_api::Error> { + self.db.read_subspace_val(key).into_storage_result() + } + + fn has_key( + &self, + key: &crate::types::storage::Key, + ) -> std::result::Result { + self.block.tree.has_key(key).into_storage_result() + } + + fn iter_prefix( + &self, + prefix: &crate::types::storage::Key, + ) -> std::result::Result { + Ok(self.db.iter_prefix(prefix)) + } + + fn iter_next( + &self, + iter: &mut Self::PrefixIter, + ) -> std::result::Result)>, storage_api::Error> + { + Ok(iter.next().map(|(key, val, _gas)| (key, val))) + } + + fn get_chain_id(&self) -> std::result::Result { + Ok(self.chain_id.to_string()) + } + + fn get_block_height( + &self, + ) -> std::result::Result { + Ok(self.block.height) + } + + fn get_block_hash( + &self, + ) -> std::result::Result { + Ok(self.block.hash.clone()) + } + + fn get_block_epoch( + &self, + ) -> std::result::Result { + Ok(self.block.epoch) + } +} + impl From for Error { fn from(error: MerkleTreeError) -> Self { Self::MerkleTreeError(error) From 5989412b90f9d2b45bc28d8b41ec4340435bd462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 12 Aug 2022 09:50:38 +0200 Subject: [PATCH 19/81] ledger/tx: inherit StorageRead in TxEnv --- shared/src/ledger/tx_env.rs | 78 +++++---------------------- tests/src/vm_host_env/mod.rs | 2 +- tx_prelude/src/error.rs | 9 ++++ tx_prelude/src/ibc.rs | 1 + tx_prelude/src/intent.rs | 3 +- tx_prelude/src/lib.rs | 55 ++++++++++--------- wasm_for_tests/wasm_source/src/lib.rs | 6 ++- 7 files changed, 61 insertions(+), 93 deletions(-) diff --git a/shared/src/ledger/tx_env.rs b/shared/src/ledger/tx_env.rs index 578357b733..1fc7fa788c 100644 --- a/shared/src/ledger/tx_env.rs +++ b/shared/src/ledger/tx_env.rs @@ -1,87 +1,35 @@ //! Transaction environment contains functions that can be called from //! inside a tx. -use borsh::{BorshDeserialize, BorshSerialize}; +use borsh::BorshSerialize; +use crate::ledger::storage_api::{self, StorageRead}; use crate::types::address::Address; use crate::types::ibc::IbcEvent; -use crate::types::storage::{self, BlockHash, BlockHeight, Epoch}; +use crate::types::storage; use crate::types::time::Rfc3339String; /// Transaction host functions -pub trait TxEnv { - /// Storage read prefix iterator - type PrefixIter; - - /// Host functions possible errors, extensible with custom user errors. +pub trait TxEnv: StorageRead { + /// Host env functions possible errors type Error; - /// Storage read Borsh encoded value. It will try to read from the write log - /// first and if no entry found then from the storage and then decode it if - /// found. - fn read( - &self, - key: &storage::Key, - ) -> Result, Self::Error>; - - /// Storage read raw bytes. It will try to read from the write log first and - /// if no entry found then from the storage. - fn read_bytes( - &self, - key: &storage::Key, - ) -> Result>, Self::Error>; - - /// Check if the storage contains the given key. It will try - /// to check the write log first and if no entry found then the storage. - fn has_key(&self, key: &storage::Key) -> Result; - - /// Getting the chain ID. - fn get_chain_id(&self) -> Result; - - /// Getting the block height. The height is that of the block to which the - /// current transaction is being applied. - fn get_block_height(&self) -> Result; - - /// Getting the block hash. The height is that of the block to which the - /// current transaction is being applied. - fn get_block_hash(&self) -> Result; - - /// Getting the block epoch. The epoch is that of the block to which the - /// current transaction is being applied. - fn get_block_epoch(&self) -> Result; - - /// Get time of the current block header as rfc 3339 string - fn get_block_time(&self) -> Result; - - /// Storage prefix iterator. It will try to get an iterator from the - /// storage. - fn iter_prefix( - &self, - prefix: &storage::Key, - ) -> Result; - - /// Storage prefix iterator next. It will try to read from the write log - /// first and if no entry found then from the storage. - fn iter_next( - &self, - iter: &mut Self::PrefixIter, - ) -> Result)>, Self::Error>; - - // --- MUTABLE ---- - /// Write a value to be encoded with Borsh at the given key to storage. fn write( &mut self, key: &storage::Key, val: T, - ) -> Result<(), Self::Error>; + ) -> Result<(), storage_api::Error>; /// Write a value as bytes at the given key to storage. fn write_bytes( &mut self, key: &storage::Key, val: impl AsRef<[u8]>, - ) -> Result<(), Self::Error>; + ) -> Result<(), storage_api::Error>; + + /// Delete a value at the given key from storage. + fn delete(&mut self, key: &storage::Key) -> Result<(), storage_api::Error>; /// Write a temporary value to be encoded with Borsh at the given key to /// storage. @@ -98,9 +46,6 @@ pub trait TxEnv { val: impl AsRef<[u8]>, ) -> Result<(), Self::Error>; - /// Delete a value at the given key from storage. - fn delete(&mut self, key: &storage::Key) -> Result<(), Self::Error>; - /// Insert a verifier address. This address must exist on chain, otherwise /// the transaction will be rejected. /// @@ -126,4 +71,7 @@ pub trait TxEnv { /// Emit an IBC event. There can be only one event per transaction. On /// multiple calls, only the last emitted event will be used. fn emit_ibc_event(&mut self, event: &IbcEvent) -> Result<(), Self::Error>; + + /// Get time of the current block header as rfc 3339 string + fn get_block_time(&self) -> Result; } diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index d539289533..cf9dccfb56 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -34,7 +34,7 @@ mod tests { use namada::types::time::DateTimeUtc; use namada::types::token::{self, Amount}; use namada::types::{address, key}; - use namada_tx_prelude::{BorshDeserialize, BorshSerialize}; + use namada_tx_prelude::{BorshDeserialize, BorshSerialize, StorageRead}; use namada_vp_prelude::VpEnv; use prost::Message; use test_log::test; diff --git a/tx_prelude/src/error.rs b/tx_prelude/src/error.rs index b34d954166..ce7b9fa5e9 100644 --- a/tx_prelude/src/error.rs +++ b/tx_prelude/src/error.rs @@ -5,6 +5,7 @@ //! avoiding `error[E0117]: only traits defined in the current crate can be //! implemented for arbitrary types` +use namada::ledger::storage_api; use thiserror::Error; #[allow(missing_docs)] @@ -101,3 +102,11 @@ impl std::fmt::Display for CustomError { self.0.fmt(f) } } + +impl From for Error { + fn from(err: storage_api::Error) -> Self { + // storage_api::Error::Custom(CustomError {Box}) + // Error:Custom(storage_api::Error::Custom(CustomError {Box})) + Self::new(err) + } +} diff --git a/tx_prelude/src/ibc.rs b/tx_prelude/src/ibc.rs index 21be213ea3..7be4e4c064 100644 --- a/tx_prelude/src/ibc.rs +++ b/tx_prelude/src/ibc.rs @@ -1,6 +1,7 @@ //! IBC lower-level functions for transactions. pub use namada::ledger::ibc::handler::{Error, IbcActions, Result}; +use namada::ledger::storage_api::StorageRead; use namada::ledger::tx_env::TxEnv; use namada::types::address::Address; pub use namada::types::ibc::IbcEvent; diff --git a/tx_prelude/src/intent.rs b/tx_prelude/src/intent.rs index 7b6826342e..05f7cede91 100644 --- a/tx_prelude/src/intent.rs +++ b/tx_prelude/src/intent.rs @@ -14,5 +14,6 @@ pub fn invalidate_exchange( let mut invalid_intent: HashSet = ctx.read(&key)?.unwrap_or_default(); invalid_intent.insert(intent.sig.clone()); - ctx.write(&key, &invalid_intent) + ctx.write(&key, &invalid_intent)?; + Ok(()) } diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index 5d0009b01b..42b63a892a 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -22,6 +22,8 @@ pub use error::*; pub use namada::ledger::governance::storage as gov_storage; pub use namada::ledger::parameters::storage as parameters_storage; pub use namada::ledger::storage::types::encode; +use namada::ledger::storage_api; +pub use namada::ledger::storage_api::StorageRead; pub use namada::ledger::treasury::storage as treasury_storage; pub use namada::ledger::tx_env::TxEnv; pub use namada::proto::{Signed, SignedTxData}; @@ -93,14 +95,13 @@ pub type TxResult = EnvResult<()>; #[derive(Debug)] pub struct KeyValIterator(pub u64, pub PhantomData); -impl TxEnv for Ctx { - type Error = Error; +impl StorageRead for Ctx { type PrefixIter = KeyValIterator<(String, Vec)>; fn read( &self, key: &namada::types::storage::Key, - ) -> Result, Error> { + ) -> Result, storage_api::Error> { let key = key.to_string(); let read_result = unsafe { anoma_tx_read(key.as_ptr() as _, key.len() as _) }; @@ -111,7 +112,7 @@ impl TxEnv for Ctx { fn read_bytes( &self, key: &namada::types::storage::Key, - ) -> Result>, Error> { + ) -> Result>, storage_api::Error> { let key = key.to_string(); let read_result = unsafe { anoma_tx_read(key.as_ptr() as _, key.len() as _) }; @@ -121,14 +122,14 @@ impl TxEnv for Ctx { fn has_key( &self, key: &namada::types::storage::Key, - ) -> Result { + ) -> Result { let key = key.to_string(); let found = unsafe { anoma_tx_has_key(key.as_ptr() as _, key.len() as _) }; Ok(HostEnvResult::is_success(found)) } - fn get_chain_id(&self) -> Result { + fn get_chain_id(&self) -> Result { let result = Vec::with_capacity(CHAIN_ID_LENGTH); unsafe { anoma_tx_get_chain_id(result.as_ptr() as _); @@ -141,13 +142,13 @@ impl TxEnv for Ctx { fn get_block_height( &self, - ) -> Result { + ) -> Result { Ok(BlockHeight(unsafe { anoma_tx_get_block_height() })) } fn get_block_hash( &self, - ) -> Result { + ) -> Result { let result = Vec::with_capacity(BLOCK_HASH_LENGTH); unsafe { anoma_tx_get_block_hash(result.as_ptr() as _); @@ -158,24 +159,16 @@ impl TxEnv for Ctx { Ok(BlockHash::try_from(slice).expect("Cannot convert the hash")) } - fn get_block_epoch(&self) -> Result { + fn get_block_epoch( + &self, + ) -> Result { Ok(Epoch(unsafe { anoma_tx_get_block_epoch() })) } - fn get_block_time(&self) -> Result { - let read_result = unsafe { anoma_tx_get_block_time() }; - let time_value = read_from_buffer(read_result, anoma_tx_result_buffer) - .expect("The block time should exist"); - Ok(Rfc3339String( - String::try_from_slice(&time_value[..]) - .expect("The conversion shouldn't fail"), - )) - } - fn iter_prefix( &self, prefix: &namada::types::storage::Key, - ) -> Result { + ) -> Result { let prefix = prefix.to_string(); let iter_id = unsafe { anoma_tx_iter_prefix(prefix.as_ptr() as _, prefix.len() as _) @@ -186,19 +179,33 @@ impl TxEnv for Ctx { fn iter_next( &self, iter: &mut Self::PrefixIter, - ) -> Result)>, Error> { + ) -> Result)>, storage_api::Error> { let read_result = unsafe { anoma_tx_iter_next(iter.0) }; Ok(read_key_val_bytes_from_buffer( read_result, anoma_tx_result_buffer, )) } +} + +impl TxEnv for Ctx { + type Error = Error; + + fn get_block_time(&self) -> Result { + let read_result = unsafe { anoma_tx_get_block_time() }; + let time_value = read_from_buffer(read_result, anoma_tx_result_buffer) + .expect("The block time should exist"); + Ok(Rfc3339String( + String::try_from_slice(&time_value[..]) + .expect("The conversion shouldn't fail"), + )) + } fn write( &mut self, key: &namada::types::storage::Key, val: T, - ) -> Result<(), Error> { + ) -> storage_api::Result<()> { let buf = val.try_to_vec().unwrap(); self.write_bytes(key, buf) } @@ -207,7 +214,7 @@ impl TxEnv for Ctx { &mut self, key: &namada::types::storage::Key, val: impl AsRef<[u8]>, - ) -> Result<(), Error> { + ) -> storage_api::Result<()> { let key = key.to_string(); unsafe { anoma_tx_write( @@ -249,7 +256,7 @@ impl TxEnv for Ctx { fn delete( &mut self, key: &namada::types::storage::Key, - ) -> Result<(), Error> { + ) -> storage_api::Result<()> { let key = key.to_string(); unsafe { anoma_tx_delete(key.as_ptr() as _, key.len() as _) }; Ok(()) diff --git a/wasm_for_tests/wasm_source/src/lib.rs b/wasm_for_tests/wasm_source/src/lib.rs index 0e47437704..2b6ef24242 100644 --- a/wasm_for_tests/wasm_source/src/lib.rs +++ b/wasm_for_tests/wasm_source/src/lib.rs @@ -119,7 +119,8 @@ pub mod main { "attempting to write new value {} to key {}", ARBITRARY_VALUE, key )); - ctx.write(&key, ARBITRARY_VALUE) + ctx.write(&key, ARBITRARY_VALUE)?; + Ok(()) } } @@ -147,7 +148,8 @@ pub mod main { let mut target_bal: token::Amount = ctx.read(&target_key)?.unwrap_or_default(); target_bal.receive(&amount); - ctx.write(&target_key, target_bal) + ctx.write(&target_key, target_bal)?; + Ok(()) } } From 10f94c2dcf950bcf6045da626c9e10fbc547dbc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 12 Aug 2022 09:48:55 +0200 Subject: [PATCH 20/81] ledger/pos: implement PosReadOnly using StorageRead trait --- shared/src/ledger/pos/mod.rs | 136 +++++++++++++++++ shared/src/ledger/pos/vp.rs | 246 +++++++++++-------------------- tests/src/native_vp/pos.rs | 2 +- tx_prelude/src/proof_of_stake.rs | 106 ++----------- 4 files changed, 236 insertions(+), 254 deletions(-) diff --git a/shared/src/ledger/pos/mod.rs b/shared/src/ledger/pos/mod.rs index c980da81da..a9d72e84bb 100644 --- a/shared/src/ledger/pos/mod.rs +++ b/shared/src/ledger/pos/mod.rs @@ -87,3 +87,139 @@ impl From for Epoch { Epoch(epoch) } } + +#[macro_use] +mod macros { + /// Implement `PosReadOnly` for a type that implements + /// [`trait@crate::ledger::storage_api::StorageRead`]. + /// + /// Excuse the horrible syntax - we haven't found a better way to use this + /// for native_vp `CtxPreStorageRead`/`CtxPostStorageRead`, which have + /// generics and explicit lifetimes. + /// + /// # Examples + /// + /// ```ignore + /// impl_pos_read_only! { impl PosReadOnly for X } + /// ``` + #[macro_export] + macro_rules! impl_pos_read_only { + ( + // Type error type has to be declared before the impl. + // This error type must `impl From for $error`. + type $error:tt = $err_ty:ty ; + // Matches anything, so that we can use lifetimes and generic types. + // This expects `impl(<.*>)? PoSReadOnly for $ty(<.*>)?`. + $( $any:tt )* ) + => { + $( $any )* + { + type Address = $crate::types::address::Address; + // type Error = $crate::ledger::native_vp::Error; + type $error = $err_ty; + type PublicKey = $crate::types::key::common::PublicKey; + type TokenAmount = $crate::types::token::Amount; + type TokenChange = $crate::types::token::Change; + + const POS_ADDRESS: Self::Address = $crate::ledger::pos::ADDRESS; + + fn staking_token_address() -> Self::Address { + $crate::ledger::pos::staking_token_address() + } + + fn read_pos_params(&self) -> std::result::Result { + let value = $crate::ledger::storage_api::StorageRead::read_bytes(self, ¶ms_key())?.unwrap(); + Ok($crate::ledger::storage::types::decode(value).unwrap()) + } + + fn read_validator_staking_reward_address( + &self, + key: &Self::Address, + ) -> std::result::Result, Self::Error> { + let value = $crate::ledger::storage_api::StorageRead::read_bytes( + self, + &validator_staking_reward_address_key(key), + )?; + Ok(value.map(|value| $crate::ledger::storage::types::decode(value).unwrap())) + } + + fn read_validator_consensus_key( + &self, + key: &Self::Address, + ) -> std::result::Result, Self::Error> { + let value = + $crate::ledger::storage_api::StorageRead::read_bytes(self, &validator_consensus_key_key(key))?; + Ok(value.map(|value| $crate::ledger::storage::types::decode(value).unwrap())) + } + + fn read_validator_state( + &self, + key: &Self::Address, + ) -> std::result::Result, Self::Error> { + let value = $crate::ledger::storage_api::StorageRead::read_bytes(self, &validator_state_key(key))?; + Ok(value.map(|value| $crate::ledger::storage::types::decode(value).unwrap())) + } + + fn read_validator_total_deltas( + &self, + key: &Self::Address, + ) -> std::result::Result, Self::Error> { + let value = + $crate::ledger::storage_api::StorageRead::read_bytes(self, &validator_total_deltas_key(key))?; + Ok(value.map(|value| $crate::ledger::storage::types::decode(value).unwrap())) + } + + fn read_validator_voting_power( + &self, + key: &Self::Address, + ) -> std::result::Result, Self::Error> { + let value = + $crate::ledger::storage_api::StorageRead::read_bytes(self, &validator_voting_power_key(key))?; + Ok(value.map(|value| $crate::ledger::storage::types::decode(value).unwrap())) + } + + fn read_validator_slashes( + &self, + key: &Self::Address, + ) -> std::result::Result, Self::Error> { + let value = $crate::ledger::storage_api::StorageRead::read_bytes(self, &validator_slashes_key(key))?; + Ok(value + .map(|value| $crate::ledger::storage::types::decode(value).unwrap()) + .unwrap_or_default()) + } + + fn read_bond( + &self, + key: &BondId, + ) -> std::result::Result, Self::Error> { + let value = $crate::ledger::storage_api::StorageRead::read_bytes(self, &bond_key(key))?; + Ok(value.map(|value| $crate::ledger::storage::types::decode(value).unwrap())) + } + + fn read_unbond( + &self, + key: &BondId, + ) -> std::result::Result, Self::Error> { + let value = $crate::ledger::storage_api::StorageRead::read_bytes(self, &unbond_key(key))?; + Ok(value.map(|value| $crate::ledger::storage::types::decode(value).unwrap())) + } + + fn read_validator_set( + &self, + ) -> std::result::Result { + let value = + $crate::ledger::storage_api::StorageRead::read_bytes(self, &validator_set_key())?.unwrap(); + Ok($crate::ledger::storage::types::decode(value).unwrap()) + } + + fn read_total_voting_power( + &self, + ) -> std::result::Result { + let value = + $crate::ledger::storage_api::StorageRead::read_bytes(self, &total_voting_power_key())?.unwrap(); + Ok($crate::ledger::storage::types::decode(value).unwrap()) + } + } + } +} +} diff --git a/shared/src/ledger/pos/vp.rs b/shared/src/ledger/pos/vp.rs index 80572c1f57..2f19b6d1a4 100644 --- a/shared/src/ledger/pos/vp.rs +++ b/shared/src/ledger/pos/vp.rs @@ -26,17 +26,20 @@ use super::{ validator_total_deltas_key, validator_voting_power_key, BondId, Bonds, Unbonds, ValidatorConsensusKeys, ValidatorSets, ValidatorTotalDeltas, }; +use crate::impl_pos_read_only; use crate::ledger::governance::vp::is_proposal_accepted; -use crate::ledger::native_vp::{self, Ctx, NativeVp, VpEnv}; +use crate::ledger::native_vp::{ + self, Ctx, CtxPostStorageRead, CtxPreStorageRead, NativeVp, +}; use crate::ledger::pos::{ is_validator_address_raw_hash_key, is_validator_consensus_key_key, is_validator_state_key, }; -use crate::ledger::storage::types::decode; use crate::ledger::storage::{self as ledger_storage, StorageHasher}; +use crate::ledger::storage_api::{self, StorageRead}; use crate::types::address::{Address, InternalAddress}; use crate::types::storage::{Key, KeySeg}; -use crate::types::{key, token}; +use crate::types::token; use crate::vm::WasmCacheAccess; #[allow(missing_docs)] @@ -44,6 +47,8 @@ use crate::vm::WasmCacheAccess; pub enum Error { #[error("Native VP error: {0}")] NativeVpError(native_vp::Error), + #[error("Storage error: {0}")] + StorageApi(storage_api::Error), } /// PoS functions result @@ -118,7 +123,8 @@ where let addr = Address::Internal(Self::ADDR); let mut changes: Vec> = vec![]; - let current_epoch = self.ctx.get_block_epoch()?; + let current_epoch = self.ctx.pre().get_block_epoch()?; + for key in keys_changed { if is_params_key(key) { let proposal_id = u64::try_from_slice(tx_data).ok(); @@ -127,8 +133,8 @@ where _ => return Ok(false), } } else if let Some(owner) = key.is_validity_predicate() { - let has_pre = self.ctx.has_key_pre(key)?; - let has_post = self.ctx.has_key_post(key)?; + let has_pre = self.ctx.pre().has_key(key)?; + let has_post = self.ctx.post().has_key(key)?; if has_pre && has_post { // VP updates must be verified by the owner return Ok(!verifiers.contains(owner)); @@ -137,18 +143,18 @@ where return Ok(false); } } else if is_validator_set_key(key) { - let pre = self.ctx.read_bytes_pre(key)?.and_then(|bytes| { + let pre = self.ctx.pre().read_bytes(key)?.and_then(|bytes| { ValidatorSets::try_from_slice(&bytes[..]).ok() }); - let post = self.ctx.read_bytes_post(key)?.and_then(|bytes| { + let post = self.ctx.post().read_bytes(key)?.and_then(|bytes| { ValidatorSets::try_from_slice(&bytes[..]).ok() }); changes.push(ValidatorSet(Data { pre, post })); } else if let Some(validator) = is_validator_state_key(key) { - let pre = self.ctx.read_bytes_pre(key)?.and_then(|bytes| { + let pre = self.ctx.pre().read_bytes(key)?.and_then(|bytes| { ValidatorStates::try_from_slice(&bytes[..]).ok() }); - let post = self.ctx.read_bytes_post(key)?.and_then(|bytes| { + let post = self.ctx.post().read_bytes(key)?.and_then(|bytes| { ValidatorStates::try_from_slice(&bytes[..]).ok() }); changes.push(Validator { @@ -158,24 +164,24 @@ where } else if let Some(validator) = is_validator_staking_reward_address_key(key) { - let pre = self - .ctx - .read_bytes_pre(key)? - .and_then(|bytes| Address::try_from_slice(&bytes[..]).ok()); - let post = self - .ctx - .read_bytes_post(key)? - .and_then(|bytes| Address::try_from_slice(&bytes[..]).ok()); + let pre = + self.ctx.pre().read_bytes(key)?.and_then(|bytes| { + Address::try_from_slice(&bytes[..]).ok() + }); + let post = + self.ctx.post().read_bytes(key)?.and_then(|bytes| { + Address::try_from_slice(&bytes[..]).ok() + }); changes.push(Validator { address: validator.clone(), update: StakingRewardAddress(Data { pre, post }), }); } else if let Some(validator) = is_validator_consensus_key_key(key) { - let pre = self.ctx.read_bytes_pre(key)?.and_then(|bytes| { + let pre = self.ctx.pre().read_bytes(key)?.and_then(|bytes| { ValidatorConsensusKeys::try_from_slice(&bytes[..]).ok() }); - let post = self.ctx.read_bytes_post(key)?.and_then(|bytes| { + let post = self.ctx.post().read_bytes(key)?.and_then(|bytes| { ValidatorConsensusKeys::try_from_slice(&bytes[..]).ok() }); changes.push(Validator { @@ -183,10 +189,10 @@ where update: ConsensusKey(Data { pre, post }), }); } else if let Some(validator) = is_validator_total_deltas_key(key) { - let pre = self.ctx.read_bytes_pre(key)?.and_then(|bytes| { + let pre = self.ctx.pre().read_bytes(key)?.and_then(|bytes| { ValidatorTotalDeltas::try_from_slice(&bytes[..]).ok() }); - let post = self.ctx.read_bytes_post(key)?.and_then(|bytes| { + let post = self.ctx.post().read_bytes(key)?.and_then(|bytes| { ValidatorTotalDeltas::try_from_slice(&bytes[..]).ok() }); changes.push(Validator { @@ -194,10 +200,10 @@ where update: TotalDeltas(Data { pre, post }), }); } else if let Some(validator) = is_validator_voting_power_key(key) { - let pre = self.ctx.read_bytes_pre(key)?.and_then(|bytes| { + let pre = self.ctx.pre().read_bytes(key)?.and_then(|bytes| { ValidatorVotingPowers::try_from_slice(&bytes[..]).ok() }); - let post = self.ctx.read_bytes_post(key)?.and_then(|bytes| { + let post = self.ctx.post().read_bytes(key)?.and_then(|bytes| { ValidatorVotingPowers::try_from_slice(&bytes[..]).ok() }); changes.push(Validator { @@ -207,14 +213,14 @@ where } else if let Some(raw_hash) = is_validator_address_raw_hash_key(key) { - let pre = self - .ctx - .read_bytes_pre(key)? - .and_then(|bytes| Address::try_from_slice(&bytes[..]).ok()); - let post = self - .ctx - .read_bytes_post(key)? - .and_then(|bytes| Address::try_from_slice(&bytes[..]).ok()); + let pre = + self.ctx.pre().read_bytes(key)?.and_then(|bytes| { + Address::try_from_slice(&bytes[..]).ok() + }); + let post = + self.ctx.post().read_bytes(key)?.and_then(|bytes| { + Address::try_from_slice(&bytes[..]).ok() + }); // Find the raw hashes of the addresses let pre = pre.map(|pre| { let raw_hash = @@ -236,26 +242,27 @@ where if owner != &addr { continue; } - let pre = self.ctx.read_bytes_pre(key)?.and_then(|bytes| { + let pre = self.ctx.pre().read_bytes(key)?.and_then(|bytes| { token::Amount::try_from_slice(&bytes[..]).ok() }); - let post = self.ctx.read_bytes_post(key)?.and_then(|bytes| { + let post = self.ctx.post().read_bytes(key)?.and_then(|bytes| { token::Amount::try_from_slice(&bytes[..]).ok() }); changes.push(Balance(Data { pre, post })); } else if let Some(bond_id) = is_bond_key(key) { - let pre = self - .ctx - .read_bytes_pre(key)? - .and_then(|bytes| Bonds::try_from_slice(&bytes[..]).ok()); - let post = self - .ctx - .read_bytes_post(key)? - .and_then(|bytes| Bonds::try_from_slice(&bytes[..]).ok()); + let pre = + self.ctx.pre().read_bytes(key)?.and_then(|bytes| { + Bonds::try_from_slice(&bytes[..]).ok() + }); + let post = + self.ctx.post().read_bytes(key)?.and_then(|bytes| { + Bonds::try_from_slice(&bytes[..]).ok() + }); // For bonds, we need to look-up slashes let slashes = self .ctx - .read_bytes_pre(&validator_slashes_key(&bond_id.validator))? + .pre() + .read_bytes(&validator_slashes_key(&bond_id.validator))? .and_then(|bytes| Slashes::try_from_slice(&bytes[..]).ok()) .unwrap_or_default(); changes.push(Bond { @@ -264,20 +271,19 @@ where slashes, }); } else if let Some(unbond_id) = is_unbond_key(key) { - let pre = self - .ctx - .read_bytes_pre(key)? - .and_then(|bytes| Unbonds::try_from_slice(&bytes[..]).ok()); - let post = self - .ctx - .read_bytes_post(key)? - .and_then(|bytes| Unbonds::try_from_slice(&bytes[..]).ok()); + let pre = + self.ctx.pre().read_bytes(key)?.and_then(|bytes| { + Unbonds::try_from_slice(&bytes[..]).ok() + }); + let post = + self.ctx.post().read_bytes(key)?.and_then(|bytes| { + Unbonds::try_from_slice(&bytes[..]).ok() + }); // For unbonds, we need to look-up slashes let slashes = self .ctx - .read_bytes_pre(&validator_slashes_key( - &unbond_id.validator, - ))? + .pre() + .read_bytes(&validator_slashes_key(&unbond_id.validator))? .and_then(|bytes| Slashes::try_from_slice(&bytes[..]).ok()) .unwrap_or_default(); changes.push(Unbond { @@ -286,10 +292,10 @@ where slashes, }); } else if is_total_voting_power_key(key) { - let pre = self.ctx.read_bytes_pre(key)?.and_then(|bytes| { + let pre = self.ctx.pre().read_bytes(key)?.and_then(|bytes| { TotalVotingPowers::try_from_slice(&bytes[..]).ok() }); - let post = self.ctx.read_bytes_post(key)?.and_then(|bytes| { + let post = self.ctx.post().read_bytes(key)?.and_then(|bytes| { TotalVotingPowers::try_from_slice(&bytes[..]).ok() }); changes.push(TotalVotingPower(Data { pre, post })); @@ -303,7 +309,7 @@ where } } - let params = self.read_pos_params()?; + let params = self.ctx.pre().read_pos_params()?; let errors = validate(¶ms, changes, current_epoch); Ok(if errors.is_empty() { true @@ -317,114 +323,22 @@ where } } -impl PosReadOnly for PosVP<'_, D, H, CA> -where - D: 'static + ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, - H: 'static + StorageHasher, - CA: 'static + WasmCacheAccess, -{ - type Address = Address; +impl_pos_read_only! { type Error = native_vp::Error; - type PublicKey = key::common::PublicKey; - type TokenAmount = token::Amount; - type TokenChange = token::Change; - - const POS_ADDRESS: Self::Address = super::ADDRESS; - - fn staking_token_address() -> Self::Address { - super::staking_token_address() - } - - fn read_pos_params(&self) -> std::result::Result { - let value = self.ctx.read_bytes_pre(¶ms_key())?.unwrap(); - Ok(decode(value).unwrap()) - } - - fn read_validator_staking_reward_address( - &self, - key: &Self::Address, - ) -> std::result::Result, Self::Error> { - let value = self - .ctx - .read_bytes_pre(&validator_staking_reward_address_key(key))?; - Ok(value.map(|value| decode(value).unwrap())) - } - - fn read_validator_consensus_key( - &self, - key: &Self::Address, - ) -> std::result::Result, Self::Error> { - let value = - self.ctx.read_bytes_pre(&validator_consensus_key_key(key))?; - Ok(value.map(|value| decode(value).unwrap())) - } - - fn read_validator_state( - &self, - key: &Self::Address, - ) -> std::result::Result, Self::Error> { - let value = self.ctx.read_bytes_pre(&validator_state_key(key))?; - Ok(value.map(|value| decode(value).unwrap())) - } - - fn read_validator_total_deltas( - &self, - key: &Self::Address, - ) -> std::result::Result, Self::Error> { - let value = - self.ctx.read_bytes_pre(&validator_total_deltas_key(key))?; - Ok(value.map(|value| decode(value).unwrap())) - } - - fn read_validator_voting_power( - &self, - key: &Self::Address, - ) -> std::result::Result, Self::Error> { - let value = - self.ctx.read_bytes_pre(&validator_voting_power_key(key))?; - Ok(value.map(|value| decode(value).unwrap())) - } - - fn read_validator_slashes( - &self, - key: &Self::Address, - ) -> std::result::Result, Self::Error> { - let value = self.ctx.read_bytes_pre(&validator_slashes_key(key))?; - Ok(value - .map(|value| decode(value).unwrap()) - .unwrap_or_default()) - } - - fn read_bond( - &self, - key: &BondId, - ) -> std::result::Result, Self::Error> { - let value = self.ctx.read_bytes_pre(&bond_key(key))?; - Ok(value.map(|value| decode(value).unwrap())) - } - - fn read_unbond( - &self, - key: &BondId, - ) -> std::result::Result, Self::Error> { - let value = self.ctx.read_bytes_pre(&unbond_key(key))?; - Ok(value.map(|value| decode(value).unwrap())) - } - - fn read_validator_set( - &self, - ) -> std::result::Result { - let value = self.ctx.read_bytes_pre(&validator_set_key())?.unwrap(); - Ok(decode(value).unwrap()) - } + impl<'f, 'a, DB, H, CA> PosReadOnly for CtxPreStorageRead<'f, 'a, DB, H, CA> + where + DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter> +'static, + H: StorageHasher +'static, + CA: WasmCacheAccess +'static +} - fn read_total_voting_power( - &self, - ) -> std::result::Result { - let value = - self.ctx.read_bytes_pre(&total_voting_power_key())?.unwrap(); - Ok(decode(value).unwrap()) - } +impl_pos_read_only! { + type Error = native_vp::Error; + impl<'f, 'a, DB, H, CA> PosReadOnly for CtxPostStorageRead<'f, 'a, DB, H, CA> + where + DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter> +'static, + H: StorageHasher +'static, + CA: WasmCacheAccess +'static } impl From for Error { @@ -432,3 +346,9 @@ impl From for Error { Self::NativeVpError(err) } } + +impl From for Error { + fn from(err: storage_api::Error) -> Self { + Self::StorageApi(err) + } +} diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index 1700be2264..af63d1f175 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -552,7 +552,7 @@ pub mod testing { use namada_tx_prelude::proof_of_stake::{ staking_token_address, BondId, Bonds, PosParams, Unbonds, }; - use namada_tx_prelude::Address; + use namada_tx_prelude::{Address, StorageRead}; use proptest::prelude::*; use crate::tx::{self, tx_host_env}; diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index ce856cd876..65a6c3f6cd 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -1,6 +1,5 @@ //! Proof of Stake system integration with functions for transactions -use namada::ledger::pos::types::Slash; pub use namada::ledger::pos::*; use namada::ledger::pos::{ bond_key, namada_proof_of_stake, params_key, total_voting_power_key, @@ -9,7 +8,7 @@ use namada::ledger::pos::{ validator_staking_reward_address_key, validator_state_key, validator_total_deltas_key, validator_voting_power_key, }; -use namada::types::address::{self, Address, InternalAddress}; +use namada::types::address::Address; use namada::types::transaction::InitValidator; use namada::types::{key, token}; pub use namada_proof_of_stake::{ @@ -114,89 +113,9 @@ impl Ctx { } } -impl namada_proof_of_stake::PosReadOnly for Ctx { - type Address = Address; +namada::impl_pos_read_only! { type Error = crate::Error; - type PublicKey = key::common::PublicKey; - type TokenAmount = token::Amount; - type TokenChange = token::Change; - - const POS_ADDRESS: Self::Address = Address::Internal(InternalAddress::PoS); - - fn staking_token_address() -> Self::Address { - address::xan() - } - - fn read_pos_params(&self) -> Result { - let params = self.read(¶ms_key())?; - Ok(params.expect("PoS params should always be set")) - } - - fn read_validator_staking_reward_address( - &self, - key: &Self::Address, - ) -> Result, Self::Error> { - self.read(&validator_staking_reward_address_key(key)) - } - - fn read_validator_consensus_key( - &self, - key: &Self::Address, - ) -> Result, Self::Error> { - self.read(&validator_consensus_key_key(key)) - } - - fn read_validator_state( - &self, - key: &Self::Address, - ) -> Result, Self::Error> { - self.read(&validator_state_key(key)) - } - - fn read_validator_total_deltas( - &self, - key: &Self::Address, - ) -> Result, Self::Error> { - self.read(&validator_total_deltas_key(key)) - } - - fn read_validator_voting_power( - &self, - key: &Self::Address, - ) -> Result, Self::Error> { - self.read(&validator_voting_power_key(key)) - } - - fn read_validator_slashes( - &self, - key: &Self::Address, - ) -> Result, Self::Error> { - let val = self.read(&validator_slashes_key(key))?; - Ok(val.unwrap_or_default()) - } - - fn read_bond(&self, key: &BondId) -> Result, Self::Error> { - self.read(&bond_key(key)) - } - - fn read_unbond( - &self, - key: &BondId, - ) -> Result, Self::Error> { - self.read(&unbond_key(key)) - } - - fn read_validator_set(&self) -> Result { - let val = self.read(&validator_set_key())?; - Ok(val.expect("Validator sets must always have a value")) - } - - fn read_total_voting_power( - &self, - ) -> Result { - let val = self.read(&total_voting_power_key())?; - Ok(val.expect("Total voting power must always have a value")) - } + impl namada_proof_of_stake::PosReadOnly for Ctx } impl From> for Error { @@ -237,7 +156,7 @@ impl namada_proof_of_stake::PosActions for Ctx { &mut self, params: &PosParams, ) -> Result<(), Self::Error> { - self.write(¶ms_key(), params) + self.write(¶ms_key(), params).into_env_result() } fn write_validator_address_raw_hash( @@ -246,6 +165,7 @@ impl namada_proof_of_stake::PosActions for Ctx { ) -> Result<(), Self::Error> { let raw_hash = address.raw_hash().unwrap().to_owned(); self.write(&validator_address_raw_hash_key(raw_hash), address) + .into_env_result() } fn write_validator_staking_reward_address( @@ -254,6 +174,7 @@ impl namada_proof_of_stake::PosActions for Ctx { value: Self::Address, ) -> Result<(), Self::Error> { self.write(&validator_staking_reward_address_key(key), &value) + .into_env_result() } fn write_validator_consensus_key( @@ -262,6 +183,7 @@ impl namada_proof_of_stake::PosActions for Ctx { value: ValidatorConsensusKeys, ) -> Result<(), Self::Error> { self.write(&validator_consensus_key_key(key), &value) + .into_env_result() } fn write_validator_state( @@ -270,6 +192,7 @@ impl namada_proof_of_stake::PosActions for Ctx { value: ValidatorStates, ) -> Result<(), Self::Error> { self.write(&validator_state_key(key), &value) + .into_env_result() } fn write_validator_total_deltas( @@ -278,6 +201,7 @@ impl namada_proof_of_stake::PosActions for Ctx { value: ValidatorTotalDeltas, ) -> Result<(), Self::Error> { self.write(&validator_total_deltas_key(key), &value) + .into_env_result() } fn write_validator_voting_power( @@ -286,6 +210,7 @@ impl namada_proof_of_stake::PosActions for Ctx { value: ValidatorVotingPowers, ) -> Result<(), Self::Error> { self.write(&validator_voting_power_key(key), &value) + .into_env_result() } fn write_bond( @@ -293,7 +218,7 @@ impl namada_proof_of_stake::PosActions for Ctx { key: &BondId, value: Bonds, ) -> Result<(), Self::Error> { - self.write(&bond_key(key), &value) + self.write(&bond_key(key), &value).into_env_result() } fn write_unbond( @@ -301,14 +226,14 @@ impl namada_proof_of_stake::PosActions for Ctx { key: &BondId, value: Unbonds, ) -> Result<(), Self::Error> { - self.write(&unbond_key(key), &value) + self.write(&unbond_key(key), &value).into_env_result() } fn write_validator_set( &mut self, value: ValidatorSets, ) -> Result<(), Self::Error> { - self.write(&validator_set_key(), &value) + self.write(&validator_set_key(), &value).into_env_result() } fn write_total_voting_power( @@ -316,14 +241,15 @@ impl namada_proof_of_stake::PosActions for Ctx { value: TotalVotingPowers, ) -> Result<(), Self::Error> { self.write(&total_voting_power_key(), &value) + .into_env_result() } fn delete_bond(&mut self, key: &BondId) -> Result<(), Self::Error> { - self.delete(&bond_key(key)) + self.delete(&bond_key(key)).into_env_result() } fn delete_unbond(&mut self, key: &BondId) -> Result<(), Self::Error> { - self.delete(&unbond_key(key)) + self.delete(&unbond_key(key)).into_env_result() } fn transfer( From eeb1e8cfd8bc151c7c19bf8c35b22514196fbef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 12 Aug 2022 10:10:53 +0200 Subject: [PATCH 21/81] update wasm checksums --- wasm/checksums.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index b8e98c6cf2..4654a502c5 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,19 +1,19 @@ { - "tx_bond.wasm": "tx_bond.091bf7885488fe9d01643d448236a3c5a8496fc72b1992d98633fa9954a79505.wasm", - "tx_from_intent.wasm": "tx_from_intent.65511fd08da70dc102f8c9f2c4c96fa611d7d2c9b680bcd323da242b1ce6d563.wasm", - "tx_ibc.wasm": "tx_ibc.bfc87f17efbd799cfa73487b7e080f845a6004cdc5f9b26c74cee93587f9d3c4.wasm", - "tx_init_account.wasm": "tx_init_account.ca95fc31bafe84f9bccc4e01336d2d993f4652fef9ea3d0bcfc10edbd9ef30d8.wasm", - "tx_init_nft.wasm": "tx_init_nft.683188dc9eceaf55263d4021bb0fe6c733a74e79998370fc54994eff0d4fd061.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.c4612a75320aa5ceb23cd61da9b43cf89b15feadad9fc28886eadfdeac38bbca.wasm", - "tx_init_validator.wasm": "tx_init_validator.05b94e3de1ff997c830ec0393df6d51fdcbe0410b28e3b9b4cc585c7132141b7.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.e29e4510c1bb9df5b98671ab2b5b5c728d21812924398e810f8d4a00885d9b98.wasm", - "tx_transfer.wasm": "tx_transfer.38739d844a43275e1e4bc18f79a225d1abece7bf302a49763073c1a5188f54d9.wasm", - "tx_unbond.wasm": "tx_unbond.c943d4a1759b2912dc1762e9bce55f16226db12b081afce912e3fa1a28459ba2.wasm", + "tx_bond.wasm": "tx_bond.683e3d5d9ad06b24df792e1a6a5f113fa238bc5635949a8da8f6826a0e338367.wasm", + "tx_from_intent.wasm": "tx_from_intent.51ab8df28b2e09232469043903ed490a42fa04afaa7827b42d5d895906a45ed8.wasm", + "tx_ibc.wasm": "tx_ibc.d79a0434a22727d0c3b926e2fb02969fe02efa58bff02b94b6f64ea6b6c6f0b2.wasm", + "tx_init_account.wasm": "tx_init_account.9a35cbe6781c2df33beb894ac2b1d323d575f859f119a43f34857eb34d7c22d2.wasm", + "tx_init_nft.wasm": "tx_init_nft.8079d16641f9f2af458f3c9818ffa2250994fdd20ec5fcdba87feab23c3576c6.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.3f5057d1a874eaa92f8ba8a8f8afc83a20139edb4bf2056300ce0c8e463950ad.wasm", + "tx_init_validator.wasm": "tx_init_validator.32936b7d7d0cdd961f11a1759f96dc09a0329528f3b2cc03ecf60751fa31f798.wasm", + "tx_mint_nft.wasm": "tx_mint_nft.a41830bed7d64926f8c4e2fe8ea56a7b485a26402fa3b0556d6ba14bc105e808.wasm", + "tx_transfer.wasm": "tx_transfer.11b66a337f337c836e4a7dd3356cc16ecede1c7c14a3dd56ed08c96557c03025.wasm", + "tx_unbond.wasm": "tx_unbond.db1f677a158a5ac1bda63d653a8f22f59ed72e0e425d55918ef70e87a4aa95e0.wasm", "tx_update_vp.wasm": "tx_update_vp.a304b3c70361cde5fda458ba5637d6825eb002198e73990a1c74351113b83d43.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.7c0b48866b9b31e913f338d0afaf41605f51c0cddf2e3a3f0d231e57e68c72f2.wasm", - "tx_withdraw.wasm": "tx_withdraw.1ec566946f3178c7a7e639e56eb4d1e26ac24e4d5cb856efd1a81bbe59390a3e.wasm", - "vp_nft.wasm": "vp_nft.ea4754591552e715ffdb125b649b3d1bb4b45ca93c3c808a51fa5ccaa9787d5c.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.70588d919e1c3d7a9c2ec37c67c5587c835ab0b9b485b73cec25b9413c9ccfd8.wasm", - "vp_token.wasm": "vp_token.5e619a0471b160712711bf44de9db1950f12900d1405dbcca0a2cea41d16a8a1.wasm", - "vp_user.wasm": "vp_user.3712b862e606effd77fc8c82058d960012ea5aef09580b8521de827484af27d1.wasm" + "tx_vote_proposal.wasm": "tx_vote_proposal.5e51a66746df8c7e786cc6837a74f87d239442ace67d1c4ef4e6751aa725514f.wasm", + "tx_withdraw.wasm": "tx_withdraw.b4705cbc2eb566faf1d14fff10190970f62aa90c30420e8b3ebbec0a85e7d04b.wasm", + "vp_nft.wasm": "vp_nft.1a32d37b32c616119d156128560a0eeb43206e91069e2c3875e74211ed3ad01f.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.4c91dfdf6ac6cdccc931ee391167033a9cdbd1b8551a428e90295bd0d6f68d85.wasm", + "vp_token.wasm": "vp_token.9f27c2e823691487bb6631fbfecb39837c1f404ef9b263e810a5a2a76f4c5957.wasm", + "vp_user.wasm": "vp_user.fb06cb724aa92d615b8da971b0ca1c89bf0caf8c1b9ea3536c252fd1c3b98881.wasm" } \ No newline at end of file From abd10c1079476d4f2643b641a1a5621af4a4007b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 12 Aug 2022 17:18:18 +0200 Subject: [PATCH 22/81] ledger: factor out TxEnv write methods into a new StorageWrite trait --- shared/src/ledger/storage_api/mod.rs | 22 ++++++++++- shared/src/ledger/tx_env.rs | 21 +--------- tests/src/native_vp/pos.rs | 3 +- tests/src/vm_host_env/ibc.rs | 1 + tests/src/vm_host_env/mod.rs | 4 +- tx_prelude/src/ibc.rs | 2 +- tx_prelude/src/lib.rs | 48 ++++++++++++----------- wasm/wasm_source/src/vp_nft.rs | 2 +- wasm/wasm_source/src/vp_testnet_faucet.rs | 2 +- wasm/wasm_source/src/vp_user.rs | 2 +- 10 files changed, 57 insertions(+), 50 deletions(-) diff --git a/shared/src/ledger/storage_api/mod.rs b/shared/src/ledger/storage_api/mod.rs index 0c0e4f4083..36e89a8171 100644 --- a/shared/src/ledger/storage_api/mod.rs +++ b/shared/src/ledger/storage_api/mod.rs @@ -3,7 +3,7 @@ mod error; -use borsh::BorshDeserialize; +use borsh::{BorshDeserialize, BorshSerialize}; pub use error::{CustomError, Error, Result, ResultExt}; use crate::types::storage::{self, BlockHash, BlockHeight, Epoch}; @@ -51,3 +51,23 @@ pub trait StorageRead { /// current transaction is being applied. fn get_block_epoch(&self) -> Result; } + +/// Common storage write interface +pub trait StorageWrite { + /// Write a value to be encoded with Borsh at the given key to storage. + fn write( + &mut self, + key: &storage::Key, + val: T, + ) -> Result<()>; + + /// Write a value as bytes at the given key to storage. + fn write_bytes( + &mut self, + key: &storage::Key, + val: impl AsRef<[u8]>, + ) -> Result<()>; + + /// Delete a value at the given key from storage. + fn delete(&mut self, key: &storage::Key) -> Result<()>; +} diff --git a/shared/src/ledger/tx_env.rs b/shared/src/ledger/tx_env.rs index 1fc7fa788c..421cc84687 100644 --- a/shared/src/ledger/tx_env.rs +++ b/shared/src/ledger/tx_env.rs @@ -3,34 +3,17 @@ use borsh::BorshSerialize; -use crate::ledger::storage_api::{self, StorageRead}; +use crate::ledger::storage_api::{StorageRead, StorageWrite}; use crate::types::address::Address; use crate::types::ibc::IbcEvent; use crate::types::storage; use crate::types::time::Rfc3339String; /// Transaction host functions -pub trait TxEnv: StorageRead { +pub trait TxEnv: StorageRead + StorageWrite { /// Host env functions possible errors type Error; - /// Write a value to be encoded with Borsh at the given key to storage. - fn write( - &mut self, - key: &storage::Key, - val: T, - ) -> Result<(), storage_api::Error>; - - /// Write a value as bytes at the given key to storage. - fn write_bytes( - &mut self, - key: &storage::Key, - val: impl AsRef<[u8]>, - ) -> Result<(), storage_api::Error>; - - /// Delete a value at the given key from storage. - fn delete(&mut self, key: &storage::Key) -> Result<(), storage_api::Error>; - /// Write a temporary value to be encoded with Borsh at the given key to /// storage. fn write_temp( diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index af63d1f175..1c68ac0269 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -537,7 +537,6 @@ pub mod testing { use derivative::Derivative; use itertools::Either; use namada::ledger::pos::namada_proof_of_stake::btree_set::BTreeSetShims; - use namada::ledger::tx_env::TxEnv; use namada::types::key::common::PublicKey; use namada::types::key::RefTo; use namada::types::storage::Epoch; @@ -552,7 +551,7 @@ pub mod testing { use namada_tx_prelude::proof_of_stake::{ staking_token_address, BondId, Bonds, PosParams, Unbonds, }; - use namada_tx_prelude::{Address, StorageRead}; + use namada_tx_prelude::{Address, StorageRead, StorageWrite}; use proptest::prelude::*; use crate::tx::{self, tx_host_env}; diff --git a/tests/src/vm_host_env/ibc.rs b/tests/src/vm_host_env/ibc.rs index a838f37819..72b33f9e5e 100644 --- a/tests/src/vm_host_env/ibc.rs +++ b/tests/src/vm_host_env/ibc.rs @@ -68,6 +68,7 @@ use namada::types::ibc::data::FungibleTokenPacketData; use namada::types::storage::{self, BlockHash, BlockHeight}; use namada::types::token::{self, Amount}; use namada::vm::{wasm, WasmCacheRwAccess}; +use namada_tx_prelude::StorageWrite; use crate::tx::{self, *}; diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index cf9dccfb56..37d1afb790 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -34,7 +34,9 @@ mod tests { use namada::types::time::DateTimeUtc; use namada::types::token::{self, Amount}; use namada::types::{address, key}; - use namada_tx_prelude::{BorshDeserialize, BorshSerialize, StorageRead}; + use namada_tx_prelude::{ + BorshDeserialize, BorshSerialize, StorageRead, StorageWrite, + }; use namada_vp_prelude::VpEnv; use prost::Message; use test_log::test; diff --git a/tx_prelude/src/ibc.rs b/tx_prelude/src/ibc.rs index 7be4e4c064..4a8a3ef3a9 100644 --- a/tx_prelude/src/ibc.rs +++ b/tx_prelude/src/ibc.rs @@ -1,7 +1,7 @@ //! IBC lower-level functions for transactions. pub use namada::ledger::ibc::handler::{Error, IbcActions, Result}; -use namada::ledger::storage_api::StorageRead; +use namada::ledger::storage_api::{StorageRead, StorageWrite}; use namada::ledger::tx_env::TxEnv; use namada::types::address::Address; pub use namada::types::ibc::IbcEvent; diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index 42b63a892a..dd9b973921 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -23,7 +23,7 @@ pub use namada::ledger::governance::storage as gov_storage; pub use namada::ledger::parameters::storage as parameters_storage; pub use namada::ledger::storage::types::encode; use namada::ledger::storage_api; -pub use namada::ledger::storage_api::StorageRead; +pub use namada::ledger::storage_api::{StorageRead, StorageWrite}; pub use namada::ledger::treasury::storage as treasury_storage; pub use namada::ledger::tx_env::TxEnv; pub use namada::proto::{Signed, SignedTxData}; @@ -188,19 +188,7 @@ impl StorageRead for Ctx { } } -impl TxEnv for Ctx { - type Error = Error; - - fn get_block_time(&self) -> Result { - let read_result = unsafe { anoma_tx_get_block_time() }; - let time_value = read_from_buffer(read_result, anoma_tx_result_buffer) - .expect("The block time should exist"); - Ok(Rfc3339String( - String::try_from_slice(&time_value[..]) - .expect("The conversion shouldn't fail"), - )) - } - +impl StorageWrite for Ctx { fn write( &mut self, key: &namada::types::storage::Key, @@ -227,6 +215,29 @@ impl TxEnv for Ctx { Ok(()) } + fn delete( + &mut self, + key: &namada::types::storage::Key, + ) -> storage_api::Result<()> { + let key = key.to_string(); + unsafe { anoma_tx_delete(key.as_ptr() as _, key.len() as _) }; + Ok(()) + } +} + +impl TxEnv for Ctx { + type Error = Error; + + fn get_block_time(&self) -> Result { + let read_result = unsafe { anoma_tx_get_block_time() }; + let time_value = read_from_buffer(read_result, anoma_tx_result_buffer) + .expect("The block time should exist"); + Ok(Rfc3339String( + String::try_from_slice(&time_value[..]) + .expect("The conversion shouldn't fail"), + )) + } + fn write_temp( &mut self, key: &namada::types::storage::Key, @@ -253,15 +264,6 @@ impl TxEnv for Ctx { Ok(()) } - fn delete( - &mut self, - key: &namada::types::storage::Key, - ) -> storage_api::Result<()> { - let key = key.to_string(); - unsafe { anoma_tx_delete(key.as_ptr() as _, key.len() as _) }; - Ok(()) - } - fn insert_verifier(&mut self, addr: &Address) -> Result<(), Error> { let addr = addr.encode(); unsafe { anoma_tx_insert_verifier(addr.as_ptr() as _, addr.len() as _) } diff --git a/wasm/wasm_source/src/vp_nft.rs b/wasm/wasm_source/src/vp_nft.rs index 4ab7f232bd..956a0a5d42 100644 --- a/wasm/wasm_source/src/vp_nft.rs +++ b/wasm/wasm_source/src/vp_nft.rs @@ -43,7 +43,7 @@ mod tests { use namada_tests::log::test; use namada_tests::tx::{self, tx_host_env, TestTxEnv}; use namada_tests::vp::*; - use namada_tx_prelude::TxEnv; + use namada_tx_prelude::{StorageWrite, TxEnv}; use super::*; diff --git a/wasm/wasm_source/src/vp_testnet_faucet.rs b/wasm/wasm_source/src/vp_testnet_faucet.rs index a3a5630ab7..b599f82251 100644 --- a/wasm/wasm_source/src/vp_testnet_faucet.rs +++ b/wasm/wasm_source/src/vp_testnet_faucet.rs @@ -97,7 +97,7 @@ mod tests { use namada_tests::tx::{self, tx_host_env, TestTxEnv}; use namada_tests::vp::vp_host_env::storage::Key; use namada_tests::vp::*; - use namada_tx_prelude::TxEnv; + use namada_tx_prelude::{StorageWrite, TxEnv}; use namada_vp_prelude::key::RefTo; use proptest::prelude::*; use storage::testing::arb_account_storage_key_no_vp; diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 3b6ddcf65f..d20472d9ec 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -380,7 +380,7 @@ mod tests { use namada_tests::tx::{self, tx_host_env, TestTxEnv}; use namada_tests::vp::vp_host_env::storage::Key; use namada_tests::vp::*; - use namada_tx_prelude::TxEnv; + use namada_tx_prelude::{StorageWrite, TxEnv}; use namada_vp_prelude::key::RefTo; use proptest::prelude::*; use storage::testing::arb_account_storage_key_no_vp; From 08b0a8f1d8aa22813dbda2782aa00be651c8e682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 12 Aug 2022 17:32:34 +0200 Subject: [PATCH 23/81] ledger: impl StorageWrite for Storage --- shared/src/ledger/storage/mod.rs | 40 +++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/shared/src/ledger/storage/mod.rs b/shared/src/ledger/storage/mod.rs index 1f106bcf69..28b160ddb4 100644 --- a/shared/src/ledger/storage/mod.rs +++ b/shared/src/ledger/storage/mod.rs @@ -12,7 +12,7 @@ use tendermint::merkle::proof::Proof; use thiserror::Error; use super::parameters::Parameters; -use super::storage_api::{ResultExt, StorageRead}; +use super::storage_api::{ResultExt, StorageRead, StorageWrite}; use super::{parameters, storage_api}; use crate::ledger::gas::MIN_STORAGE_GAS; use crate::ledger::parameters::EpochDuration; @@ -758,6 +758,44 @@ where } } +impl StorageWrite for Storage +where + D: DB + for<'iter> DBIter<'iter>, + H: StorageHasher, +{ + fn write( + &mut self, + key: &crate::types::storage::Key, + val: T, + ) -> storage_api::Result<()> { + let val = val.try_to_vec().unwrap(); + self.write_bytes(key, val) + } + + fn write_bytes( + &mut self, + key: &crate::types::storage::Key, + val: impl AsRef<[u8]>, + ) -> storage_api::Result<()> { + let _ = self + .db + .write_subspace_val(self.block.height, key, val) + .into_storage_result()?; + Ok(()) + } + + fn delete( + &mut self, + key: &crate::types::storage::Key, + ) -> storage_api::Result<()> { + let _ = self + .db + .delete_subspace_val(self.block.height, key) + .into_storage_result()?; + Ok(()) + } +} + impl From for Error { fn from(error: MerkleTreeError) -> Self { Self::MerkleTreeError(error) From 773033dafcfdec5b159ad7581278d952d8f08b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 16 Aug 2022 16:41:27 +0200 Subject: [PATCH 24/81] add <'iter> lifetime to StorageRead trait to get around lack of GATs --- shared/src/ledger/native_vp.rs | 5 +++-- shared/src/ledger/storage/mod.rs | 7 ++----- shared/src/ledger/storage_api/mod.rs | 16 ++++++++++++++-- shared/src/ledger/tx_env.rs | 2 +- tx_prelude/src/lib.rs | 4 ++-- vp_prelude/src/lib.rs | 4 ++-- 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/shared/src/ledger/native_vp.rs b/shared/src/ledger/native_vp.rs index fa30efb7ea..5cbab7d639 100644 --- a/shared/src/ledger/native_vp.rs +++ b/shared/src/ledger/native_vp.rs @@ -150,7 +150,7 @@ where } } -impl<'f, 'a, DB, H, CA> StorageRead for CtxPreStorageRead<'f, 'a, DB, H, CA> +impl<'f, 'a, DB, H, CA> StorageRead<'f> for CtxPreStorageRead<'f, 'a, DB, H, CA> where DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, H: 'static + StorageHasher, @@ -210,7 +210,8 @@ where } } -impl<'f, 'a, DB, H, CA> StorageRead for CtxPostStorageRead<'f, 'a, DB, H, CA> +impl<'f, 'a, DB, H, CA> StorageRead<'f> + for CtxPostStorageRead<'f, 'a, DB, H, CA> where DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, H: 'static + StorageHasher, diff --git a/shared/src/ledger/storage/mod.rs b/shared/src/ledger/storage/mod.rs index 28b160ddb4..1fb84c9a94 100644 --- a/shared/src/ledger/storage/mod.rs +++ b/shared/src/ledger/storage/mod.rs @@ -685,10 +685,7 @@ where } } -// The `'iter` lifetime is needed for the associated type `PrefixIter`. -// Note that the `D: DBIter<'iter>` bound uses another higher-rank lifetime -// (see https://doc.rust-lang.org/nomicon/hrtb.html). -impl<'iter, D, H> StorageRead for &'iter Storage +impl<'iter, D, H> StorageRead<'iter> for Storage where D: DB + for<'iter_> DBIter<'iter_>, H: StorageHasher, @@ -721,7 +718,7 @@ where } fn iter_prefix( - &self, + &'iter self, prefix: &crate::types::storage::Key, ) -> std::result::Result { Ok(self.db.iter_prefix(prefix)) diff --git a/shared/src/ledger/storage_api/mod.rs b/shared/src/ledger/storage_api/mod.rs index 36e89a8171..235108c52f 100644 --- a/shared/src/ledger/storage_api/mod.rs +++ b/shared/src/ledger/storage_api/mod.rs @@ -8,8 +8,14 @@ pub use error::{CustomError, Error, Result, ResultExt}; use crate::types::storage::{self, BlockHash, BlockHeight, Epoch}; +// TODO: once GATs are stabilized, we should be able to remove the `'iter` +// lifetime param that is currently the only way to make the prefix iterator +// typecheck in the `>::PrefixIter` associated type used in +// `impl StorageRead for Storage` (shared/src/ledger/storage/mod.rs). +// +// See /// Common storage read interface -pub trait StorageRead { +pub trait StorageRead<'iter> { /// Storage read prefix iterator type PrefixIter; @@ -28,7 +34,13 @@ pub trait StorageRead { /// Storage prefix iterator. It will try to get an iterator from the /// storage. - fn iter_prefix(&self, prefix: &storage::Key) -> Result; + /// + /// For a more user-friendly iterator API, use [`fn@iter_prefix`] or + /// [`fn@iter_prefix_bytes`] instead. + fn iter_prefix( + &'iter self, + prefix: &storage::Key, + ) -> Result; /// Storage prefix iterator for. It will try to read from the storage. fn iter_next( diff --git a/shared/src/ledger/tx_env.rs b/shared/src/ledger/tx_env.rs index 421cc84687..1db8fad09b 100644 --- a/shared/src/ledger/tx_env.rs +++ b/shared/src/ledger/tx_env.rs @@ -10,7 +10,7 @@ use crate::types::storage; use crate::types::time::Rfc3339String; /// Transaction host functions -pub trait TxEnv: StorageRead + StorageWrite { +pub trait TxEnv<'iter>: StorageRead<'iter> + StorageWrite { /// Host env functions possible errors type Error; diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index dd9b973921..02341e70e5 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -95,7 +95,7 @@ pub type TxResult = EnvResult<()>; #[derive(Debug)] pub struct KeyValIterator(pub u64, pub PhantomData); -impl StorageRead for Ctx { +impl StorageRead<'_> for Ctx { type PrefixIter = KeyValIterator<(String, Vec)>; fn read( @@ -225,7 +225,7 @@ impl StorageWrite for Ctx { } } -impl TxEnv for Ctx { +impl TxEnv<'_> for Ctx { type Error = Error; fn get_block_time(&self) -> Result { diff --git a/vp_prelude/src/lib.rs b/vp_prelude/src/lib.rs index 37d1958aa9..24d702b6b5 100644 --- a/vp_prelude/src/lib.rs +++ b/vp_prelude/src/lib.rs @@ -295,7 +295,7 @@ impl VpEnv for Ctx { } } -impl StorageRead for CtxPreStorageRead<'_> { +impl StorageRead<'_> for CtxPreStorageRead<'_> { type PrefixIter = KeyValIterator<(String, Vec)>; fn read( @@ -372,7 +372,7 @@ impl StorageRead for CtxPreStorageRead<'_> { } } -impl StorageRead for CtxPostStorageRead<'_> { +impl StorageRead<'_> for CtxPostStorageRead<'_> { type PrefixIter = KeyValIterator<(String, Vec)>; fn read( From 28b4307e92a4ac01fdab65aa422dc5ba2f6e90f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 16 Aug 2022 17:01:14 +0200 Subject: [PATCH 25/81] add more comments for StorageRead lifetime with an example usage --- shared/src/ledger/storage_api/mod.rs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/shared/src/ledger/storage_api/mod.rs b/shared/src/ledger/storage_api/mod.rs index 235108c52f..0e7e299970 100644 --- a/shared/src/ledger/storage_api/mod.rs +++ b/shared/src/ledger/storage_api/mod.rs @@ -8,13 +8,26 @@ pub use error::{CustomError, Error, Result, ResultExt}; use crate::types::storage::{self, BlockHash, BlockHeight, Epoch}; -// TODO: once GATs are stabilized, we should be able to remove the `'iter` -// lifetime param that is currently the only way to make the prefix iterator -// typecheck in the `>::PrefixIter` associated type used in -// `impl StorageRead for Storage` (shared/src/ledger/storage/mod.rs). -// -// See /// Common storage read interface +/// +/// If you're using this trait and having compiler complaining about needing an +/// explicit lifetime parameter, simply use trait bounds with the following +/// syntax: +/// +/// ```rust,ignore +/// where +/// S: for<'iter> StorageRead<'iter> +/// ``` +/// +/// If you want to know why this is needed, see the to-do task below. The +/// syntax for this relies on higher-rank lifetimes, see e.g. +/// . +/// +/// TODO: once GATs are stabilized, we should be able to remove the `'iter` +/// lifetime param that is currently the only way to make the prefix iterator +/// typecheck in the `>::PrefixIter` associated type used in +/// `impl StorageRead for Storage` (shared/src/ledger/storage/mod.rs). +/// See pub trait StorageRead<'iter> { /// Storage read prefix iterator type PrefixIter; From 8bc5816483f6eec4980e757815d3a656d73215e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 16 Aug 2022 17:48:14 +0200 Subject: [PATCH 26/81] storage: remove unnecessary clone --- shared/src/ledger/storage/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shared/src/ledger/storage/mod.rs b/shared/src/ledger/storage/mod.rs index 1fb84c9a94..bb97ab8fae 100644 --- a/shared/src/ledger/storage/mod.rs +++ b/shared/src/ledger/storage/mod.rs @@ -424,12 +424,13 @@ where pub fn write( &mut self, key: &Key, - value: impl AsRef<[u8]> + Clone, + value: impl AsRef<[u8]>, ) -> Result<(u64, i64)> { tracing::debug!("storage write key {}", key,); - self.block.tree.update(key, value.clone())?; + let value = value.as_ref(); + self.block.tree.update(key, &value)?; - let len = value.as_ref().len(); + let len = value.len(); let gas = key.len() + len; let size_diff = self.db.write_subspace_val(self.last_height, key, value)?; From 7decfab26b9c3d2b165f574e80cb5e2f27146307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 16 Aug 2022 17:48:26 +0200 Subject: [PATCH 27/81] fix missing StorageWrite for Storage merkle tree update --- shared/src/ledger/storage/mod.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/shared/src/ledger/storage/mod.rs b/shared/src/ledger/storage/mod.rs index bb97ab8fae..d8c8e28091 100644 --- a/shared/src/ledger/storage/mod.rs +++ b/shared/src/ledger/storage/mod.rs @@ -426,6 +426,8 @@ where key: &Key, value: impl AsRef<[u8]>, ) -> Result<(u64, i64)> { + // Note that this method is the same as `StorageWrite::write_bytes`, + // but with gas and storage bytes len diff accounting tracing::debug!("storage write key {}", key,); let value = value.as_ref(); self.block.tree.update(key, &value)?; @@ -440,6 +442,8 @@ where /// Delete the specified subspace and returns the gas cost and the size /// difference pub fn delete(&mut self, key: &Key) -> Result<(u64, i64)> { + // Note that this method is the same as `StorageWrite::delete`, + // but with gas and storage bytes len diff accounting let mut deleted_bytes_len = 0; if self.has_key(key)?.0 { self.block.tree.delete(key)?; @@ -766,7 +770,7 @@ where key: &crate::types::storage::Key, val: T, ) -> storage_api::Result<()> { - let val = val.try_to_vec().unwrap(); + let val = val.try_to_vec().into_storage_result()?; self.write_bytes(key, val) } @@ -775,6 +779,11 @@ where key: &crate::types::storage::Key, val: impl AsRef<[u8]>, ) -> storage_api::Result<()> { + // Note that this method is the same as `Storage::write`, but without + // gas and storage bytes len diff accounting, because it can only be + // used by the protocol that has a direct mutable access to storage + let val = val.as_ref(); + self.block.tree.update(key, &val).into_storage_result()?; let _ = self .db .write_subspace_val(self.block.height, key, val) @@ -786,6 +795,10 @@ where &mut self, key: &crate::types::storage::Key, ) -> storage_api::Result<()> { + // Note that this method is the same as `Storage::delete`, but without + // gas and storage bytes len diff accounting, because it can only be + // used by the protocol that has a direct mutable access to storage + self.block.tree.delete(key).into_storage_result()?; let _ = self .db .delete_subspace_val(self.block.height, key) From b92edd43b720b404584e306d081621c7314a0886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sat, 20 Aug 2022 19:11:40 +0200 Subject: [PATCH 28/81] update wasm checksums --- wasm/checksums.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index 4654a502c5..5c658485f0 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,17 +1,17 @@ { - "tx_bond.wasm": "tx_bond.683e3d5d9ad06b24df792e1a6a5f113fa238bc5635949a8da8f6826a0e338367.wasm", - "tx_from_intent.wasm": "tx_from_intent.51ab8df28b2e09232469043903ed490a42fa04afaa7827b42d5d895906a45ed8.wasm", - "tx_ibc.wasm": "tx_ibc.d79a0434a22727d0c3b926e2fb02969fe02efa58bff02b94b6f64ea6b6c6f0b2.wasm", - "tx_init_account.wasm": "tx_init_account.9a35cbe6781c2df33beb894ac2b1d323d575f859f119a43f34857eb34d7c22d2.wasm", - "tx_init_nft.wasm": "tx_init_nft.8079d16641f9f2af458f3c9818ffa2250994fdd20ec5fcdba87feab23c3576c6.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.3f5057d1a874eaa92f8ba8a8f8afc83a20139edb4bf2056300ce0c8e463950ad.wasm", - "tx_init_validator.wasm": "tx_init_validator.32936b7d7d0cdd961f11a1759f96dc09a0329528f3b2cc03ecf60751fa31f798.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.a41830bed7d64926f8c4e2fe8ea56a7b485a26402fa3b0556d6ba14bc105e808.wasm", - "tx_transfer.wasm": "tx_transfer.11b66a337f337c836e4a7dd3356cc16ecede1c7c14a3dd56ed08c96557c03025.wasm", - "tx_unbond.wasm": "tx_unbond.db1f677a158a5ac1bda63d653a8f22f59ed72e0e425d55918ef70e87a4aa95e0.wasm", + "tx_bond.wasm": "tx_bond.98a51e759f7b41873faee295fa69ee919d64ee95e4aa5cd923daff59325fda94.wasm", + "tx_from_intent.wasm": "tx_from_intent.c598df3ee8cc5980f83df56b1acd8132c85421d360a4f54c1235c8576f0d3390.wasm", + "tx_ibc.wasm": "tx_ibc.c838d61c5ad5b5f24855f19a85fda1bad159701b90a4672e98db6d8fee966880.wasm", + "tx_init_account.wasm": "tx_init_account.02a6a717930a35962e5d2fddd76bb78194796379d7c584645ce8c30b1e672ae6.wasm", + "tx_init_nft.wasm": "tx_init_nft.09974068959257551e82281839f4506fcc377713d174984c25af37455e08ac84.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.a9a45a5a4ec4fffe2e28d8bec8c013dc571e62d01b730eea2d09fc12a3c700dd.wasm", + "tx_init_validator.wasm": "tx_init_validator.03d30dad39d35d4285ff012af80ad3e94f8d10d802410aac4a7fd1c86e8d3f6c.wasm", + "tx_mint_nft.wasm": "tx_mint_nft.32747cf99afd40b14114812bea65198d454eb9d7c86d6300a8ed9306df9aa9be.wasm", + "tx_transfer.wasm": "tx_transfer.05c24ab9ad8bdd09bc910d9bbfc8686fc5c436594b635cc2105f0ddde5c8ab63.wasm", + "tx_unbond.wasm": "tx_unbond.a3ffc49d7b481e1d43c7f67cbccf9ce31476f98a9822b6b57ddf51c33f427605.wasm", "tx_update_vp.wasm": "tx_update_vp.a304b3c70361cde5fda458ba5637d6825eb002198e73990a1c74351113b83d43.wasm", "tx_vote_proposal.wasm": "tx_vote_proposal.5e51a66746df8c7e786cc6837a74f87d239442ace67d1c4ef4e6751aa725514f.wasm", - "tx_withdraw.wasm": "tx_withdraw.b4705cbc2eb566faf1d14fff10190970f62aa90c30420e8b3ebbec0a85e7d04b.wasm", + "tx_withdraw.wasm": "tx_withdraw.efab1590d68edee54058ddf03313697be4aaa8adf8b012f98338be514e9f6e87.wasm", "vp_nft.wasm": "vp_nft.1a32d37b32c616119d156128560a0eeb43206e91069e2c3875e74211ed3ad01f.wasm", "vp_testnet_faucet.wasm": "vp_testnet_faucet.4c91dfdf6ac6cdccc931ee391167033a9cdbd1b8551a428e90295bd0d6f68d85.wasm", "vp_token.wasm": "vp_token.9f27c2e823691487bb6631fbfecb39837c1f404ef9b263e810a5a2a76f4c5957.wasm", From ace6716fba7be13c4424e919ca0aae631f0b5fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sun, 21 Aug 2022 14:26:44 +0200 Subject: [PATCH 29/81] changelog: add #331 --- .../unreleased/improvements/331-common-write-storage-trait.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/improvements/331-common-write-storage-trait.md diff --git a/.changelog/unreleased/improvements/331-common-write-storage-trait.md b/.changelog/unreleased/improvements/331-common-write-storage-trait.md new file mode 100644 index 0000000000..2e3e605197 --- /dev/null +++ b/.changelog/unreleased/improvements/331-common-write-storage-trait.md @@ -0,0 +1,2 @@ +- Added a StorageWrite trait for a common interface for transactions and direct + storage access for protocol ([#331](https://github.com/anoma/namada/pull/331)) \ No newline at end of file From 999df37b8675742ee703556871e5850e64da19e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 15 Aug 2022 15:54:35 +0200 Subject: [PATCH 30/81] storage_api: add default borsh encoded read/write impl and handle errors --- shared/src/ledger/native_vp.rs | 14 -------------- shared/src/ledger/storage/mod.rs | 20 -------------------- shared/src/ledger/storage_api/mod.rs | 16 ++++++++++++++-- tx_prelude/src/lib.rs | 20 -------------------- vp_prelude/src/lib.rs | 28 ---------------------------- 5 files changed, 14 insertions(+), 84 deletions(-) diff --git a/shared/src/ledger/native_vp.rs b/shared/src/ledger/native_vp.rs index 5cbab7d639..41fbc1cda3 100644 --- a/shared/src/ledger/native_vp.rs +++ b/shared/src/ledger/native_vp.rs @@ -158,13 +158,6 @@ where { type PrefixIter = >::PrefixIter; - fn read( - &self, - key: &crate::types::storage::Key, - ) -> Result, storage_api::Error> { - self.ctx.read_pre(key).into_storage_result() - } - fn read_bytes( &self, key: &crate::types::storage::Key, @@ -219,13 +212,6 @@ where { type PrefixIter = >::PrefixIter; - fn read( - &self, - key: &crate::types::storage::Key, - ) -> Result, storage_api::Error> { - self.ctx.read_post(key).into_storage_result() - } - fn read_bytes( &self, key: &crate::types::storage::Key, diff --git a/shared/src/ledger/storage/mod.rs b/shared/src/ledger/storage/mod.rs index d8c8e28091..235e2164e3 100644 --- a/shared/src/ledger/storage/mod.rs +++ b/shared/src/ledger/storage/mod.rs @@ -697,17 +697,6 @@ where { type PrefixIter = >::PrefixIter; - fn read( - &self, - key: &crate::types::storage::Key, - ) -> std::result::Result, storage_api::Error> { - self.read_bytes(key) - .map(|maybe_value| { - maybe_value.and_then(|t| T::try_from_slice(&t[..]).ok()) - }) - .into_storage_result() - } - fn read_bytes( &self, key: &crate::types::storage::Key, @@ -765,15 +754,6 @@ where D: DB + for<'iter> DBIter<'iter>, H: StorageHasher, { - fn write( - &mut self, - key: &crate::types::storage::Key, - val: T, - ) -> storage_api::Result<()> { - let val = val.try_to_vec().into_storage_result()?; - self.write_bytes(key, val) - } - fn write_bytes( &mut self, key: &crate::types::storage::Key, diff --git a/shared/src/ledger/storage_api/mod.rs b/shared/src/ledger/storage_api/mod.rs index 0e7e299970..873c14ef9b 100644 --- a/shared/src/ledger/storage_api/mod.rs +++ b/shared/src/ledger/storage_api/mod.rs @@ -37,7 +37,16 @@ pub trait StorageRead<'iter> { fn read( &self, key: &storage::Key, - ) -> Result>; + ) -> Result> { + let bytes = self.read_bytes(key)?; + match bytes { + Some(bytes) => { + let val = T::try_from_slice(&bytes).into_storage_result()?; + Ok(Some(val)) + } + None => Ok(None), + } + } /// Storage read raw bytes. It will try to read from the storage. fn read_bytes(&self, key: &storage::Key) -> Result>>; @@ -84,7 +93,10 @@ pub trait StorageWrite { &mut self, key: &storage::Key, val: T, - ) -> Result<()>; + ) -> Result<()> { + let bytes = val.try_to_vec().into_storage_result()?; + self.write_bytes(key, bytes) + } /// Write a value as bytes at the given key to storage. fn write_bytes( diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index 02341e70e5..ade290bf65 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -98,17 +98,6 @@ pub struct KeyValIterator(pub u64, pub PhantomData); impl StorageRead<'_> for Ctx { type PrefixIter = KeyValIterator<(String, Vec)>; - fn read( - &self, - key: &namada::types::storage::Key, - ) -> Result, storage_api::Error> { - let key = key.to_string(); - let read_result = - unsafe { anoma_tx_read(key.as_ptr() as _, key.len() as _) }; - Ok(read_from_buffer(read_result, anoma_tx_result_buffer) - .and_then(|t| T::try_from_slice(&t[..]).ok())) - } - fn read_bytes( &self, key: &namada::types::storage::Key, @@ -189,15 +178,6 @@ impl StorageRead<'_> for Ctx { } impl StorageWrite for Ctx { - fn write( - &mut self, - key: &namada::types::storage::Key, - val: T, - ) -> storage_api::Result<()> { - let buf = val.try_to_vec().unwrap(); - self.write_bytes(key, buf) - } - fn write_bytes( &mut self, key: &namada::types::storage::Key, diff --git a/vp_prelude/src/lib.rs b/vp_prelude/src/lib.rs index 24d702b6b5..056d9cb1d5 100644 --- a/vp_prelude/src/lib.rs +++ b/vp_prelude/src/lib.rs @@ -298,20 +298,6 @@ impl VpEnv for Ctx { impl StorageRead<'_> for CtxPreStorageRead<'_> { type PrefixIter = KeyValIterator<(String, Vec)>; - fn read( - &self, - key: &storage::Key, - ) -> Result, storage_api::Error> { - let bytes = self.read_bytes(key)?; - match bytes { - Some(bytes) => match T::try_from_slice(&bytes[..]) { - Ok(val) => Ok(Some(val)), - Err(err) => Err(storage_api::Error::new(err)), - }, - None => Ok(None), - } - } - fn read_bytes( &self, key: &storage::Key, @@ -375,20 +361,6 @@ impl StorageRead<'_> for CtxPreStorageRead<'_> { impl StorageRead<'_> for CtxPostStorageRead<'_> { type PrefixIter = KeyValIterator<(String, Vec)>; - fn read( - &self, - key: &storage::Key, - ) -> Result, storage_api::Error> { - let bytes = self.read_bytes(key)?; - match bytes { - Some(bytes) => match T::try_from_slice(&bytes[..]) { - Ok(val) => Ok(Some(val)), - Err(err) => Err(storage_api::Error::new(err)), - }, - None => Ok(None), - } - } - fn read_bytes( &self, key: &storage::Key, From a5473788002c9847734b08c42fdc0548fbde4fcb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 15 Aug 2022 16:00:41 +0000 Subject: [PATCH 31/81] wasm checksums update --- wasm/checksums.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index 5c658485f0..2478056cb0 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,19 +1,19 @@ { - "tx_bond.wasm": "tx_bond.98a51e759f7b41873faee295fa69ee919d64ee95e4aa5cd923daff59325fda94.wasm", - "tx_from_intent.wasm": "tx_from_intent.c598df3ee8cc5980f83df56b1acd8132c85421d360a4f54c1235c8576f0d3390.wasm", - "tx_ibc.wasm": "tx_ibc.c838d61c5ad5b5f24855f19a85fda1bad159701b90a4672e98db6d8fee966880.wasm", - "tx_init_account.wasm": "tx_init_account.02a6a717930a35962e5d2fddd76bb78194796379d7c584645ce8c30b1e672ae6.wasm", - "tx_init_nft.wasm": "tx_init_nft.09974068959257551e82281839f4506fcc377713d174984c25af37455e08ac84.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.a9a45a5a4ec4fffe2e28d8bec8c013dc571e62d01b730eea2d09fc12a3c700dd.wasm", - "tx_init_validator.wasm": "tx_init_validator.03d30dad39d35d4285ff012af80ad3e94f8d10d802410aac4a7fd1c86e8d3f6c.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.32747cf99afd40b14114812bea65198d454eb9d7c86d6300a8ed9306df9aa9be.wasm", - "tx_transfer.wasm": "tx_transfer.05c24ab9ad8bdd09bc910d9bbfc8686fc5c436594b635cc2105f0ddde5c8ab63.wasm", - "tx_unbond.wasm": "tx_unbond.a3ffc49d7b481e1d43c7f67cbccf9ce31476f98a9822b6b57ddf51c33f427605.wasm", + "tx_bond.wasm": "tx_bond.078d5be45d839fc1b6c034c4fdfe2055add709daa05868826682d3b6abf27ac4.wasm", + "tx_from_intent.wasm": "tx_from_intent.dcdfc49a02c76f277afac84e3511ad47c70b3bf54d1273a15c6dc75648316937.wasm", + "tx_ibc.wasm": "tx_ibc.3957053e0ea9caaa49128994711339aea6ede77a99f150688df3de870c8b17d4.wasm", + "tx_init_account.wasm": "tx_init_account.92e59887817a6d789dc8a85bb1eff3c86bd0a9bd1ddc8a9e0e5de5c9e9c2ddc6.wasm", + "tx_init_nft.wasm": "tx_init_nft.4125bdf149533cd201895cfaf6cdfbb9616182c187137029fe3c9214030f1877.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.8da848905d5a8ad1b15046d5e59e04f307bde73dcc8d2ab0b6d8d235a31a8b52.wasm", + "tx_init_validator.wasm": "tx_init_validator.364786e78253bd9ce72d089cc1539a882eb8ef6fd8c818616d003241b47ac010.wasm", + "tx_mint_nft.wasm": "tx_mint_nft.b2c34b21a2f0b533e3dcd4b2ee64e45ca00ccad8b3f22c410474c7a67c3302e8.wasm", + "tx_transfer.wasm": "tx_transfer.6fac9a68bcd1a50d9cec64605f8637dfa897ce3be242edc344858cf4075fc100.wasm", + "tx_unbond.wasm": "tx_unbond.eeaf8ff32984275288b0a9a36c9579dace6f3ecfaf59255af769acf57a00df4a.wasm", "tx_update_vp.wasm": "tx_update_vp.a304b3c70361cde5fda458ba5637d6825eb002198e73990a1c74351113b83d43.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.5e51a66746df8c7e786cc6837a74f87d239442ace67d1c4ef4e6751aa725514f.wasm", - "tx_withdraw.wasm": "tx_withdraw.efab1590d68edee54058ddf03313697be4aaa8adf8b012f98338be514e9f6e87.wasm", - "vp_nft.wasm": "vp_nft.1a32d37b32c616119d156128560a0eeb43206e91069e2c3875e74211ed3ad01f.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.4c91dfdf6ac6cdccc931ee391167033a9cdbd1b8551a428e90295bd0d6f68d85.wasm", - "vp_token.wasm": "vp_token.9f27c2e823691487bb6631fbfecb39837c1f404ef9b263e810a5a2a76f4c5957.wasm", - "vp_user.wasm": "vp_user.fb06cb724aa92d615b8da971b0ca1c89bf0caf8c1b9ea3536c252fd1c3b98881.wasm" + "tx_vote_proposal.wasm": "tx_vote_proposal.8f306e1d1bcc9ca7f47a39108554fc847d4ac6df7a8d87a6effdffeae6adc4c4.wasm", + "tx_withdraw.wasm": "tx_withdraw.c288861e842f0b1ac6fbb95b14b33c8819ac587bbd5b483f2cdd0ea184206c65.wasm", + "vp_nft.wasm": "vp_nft.557883861270a0a5e42802ce4bfbbc28f9f0dfa3b76520ffdd319b69a4f2f34c.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.1fb8c53e7c164d141661743f3cdfaf30d30e6e16abd2f8814c0d8829a2e36e8a.wasm", + "vp_token.wasm": "vp_token.b130ca28e8029f5ad7a33bf3d4fbb9a3b08a29484feba34b988e902c4ce9c966.wasm", + "vp_user.wasm": "vp_user.2c45ba3e0b442210d4dd953679c774cc15f8773c5c25bda7f2ad60eaaff2d0a9.wasm" } \ No newline at end of file From 6f0be8fa9ac507e5868765070b5b0a0f16a1a4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 22 Aug 2022 13:10:15 +0200 Subject: [PATCH 32/81] changelog: add #334 --- .../unreleased/improvements/334-refactor-storage-read-write.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/improvements/334-refactor-storage-read-write.md diff --git a/.changelog/unreleased/improvements/334-refactor-storage-read-write.md b/.changelog/unreleased/improvements/334-refactor-storage-read-write.md new file mode 100644 index 0000000000..0642596268 --- /dev/null +++ b/.changelog/unreleased/improvements/334-refactor-storage-read-write.md @@ -0,0 +1,2 @@ +- Re-use encoding/decoding storage write/read and handle any errors + ([#334](https://github.com/anoma/namada/pull/334)) \ No newline at end of file From d737acd9ecdeff80fc62712fa19c571ace76097e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 15 Aug 2022 17:51:04 +0200 Subject: [PATCH 33/81] storage_api: build a nicer `iter_prefix` function on top of StorageRead --- shared/src/ledger/storage_api/mod.rs | 66 ++++++++++++++++++++++++++++ tx_prelude/src/lib.rs | 4 +- vp_prelude/src/lib.rs | 10 +++-- 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/shared/src/ledger/storage_api/mod.rs b/shared/src/ledger/storage_api/mod.rs index 0e7e299970..ab2f73784f 100644 --- a/shared/src/ledger/storage_api/mod.rs +++ b/shared/src/ledger/storage_api/mod.rs @@ -96,3 +96,69 @@ pub trait StorageWrite { /// Delete a value at the given key from storage. fn delete(&mut self, key: &storage::Key) -> Result<()>; } + +/// Iterate items matching the given prefix. +pub fn iter_prefix_bytes<'a>( + storage: &'a impl StorageRead<'a>, + prefix: &crate::types::storage::Key, +) -> Result)>> + 'a> { + let iter = storage.iter_prefix(prefix)?; + let iter = itertools::unfold(iter, |iter| { + match storage.iter_next(iter) { + Ok(Some((key, val))) => { + let key = match storage::Key::parse(key).into_storage_result() { + Ok(key) => key, + Err(err) => { + // Propagate key encoding errors into Iterator's Item + return Some(Err(err)); + } + }; + Some(Ok((key, val))) + } + Ok(None) => None, + Err(err) => { + // Propagate `iter_next` errors into Iterator's Item + Some(Err(err)) + } + } + }); + Ok(iter) +} + +/// Iterate Borsh encoded items matching the given prefix. +pub fn iter_prefix<'a, T>( + storage: &'a impl StorageRead<'a>, + prefix: &crate::types::storage::Key, +) -> Result> + 'a> +where + T: BorshDeserialize, +{ + let iter = storage.iter_prefix(prefix)?; + let iter = itertools::unfold(iter, |iter| { + match storage.iter_next(iter) { + Ok(Some((key, val))) => { + let key = match storage::Key::parse(key).into_storage_result() { + Ok(key) => key, + Err(err) => { + // Propagate key encoding errors into Iterator's Item + return Some(Err(err)); + } + }; + let val = match T::try_from_slice(&val).into_storage_result() { + Ok(val) => val, + Err(err) => { + // Propagate val encoding errors into Iterator's Item + return Some(Err(err)); + } + }; + Some(Ok((key, val))) + } + Ok(None) => None, + Err(err) => { + // Propagate `iter_next` errors into Iterator's Item + Some(Err(err)) + } + } + }); + Ok(iter) +} diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index 02341e70e5..bd1833ec58 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -23,7 +23,9 @@ pub use namada::ledger::governance::storage as gov_storage; pub use namada::ledger::parameters::storage as parameters_storage; pub use namada::ledger::storage::types::encode; use namada::ledger::storage_api; -pub use namada::ledger::storage_api::{StorageRead, StorageWrite}; +pub use namada::ledger::storage_api::{ + iter_prefix, iter_prefix_bytes, StorageRead, StorageWrite, +}; pub use namada::ledger::treasury::storage as treasury_storage; pub use namada::ledger::tx_env::TxEnv; pub use namada::proto::{Signed, SignedTxData}; diff --git a/vp_prelude/src/lib.rs b/vp_prelude/src/lib.rs index 24d702b6b5..3ee761f78f 100644 --- a/vp_prelude/src/lib.rs +++ b/vp_prelude/src/lib.rs @@ -22,7 +22,9 @@ use std::marker::PhantomData; pub use borsh::{BorshDeserialize, BorshSerialize}; pub use error::*; pub use namada::ledger::governance::storage as gov_storage; -pub use namada::ledger::storage_api::{self, StorageRead}; +pub use namada::ledger::storage_api::{ + self, iter_prefix, iter_prefix_bytes, StorageRead, +}; pub use namada::ledger::vp_env::VpEnv; pub use namada::ledger::{parameters, pos as proof_of_stake}; pub use namada::proto::{Signed, SignedTxData}; @@ -334,7 +336,7 @@ impl StorageRead<'_> for CtxPreStorageRead<'_> { prefix: &storage::Key, ) -> Result { // Note that this is the same as `CtxPostStorageRead` - iter_prefix(prefix) + iter_prefix_impl(prefix) } fn iter_next( @@ -411,7 +413,7 @@ impl StorageRead<'_> for CtxPostStorageRead<'_> { prefix: &storage::Key, ) -> Result { // Note that this is the same as `CtxPreStorageRead` - iter_prefix(prefix) + iter_prefix_impl(prefix) } fn iter_next( @@ -442,7 +444,7 @@ impl StorageRead<'_> for CtxPostStorageRead<'_> { } } -fn iter_prefix( +fn iter_prefix_impl( prefix: &storage::Key, ) -> Result)>, storage_api::Error> { let prefix = prefix.to_string(); From 4558dfa42248a9282bb6d4b5a492ac22bb3aa040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 15 Aug 2022 17:59:08 +0200 Subject: [PATCH 34/81] test/vm_host_env: refactor prefix iter tests with new `iter_prefix` fn --- tests/src/vm_host_env/mod.rs | 51 ++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index 37d1afb790..98f95e7fae 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -148,9 +148,11 @@ mod tests { tx_host_env::init(); let empty_key = storage::Key::parse("empty").unwrap(); - let mut iter = tx::ctx().iter_prefix(&empty_key).unwrap(); + let mut iter = + namada_tx_prelude::iter_prefix_bytes(tx::ctx(), &empty_key) + .unwrap(); assert!( - tx::ctx().iter_next(&mut iter).unwrap().is_none(), + iter.next().is_none(), "Trying to iter a prefix that doesn't have any matching keys \ should yield an empty iterator." ); @@ -167,15 +169,12 @@ mod tests { }); // Then try to iterate over their prefix - let iter = tx::ctx().iter_prefix(&prefix).unwrap(); - let iter = itertools::unfold(iter, |iter| { - if let Ok(Some((key, value))) = tx::ctx().iter_next(iter) { - let decoded_value = i32::try_from_slice(&value[..]).unwrap(); - return Some((key, decoded_value)); - } - None + let iter = namada_tx_prelude::iter_prefix(tx::ctx(), &prefix) + .unwrap() + .map(|item| item.unwrap()); + let expected = (0..10).map(|i| { + (storage::Key::parse(format!("{}/{}", prefix, i)).unwrap(), i) }); - let expected = (0..10).map(|i| (format!("{}/{}", prefix, i), i)); itertools::assert_equal(iter.sorted(), expected.sorted()); } @@ -380,29 +379,25 @@ mod tests { tx::ctx().write(&new_key, 11_i32).unwrap(); }); - let iter_pre = vp::CTX.iter_prefix(&prefix).unwrap(); - let iter_pre = itertools::unfold(iter_pre, |iter| { - if let Ok(Some((key, value))) = vp::CTX.iter_pre_next(iter) { - if let Ok(decoded_value) = i32::try_from_slice(&value[..]) { - return Some((key, decoded_value)); - } - } - None + let ctx_pre = vp::CTX.pre(); + let iter_pre = namada_vp_prelude::iter_prefix(&ctx_pre, &prefix) + .unwrap() + .map(|item| item.unwrap()); + let expected_pre = (0..10).map(|i| { + (storage::Key::parse(format!("{}/{}", prefix, i)).unwrap(), i) }); - let expected_pre = (0..10).map(|i| (format!("{}/{}", prefix, i), i)); itertools::assert_equal(iter_pre.sorted(), expected_pre.sorted()); - let iter_post = vp::CTX.iter_prefix(&prefix).unwrap(); - let iter_post = itertools::unfold(iter_post, |iter| { - if let Ok(Some((key, value))) = vp::CTX.iter_post_next(iter) { - let decoded_value = i32::try_from_slice(&value[..]).unwrap(); - return Some((key, decoded_value)); - } - None - }); + let ctx_post = vp::CTX.post(); + let iter_post = namada_vp_prelude::iter_prefix(&ctx_post, &prefix) + .unwrap() + .map(|item| item.unwrap()); let expected_post = (0..10).map(|i| { let val = if i == 5 { 100 } else { i }; - (format!("{}/{}", prefix, i), val) + ( + storage::Key::parse(format!("{}/{}", prefix, i)).unwrap(), + val, + ) }); itertools::assert_equal(iter_post.sorted(), expected_post.sorted()); } From d32d7af363124d52d41cd7f4a0758fa0fb88d97e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 15 Aug 2022 16:34:57 +0000 Subject: [PATCH 35/81] wasm checksums update --- wasm/checksums.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index 5c658485f0..0c7b7cf504 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,19 +1,19 @@ { - "tx_bond.wasm": "tx_bond.98a51e759f7b41873faee295fa69ee919d64ee95e4aa5cd923daff59325fda94.wasm", - "tx_from_intent.wasm": "tx_from_intent.c598df3ee8cc5980f83df56b1acd8132c85421d360a4f54c1235c8576f0d3390.wasm", - "tx_ibc.wasm": "tx_ibc.c838d61c5ad5b5f24855f19a85fda1bad159701b90a4672e98db6d8fee966880.wasm", - "tx_init_account.wasm": "tx_init_account.02a6a717930a35962e5d2fddd76bb78194796379d7c584645ce8c30b1e672ae6.wasm", - "tx_init_nft.wasm": "tx_init_nft.09974068959257551e82281839f4506fcc377713d174984c25af37455e08ac84.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.a9a45a5a4ec4fffe2e28d8bec8c013dc571e62d01b730eea2d09fc12a3c700dd.wasm", - "tx_init_validator.wasm": "tx_init_validator.03d30dad39d35d4285ff012af80ad3e94f8d10d802410aac4a7fd1c86e8d3f6c.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.32747cf99afd40b14114812bea65198d454eb9d7c86d6300a8ed9306df9aa9be.wasm", - "tx_transfer.wasm": "tx_transfer.05c24ab9ad8bdd09bc910d9bbfc8686fc5c436594b635cc2105f0ddde5c8ab63.wasm", - "tx_unbond.wasm": "tx_unbond.a3ffc49d7b481e1d43c7f67cbccf9ce31476f98a9822b6b57ddf51c33f427605.wasm", + "tx_bond.wasm": "tx_bond.d80c5ac518c223eea92a51eeb033462e9ea4498530d12de5b6e6ca6b3fa59ea8.wasm", + "tx_from_intent.wasm": "tx_from_intent.49565974c6e2c3d64a05729bd57217b244d804fc9f4e5369d1f3515aeaa8397f.wasm", + "tx_ibc.wasm": "tx_ibc.0d9d639037a8dc54c53ecbedc8396ea103ee7c8f485790ddf7223f4e7e6a9779.wasm", + "tx_init_account.wasm": "tx_init_account.97bfee0b78c87abc217c58351f1d6242990a06c7379b27f948950f04f36b49a2.wasm", + "tx_init_nft.wasm": "tx_init_nft.6207eabda37cd356b6093d069f388bd84463b5e1b8860811a36a9b63da84951f.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.1073d69f69172276c61c104f7b4678019723df19ddb30aedf6293a00de973846.wasm", + "tx_init_validator.wasm": "tx_init_validator.40f0152c1bd59f46ec26123d98d0b49a0a458335b6012818cf8504b7708bf625.wasm", + "tx_mint_nft.wasm": "tx_mint_nft.f1c8039a6fb5e01e7441cfa2e3fb2f84a1cb60ed660745ddc172d0d01f1b0ab1.wasm", + "tx_transfer.wasm": "tx_transfer.191d28c340900e6dc297e66b054cd83b690ae0357a8e13b37962b265b2e17da8.wasm", + "tx_unbond.wasm": "tx_unbond.ea73369f68abef405c4f7a3a09c3da6aa68493108d48b1e4e243d26766f00283.wasm", "tx_update_vp.wasm": "tx_update_vp.a304b3c70361cde5fda458ba5637d6825eb002198e73990a1c74351113b83d43.wasm", "tx_vote_proposal.wasm": "tx_vote_proposal.5e51a66746df8c7e786cc6837a74f87d239442ace67d1c4ef4e6751aa725514f.wasm", - "tx_withdraw.wasm": "tx_withdraw.efab1590d68edee54058ddf03313697be4aaa8adf8b012f98338be514e9f6e87.wasm", + "tx_withdraw.wasm": "tx_withdraw.f776265133f972e6705797561b6bb37f9e21de07f3611b23cfdd6e39cb252c0f.wasm", "vp_nft.wasm": "vp_nft.1a32d37b32c616119d156128560a0eeb43206e91069e2c3875e74211ed3ad01f.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.4c91dfdf6ac6cdccc931ee391167033a9cdbd1b8551a428e90295bd0d6f68d85.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.c30baa6d2dab2c336a6b2485e3ccf41cd548b135ca5245b80fab15858c70365c.wasm", "vp_token.wasm": "vp_token.9f27c2e823691487bb6631fbfecb39837c1f404ef9b263e810a5a2a76f4c5957.wasm", - "vp_user.wasm": "vp_user.fb06cb724aa92d615b8da971b0ca1c89bf0caf8c1b9ea3536c252fd1c3b98881.wasm" + "vp_user.wasm": "vp_user.59b7a9262c951ff451d3aec0604ae3dd0fc4729f8d994c114be6320ce5d38712.wasm" } \ No newline at end of file From fec72205d4417afa358121674d2110ffb81c2a06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 22 Aug 2022 13:27:11 +0200 Subject: [PATCH 36/81] changelog: add #335 --- .../improvements/335-refactor-storage-prefix-iter.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/unreleased/improvements/335-refactor-storage-prefix-iter.md diff --git a/.changelog/unreleased/improvements/335-refactor-storage-prefix-iter.md b/.changelog/unreleased/improvements/335-refactor-storage-prefix-iter.md new file mode 100644 index 0000000000..d51f6c72f0 --- /dev/null +++ b/.changelog/unreleased/improvements/335-refactor-storage-prefix-iter.md @@ -0,0 +1,3 @@ +- Added a simpler prefix iterator API that returns `std::iter::Iterator` with + the storage keys parsed and a variant that also decodes stored values with + Borsh ([#335](https://github.com/anoma/namada/pull/335)) \ No newline at end of file From 2d177f8bd3548739818524255892589b9d424a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 25 Aug 2022 10:30:32 +0200 Subject: [PATCH 37/81] shared: Add pre/post to VpEnv and use them to provide default impls --- shared/src/ledger/native_vp.rs | 191 +++++++++++---------------- shared/src/ledger/storage_api/mod.rs | 12 +- shared/src/ledger/vp_env.rs | 142 ++++++++++++-------- vp_prelude/src/lib.rs | 122 ++++++----------- 4 files changed, 210 insertions(+), 257 deletions(-) diff --git a/shared/src/ledger/native_vp.rs b/shared/src/ledger/native_vp.rs index 41fbc1cda3..f17b339086 100644 --- a/shared/src/ledger/native_vp.rs +++ b/shared/src/ledger/native_vp.rs @@ -76,25 +76,25 @@ where /// Read access to the prior storage (state before tx execution) via /// [`trait@StorageRead`]. #[derive(Debug)] -pub struct CtxPreStorageRead<'b, 'a: 'b, DB, H, CA> +pub struct CtxPreStorageRead<'view, 'a: 'view, DB, H, CA> where DB: storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, CA: WasmCacheAccess, { - ctx: &'b Ctx<'a, DB, H, CA>, + ctx: &'view Ctx<'a, DB, H, CA>, } /// Read access to the posterior storage (state after tx execution) via /// [`trait@StorageRead`]. #[derive(Debug)] -pub struct CtxPostStorageRead<'f, 'a: 'f, DB, H, CA> +pub struct CtxPostStorageRead<'view, 'a: 'view, DB, H, CA> where DB: storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, CA: WasmCacheAccess, { - ctx: &'f Ctx<'a, DB, H, CA>, + ctx: &'view Ctx<'a, DB, H, CA>, } impl<'a, DB, H, CA> Ctx<'a, DB, H, CA> @@ -139,18 +139,21 @@ where /// Read access to the prior storage (state before tx execution) /// via [`trait@StorageRead`]. - pub fn pre<'b>(&'b self) -> CtxPreStorageRead<'b, 'a, DB, H, CA> { + pub fn pre<'view>(&'view self) -> CtxPreStorageRead<'view, 'a, DB, H, CA> { CtxPreStorageRead { ctx: self } } /// Read access to the posterior storage (state after tx execution) /// via [`trait@StorageRead`]. - pub fn post<'b>(&'b self) -> CtxPostStorageRead<'b, 'a, DB, H, CA> { + pub fn post<'view>( + &'view self, + ) -> CtxPostStorageRead<'view, 'a, DB, H, CA> { CtxPostStorageRead { ctx: self } } } -impl<'f, 'a, DB, H, CA> StorageRead<'f> for CtxPreStorageRead<'f, 'a, DB, H, CA> +impl<'view, 'a, DB, H, CA> StorageRead<'view> + for CtxPreStorageRead<'view, 'a, DB, H, CA> where DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, H: 'static + StorageHasher, @@ -162,16 +165,38 @@ where &self, key: &crate::types::storage::Key, ) -> Result>, storage_api::Error> { - self.ctx.read_bytes_pre(key).into_storage_result() + vp_env::read_pre( + &mut *self.ctx.gas_meter.borrow_mut(), + self.ctx.storage, + self.ctx.write_log, + key, + ) + .into_storage_result() } fn has_key( &self, key: &crate::types::storage::Key, ) -> Result { - self.ctx.has_key_pre(key).into_storage_result() + vp_env::has_key_pre( + &mut *self.ctx.gas_meter.borrow_mut(), + self.ctx.storage, + key, + ) + .into_storage_result() } + fn iter_next( + &self, + iter: &mut Self::PrefixIter, + ) -> Result)>, storage_api::Error> { + vp_env::iter_pre_next::(&mut *self.ctx.gas_meter.borrow_mut(), iter) + .into_storage_result() + } + + // ---- Methods below are implemented in `self.ctx`, because they are + // the same in `pre/post` ---- + fn iter_prefix( &self, prefix: &crate::types::storage::Key, @@ -179,13 +204,6 @@ where self.ctx.iter_prefix(prefix).into_storage_result() } - fn iter_next( - &self, - iter: &mut Self::PrefixIter, - ) -> Result)>, storage_api::Error> { - self.ctx.iter_pre_next(iter).into_storage_result() - } - fn get_chain_id(&self) -> Result { self.ctx.get_chain_id().into_storage_result() } @@ -203,8 +221,8 @@ where } } -impl<'f, 'a, DB, H, CA> StorageRead<'f> - for CtxPostStorageRead<'f, 'a, DB, H, CA> +impl<'view, 'a, DB, H, CA> StorageRead<'view> + for CtxPostStorageRead<'view, 'a, DB, H, CA> where DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, H: 'static + StorageHasher, @@ -216,16 +234,43 @@ where &self, key: &crate::types::storage::Key, ) -> Result>, storage_api::Error> { - self.ctx.read_bytes_post(key).into_storage_result() + vp_env::read_post( + &mut *self.ctx.gas_meter.borrow_mut(), + self.ctx.storage, + self.ctx.write_log, + key, + ) + .into_storage_result() } fn has_key( &self, key: &crate::types::storage::Key, ) -> Result { - self.ctx.has_key_post(key).into_storage_result() + vp_env::has_key_post( + &mut *self.ctx.gas_meter.borrow_mut(), + self.ctx.storage, + self.ctx.write_log, + key, + ) + .into_storage_result() } + fn iter_next( + &self, + iter: &mut Self::PrefixIter, + ) -> Result)>, storage_api::Error> { + vp_env::iter_post_next::( + &mut *self.ctx.gas_meter.borrow_mut(), + self.ctx.write_log, + iter, + ) + .into_storage_result() + } + + // ---- Methods below are implemented in `self.ctx`, because they are + // the same in `pre/post` ---- + fn iter_prefix( &self, prefix: &crate::types::storage::Key, @@ -233,13 +278,6 @@ where self.ctx.iter_prefix(prefix).into_storage_result() } - fn iter_next( - &self, - iter: &mut Self::PrefixIter, - ) -> Result)>, storage_api::Error> { - self.ctx.iter_post_next(iter).into_storage_result() - } - fn get_chain_id(&self) -> Result { self.ctx.get_chain_id().into_storage_result() } @@ -257,63 +295,23 @@ where } } -impl<'a, DB, H, CA> VpEnv for Ctx<'a, DB, H, CA> +impl<'view, 'a: 'view, DB, H, CA> VpEnv<'view> for Ctx<'a, DB, H, CA> where DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, H: 'static + StorageHasher, CA: 'static + WasmCacheAccess, { type Error = Error; + type Post = CtxPostStorageRead<'view, 'a, DB, H, CA>; + type Pre = CtxPreStorageRead<'view, 'a, DB, H, CA>; type PrefixIter = >::PrefixIter; - fn read_pre( - &self, - key: &Key, - ) -> Result, Self::Error> { - vp_env::read_pre( - &mut *self.gas_meter.borrow_mut(), - self.storage, - self.write_log, - key, - ) - .map(|data| data.and_then(|t| T::try_from_slice(&t[..]).ok())) - } - - fn read_bytes_pre( - &self, - key: &Key, - ) -> Result>, Self::Error> { - vp_env::read_pre( - &mut *self.gas_meter.borrow_mut(), - self.storage, - self.write_log, - key, - ) - } - - fn read_post( - &self, - key: &Key, - ) -> Result, Self::Error> { - vp_env::read_post( - &mut *self.gas_meter.borrow_mut(), - self.storage, - self.write_log, - key, - ) - .map(|data| data.and_then(|t| T::try_from_slice(&t[..]).ok())) + fn pre(&'view self) -> Self::Pre { + CtxPreStorageRead { ctx: self } } - fn read_bytes_post( - &self, - key: &Key, - ) -> Result>, Self::Error> { - vp_env::read_post( - &mut *self.gas_meter.borrow_mut(), - self.storage, - self.write_log, - key, - ) + fn post(&'view self) -> Self::Post { + CtxPostStorageRead { ctx: self } } fn read_temp( @@ -339,44 +337,27 @@ where ) } - fn has_key_pre(&self, key: &Key) -> Result { - vp_env::has_key_pre( - &mut *self.gas_meter.borrow_mut(), - self.storage, - key, - ) - } - - fn has_key_post(&self, key: &Key) -> Result { - vp_env::has_key_post( - &mut *self.gas_meter.borrow_mut(), - self.storage, - self.write_log, - key, - ) - } - - fn get_chain_id(&self) -> Result { + fn get_chain_id(&'view self) -> Result { vp_env::get_chain_id(&mut *self.gas_meter.borrow_mut(), self.storage) } - fn get_block_height(&self) -> Result { + fn get_block_height(&'view self) -> Result { vp_env::get_block_height( &mut *self.gas_meter.borrow_mut(), self.storage, ) } - fn get_block_hash(&self) -> Result { + fn get_block_hash(&'view self) -> Result { vp_env::get_block_hash(&mut *self.gas_meter.borrow_mut(), self.storage) } - fn get_block_epoch(&self) -> Result { + fn get_block_epoch(&'view self) -> Result { vp_env::get_block_epoch(&mut *self.gas_meter.borrow_mut(), self.storage) } fn iter_prefix( - &self, + &'view self, prefix: &Key, ) -> Result { vp_env::iter_prefix( @@ -386,24 +367,6 @@ where ) } - fn iter_pre_next( - &self, - iter: &mut Self::PrefixIter, - ) -> Result)>, Self::Error> { - vp_env::iter_pre_next::(&mut *self.gas_meter.borrow_mut(), iter) - } - - fn iter_post_next( - &self, - iter: &mut Self::PrefixIter, - ) -> Result)>, Self::Error> { - vp_env::iter_post_next::( - &mut *self.gas_meter.borrow_mut(), - self.write_log, - iter, - ) - } - fn eval( &self, vp_code: Vec, diff --git a/shared/src/ledger/storage_api/mod.rs b/shared/src/ledger/storage_api/mod.rs index 873c14ef9b..94be4d8568 100644 --- a/shared/src/ledger/storage_api/mod.rs +++ b/shared/src/ledger/storage_api/mod.rs @@ -54,6 +54,12 @@ pub trait StorageRead<'iter> { /// Storage `has_key` in. It will try to read from the storage. fn has_key(&self, key: &storage::Key) -> Result; + /// Storage prefix iterator for. It will try to read from the storage. + fn iter_next( + &self, + iter: &mut Self::PrefixIter, + ) -> Result)>>; + /// Storage prefix iterator. It will try to get an iterator from the /// storage. /// @@ -64,12 +70,6 @@ pub trait StorageRead<'iter> { prefix: &storage::Key, ) -> Result; - /// Storage prefix iterator for. It will try to read from the storage. - fn iter_next( - &self, - iter: &mut Self::PrefixIter, - ) -> Result)>>; - /// Getting the chain ID. fn get_chain_id(&self) -> Result; diff --git a/shared/src/ledger/vp_env.rs b/shared/src/ledger/vp_env.rs index 95e7c48782..e1f72d8505 100644 --- a/shared/src/ledger/vp_env.rs +++ b/shared/src/ledger/vp_env.rs @@ -7,7 +7,7 @@ use borsh::BorshDeserialize; use thiserror::Error; use super::gas::MIN_STORAGE_GAS; -use super::storage_api; +use super::storage_api::{self, StorageRead}; use crate::ledger::gas; use crate::ledger::gas::VpGasMeter; use crate::ledger::storage::write_log::WriteLog; @@ -18,40 +18,24 @@ use crate::types::key::common; use crate::types::storage::{BlockHash, BlockHeight, Epoch, Key}; /// Validity predicate's environment is available for native VPs and WASM VPs -pub trait VpEnv { +pub trait VpEnv<'view> { /// Storage read prefix iterator type PrefixIter; /// Host functions possible errors, extensible with custom user errors. - type Error; + type Error: From; - /// Storage read prior state Borsh encoded value (before tx execution). It - /// will try to read from the storage and decode it if found. - fn read_pre( - &self, - key: &Key, - ) -> Result, Self::Error>; + /// Type to read storage state before the transaction execution + type Pre: StorageRead<'view, PrefixIter = Self::PrefixIter>; - /// Storage read prior state raw bytes (before tx execution). It - /// will try to read from the storage. - fn read_bytes_pre(&self, key: &Key) - -> Result>, Self::Error>; + /// Type to read storage state after the transaction execution + type Post: StorageRead<'view, PrefixIter = Self::PrefixIter>; - /// Storage read posterior state Borsh encoded value (after tx execution). - /// It will try to read from the write log first and if no entry found - /// then from the storage and then decode it if found. - fn read_post( - &self, - key: &Key, - ) -> Result, Self::Error>; + /// Read storage state before the transaction execution + fn pre(&'view self) -> Self::Pre; - /// Storage read posterior state raw bytes (after tx execution). It will try - /// to read from the write log first and if no entry found then from the - /// storage. - fn read_bytes_post( - &self, - key: &Key, - ) -> Result>, Self::Error>; + /// Read storage state after the transaction execution + fn post(&'view self) -> Self::Post; /// Storage read temporary state Borsh encoded value (after tx execution). /// It will try to read from only the write log and then decode it if @@ -68,51 +52,28 @@ pub trait VpEnv { key: &Key, ) -> Result>, Self::Error>; - /// Storage `has_key` in prior state (before tx execution). It will try to - /// read from the storage. - fn has_key_pre(&self, key: &Key) -> Result; - - /// Storage `has_key` in posterior state (after tx execution). It will try - /// to check the write log first and if no entry found then the storage. - fn has_key_post(&self, key: &Key) -> Result; - /// Getting the chain ID. - fn get_chain_id(&self) -> Result; + fn get_chain_id(&'view self) -> Result; /// Getting the block height. The height is that of the block to which the /// current transaction is being applied. - fn get_block_height(&self) -> Result; + fn get_block_height(&'view self) -> Result; /// Getting the block hash. The height is that of the block to which the /// current transaction is being applied. - fn get_block_hash(&self) -> Result; + fn get_block_hash(&'view self) -> Result; /// Getting the block epoch. The epoch is that of the block to which the /// current transaction is being applied. - fn get_block_epoch(&self) -> Result; + fn get_block_epoch(&'view self) -> Result; /// Storage prefix iterator. It will try to get an iterator from the /// storage. fn iter_prefix( - &self, + &'view self, prefix: &Key, ) -> Result; - /// Storage prefix iterator for prior state (before tx execution). It will - /// try to read from the storage. - fn iter_pre_next( - &self, - iter: &mut Self::PrefixIter, - ) -> Result)>, Self::Error>; - - /// Storage prefix iterator next for posterior state (after tx execution). - /// It will try to read from the write log first and if no entry found - /// then from the storage. - fn iter_post_next( - &self, - iter: &mut Self::PrefixIter, - ) -> Result)>, Self::Error>; - /// Evaluate a validity predicate with given data. The address, changed /// storage keys and verifiers will have the same values as the input to /// caller's validity predicate. @@ -136,6 +97,77 @@ pub trait VpEnv { /// Get a tx hash fn get_tx_code_hash(&self) -> Result; + + // ---- Methods below have default implementation via `pre/post` ---- + + /// Storage read prior state Borsh encoded value (before tx execution). It + /// will try to read from the storage and decode it if found. + fn read_pre( + &'view self, + key: &Key, + ) -> Result, Self::Error> { + self.pre().read(key).map_err(Into::into) + } + + /// Storage read prior state raw bytes (before tx execution). It + /// will try to read from the storage. + fn read_bytes_pre( + &'view self, + key: &Key, + ) -> Result>, Self::Error> { + self.pre().read_bytes(key).map_err(Into::into) + } + + /// Storage read posterior state Borsh encoded value (after tx execution). + /// It will try to read from the write log first and if no entry found + /// then from the storage and then decode it if found. + fn read_post( + &'view self, + key: &Key, + ) -> Result, Self::Error> { + self.post().read(key).map_err(Into::into) + } + + /// Storage read posterior state raw bytes (after tx execution). It will try + /// to read from the write log first and if no entry found then from the + /// storage. + fn read_bytes_post( + &'view self, + key: &Key, + ) -> Result>, Self::Error> { + self.post().read_bytes(key).map_err(Into::into) + } + + /// Storage `has_key` in prior state (before tx execution). It will try to + /// read from the storage. + fn has_key_pre(&'view self, key: &Key) -> Result { + self.pre().has_key(key).map_err(Into::into) + } + + /// Storage `has_key` in posterior state (after tx execution). It will try + /// to check the write log first and if no entry found then the storage. + fn has_key_post(&'view self, key: &Key) -> Result { + self.post().has_key(key).map_err(Into::into) + } + + /// Storage prefix iterator for prior state (before tx execution). It will + /// try to read from the storage. + fn iter_pre_next( + &'view self, + iter: &mut Self::PrefixIter, + ) -> Result)>, Self::Error> { + self.pre().iter_next(iter).map_err(Into::into) + } + + /// Storage prefix iterator next for posterior state (after tx execution). + /// It will try to read from the write log first and if no entry found + /// then from the storage. + fn iter_post_next( + &'view self, + iter: &mut Self::PrefixIter, + ) -> Result)>, Self::Error> { + self.post().iter_next(iter).map_err(Into::into) + } } /// These runtime errors will abort VP execution immediately diff --git a/vp_prelude/src/lib.rs b/vp_prelude/src/lib.rs index 056d9cb1d5..d1d3845a48 100644 --- a/vp_prelude/src/lib.rs +++ b/vp_prelude/src/lib.rs @@ -150,36 +150,18 @@ pub fn reject() -> VpResult { #[derive(Debug)] pub struct KeyValIterator(pub u64, pub PhantomData); -impl VpEnv for Ctx { +impl<'view> VpEnv<'view> for Ctx { type Error = Error; + type Post = CtxPostStorageRead<'view>; + type Pre = CtxPreStorageRead<'view>; type PrefixIter = KeyValIterator<(String, Vec)>; - fn read_pre( - &self, - key: &storage::Key, - ) -> Result, Self::Error> { - self.pre().read(key).into_env_result() - } - - fn read_bytes_pre( - &self, - key: &storage::Key, - ) -> Result>, Self::Error> { - self.pre().read_bytes(key).into_env_result() - } - - fn read_post( - &self, - key: &storage::Key, - ) -> Result, Self::Error> { - self.post().read(key).into_env_result() + fn pre(&'view self) -> Self::Pre { + CtxPreStorageRead { _ctx: self } } - fn read_bytes_post( - &self, - key: &storage::Key, - ) -> Result>, Self::Error> { - self.post().read_bytes(key).into_env_result() + fn post(&'view self) -> Self::Post { + CtxPostStorageRead { _ctx: self } } fn read_temp( @@ -203,29 +185,24 @@ impl VpEnv for Ctx { Ok(read_from_buffer(read_result, anoma_vp_result_buffer)) } - fn has_key_pre(&self, key: &storage::Key) -> Result { - self.pre().has_key(key).into_env_result() - } - - fn has_key_post(&self, key: &storage::Key) -> Result { - self.post().has_key(key).into_env_result() - } - - fn get_chain_id(&self) -> Result { + fn get_chain_id(&'view self) -> Result { // Both `CtxPreStorageRead` and `CtxPostStorageRead` have the same impl - self.pre().get_chain_id().into_env_result() + get_chain_id().into_env_result() } - fn get_block_height(&self) -> Result { - self.pre().get_block_height().into_env_result() + fn get_block_height(&'view self) -> Result { + // Both `CtxPreStorageRead` and `CtxPostStorageRead` have the same impl + get_block_height().into_env_result() } - fn get_block_hash(&self) -> Result { - self.pre().get_block_hash().into_env_result() + fn get_block_hash(&'view self) -> Result { + // Both `CtxPreStorageRead` and `CtxPostStorageRead` have the same impl + get_block_hash().into_env_result() } - fn get_block_epoch(&self) -> Result { - self.pre().get_block_epoch().into_env_result() + fn get_block_epoch(&'view self) -> Result { + // Both `CtxPreStorageRead` and `CtxPostStorageRead` have the same impl + get_block_epoch().into_env_result() } fn iter_prefix( @@ -233,21 +210,7 @@ impl VpEnv for Ctx { prefix: &storage::Key, ) -> Result { // Both `CtxPreStorageRead` and `CtxPostStorageRead` have the same impl - self.pre().iter_prefix(prefix).into_env_result() - } - - fn iter_pre_next( - &self, - iter: &mut Self::PrefixIter, - ) -> Result)>, Self::Error> { - self.pre().iter_next(iter).into_env_result() - } - - fn iter_post_next( - &self, - iter: &mut Self::PrefixIter, - ) -> Result)>, Self::Error> { - self.post().iter_next(iter).into_env_result() + iter_prefix(prefix).into_env_result() } fn eval( @@ -315,14 +278,6 @@ impl StorageRead<'_> for CtxPreStorageRead<'_> { Ok(HostEnvResult::is_success(found)) } - fn iter_prefix( - &self, - prefix: &storage::Key, - ) -> Result { - // Note that this is the same as `CtxPostStorageRead` - iter_prefix(prefix) - } - fn iter_next( &self, iter: &mut Self::PrefixIter, @@ -334,27 +289,29 @@ impl StorageRead<'_> for CtxPreStorageRead<'_> { )) } + // ---- Methods below share the same implementation in `pre/post` ---- + + fn iter_prefix( + &self, + prefix: &storage::Key, + ) -> Result { + iter_prefix(prefix) + } + fn get_chain_id(&self) -> Result { get_chain_id() } fn get_block_height(&self) -> Result { - Ok(BlockHeight(unsafe { anoma_vp_get_block_height() })) + get_block_height() } fn get_block_hash(&self) -> Result { - let result = Vec::with_capacity(BLOCK_HASH_LENGTH); - unsafe { - anoma_vp_get_block_hash(result.as_ptr() as _); - } - let slice = unsafe { - slice::from_raw_parts(result.as_ptr(), BLOCK_HASH_LENGTH) - }; - Ok(BlockHash::try_from(slice).expect("Cannot convert the hash")) + get_block_hash() } fn get_block_epoch(&self) -> Result { - Ok(Epoch(unsafe { anoma_vp_get_block_epoch() })) + get_block_epoch() } } @@ -378,14 +335,6 @@ impl StorageRead<'_> for CtxPostStorageRead<'_> { Ok(HostEnvResult::is_success(found)) } - fn iter_prefix( - &self, - prefix: &storage::Key, - ) -> Result { - // Note that this is the same as `CtxPreStorageRead` - iter_prefix(prefix) - } - fn iter_next( &self, iter: &mut Self::PrefixIter, @@ -397,6 +346,15 @@ impl StorageRead<'_> for CtxPostStorageRead<'_> { )) } + // ---- Methods below share the same implementation in `pre/post` ---- + + fn iter_prefix( + &self, + prefix: &storage::Key, + ) -> Result { + iter_prefix(prefix) + } + fn get_chain_id(&self) -> Result { get_chain_id() } From c86185c640fdb91ff16f558d861fe7fed313e8ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 25 Aug 2022 13:03:44 +0200 Subject: [PATCH 38/81] changelog: add #380 --- .../improvements/380-vp-env-pre-post-via-storage-api.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/unreleased/improvements/380-vp-env-pre-post-via-storage-api.md diff --git a/.changelog/unreleased/improvements/380-vp-env-pre-post-via-storage-api.md b/.changelog/unreleased/improvements/380-vp-env-pre-post-via-storage-api.md new file mode 100644 index 0000000000..655cdf256a --- /dev/null +++ b/.changelog/unreleased/improvements/380-vp-env-pre-post-via-storage-api.md @@ -0,0 +1,3 @@ +- Added `pre/post` methods into `trait VpEnv` that return objects implementing + `trait StorageRead` for re-use of library code written on top of `StorageRead` + inside validity predicates. ([#380](https://github.com/anoma/namada/pull/380)) \ No newline at end of file From b811466de29d4c819b89d1a16f882a9082f5a100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 25 Aug 2022 14:38:03 +0200 Subject: [PATCH 39/81] update wasm checksums --- wasm/checksums.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index 2478056cb0..1c5fec452f 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -12,8 +12,8 @@ "tx_update_vp.wasm": "tx_update_vp.a304b3c70361cde5fda458ba5637d6825eb002198e73990a1c74351113b83d43.wasm", "tx_vote_proposal.wasm": "tx_vote_proposal.8f306e1d1bcc9ca7f47a39108554fc847d4ac6df7a8d87a6effdffeae6adc4c4.wasm", "tx_withdraw.wasm": "tx_withdraw.c288861e842f0b1ac6fbb95b14b33c8819ac587bbd5b483f2cdd0ea184206c65.wasm", - "vp_nft.wasm": "vp_nft.557883861270a0a5e42802ce4bfbbc28f9f0dfa3b76520ffdd319b69a4f2f34c.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.1fb8c53e7c164d141661743f3cdfaf30d30e6e16abd2f8814c0d8829a2e36e8a.wasm", - "vp_token.wasm": "vp_token.b130ca28e8029f5ad7a33bf3d4fbb9a3b08a29484feba34b988e902c4ce9c966.wasm", - "vp_user.wasm": "vp_user.2c45ba3e0b442210d4dd953679c774cc15f8773c5c25bda7f2ad60eaaff2d0a9.wasm" + "vp_nft.wasm": "vp_nft.379f0a9fdbc9611ba9afc8b03ea17eb1e7c63992be3c2ecd5dd506a0ec3809f3.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.9d28df0e4eea55c98ac05638cfc6211aaf8e1a4b9489f7057634c66d39835c36.wasm", + "vp_token.wasm": "vp_token.6506aea021bb6de9186e96fa0d9ea2ad35bcb66777d6ecf890a66cfe36a74f23.wasm", + "vp_user.wasm": "vp_user.77a0d0d406e300b2d5b9bc1c13bc50f233b6923d369db939ac82c8e10b64543c.wasm" } \ No newline at end of file From b8eccf3d04fefa61203a5fa6b1a534156e256313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 8 Sep 2022 17:11:57 +0200 Subject: [PATCH 40/81] deps: replace hex with data-encoding --- Cargo.lock | 6 +++--- apps/Cargo.toml | 2 +- apps/src/lib/client/rpc.rs | 3 ++- apps/src/lib/config/genesis.rs | 12 ++++++------ apps/src/lib/node/gossip/p2p/identity.rs | 6 ++++-- apps/src/lib/wallet/keys.rs | 7 ++++--- apps/src/lib/wasm_loader/mod.rs | 4 ++-- shared/Cargo.toml | 2 +- shared/src/proto/mod.rs | 5 +++-- shared/src/proto/types.rs | 3 ++- shared/src/types/key/common.rs | 13 +++++++++---- shared/src/types/key/dkg_session_keys.rs | 7 +++++-- shared/src/types/key/ed25519.rs | 13 +++++++++---- shared/src/types/key/mod.rs | 6 +++--- tests/Cargo.toml | 2 +- tests/src/e2e/ledger_tests.rs | 3 ++- wasm/wasm_source/Cargo.lock | 8 +++++++- wasm_for_tests/wasm_source/Cargo.lock | 8 +++++++- 18 files changed, 71 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b7fce8b64d..3aef73a09a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3843,12 +3843,12 @@ dependencies = [ "byte-unit", "chrono", "clru", + "data-encoding", "derivative", "ed25519-consensus", "ferveo", "ferveo-common", "group-threshold-cryptography", - "hex", "ibc", "ibc-proto", "ics23", @@ -3905,6 +3905,7 @@ dependencies = [ "color-eyre", "config", "curl", + "data-encoding", "derivative", "directories", "ed25519-consensus", @@ -3915,7 +3916,6 @@ dependencies = [ "flate2", "futures 0.3.21", "git2", - "hex", "itertools 0.10.3", "jsonpath_lib", "libc", @@ -4007,13 +4007,13 @@ dependencies = [ "chrono", "color-eyre", "concat-idents", + "data-encoding", "derivative", "escargot", "expectrl", "eyre", "file-serve", "fs_extra", - "hex", "itertools 0.10.3", "libp2p", "namada", diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 7a4dffac76..3fcd3af977 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -62,6 +62,7 @@ clap = {git = "https://github.com/clap-rs/clap/", tag = "v3.0.0-beta.2", default color-eyre = "0.5.10" config = "0.11.0" curl = "0.4.43" +data-encoding = "2.3.2" derivative = "2.2.0" directories = "4.0.1" ed25519-consensus = "1.2.0" @@ -71,7 +72,6 @@ eyre = "0.6.5" flate2 = "1.0.22" file-lock = "2.0.2" futures = "0.3" -hex = "0.4.3" itertools = "0.10.1" jsonpath_lib = "0.3.0" libc = "0.2.97" diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 34652ac825..67e81c1588 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -11,6 +11,7 @@ use async_std::fs::{self}; use async_std::path::PathBuf; use async_std::prelude::*; use borsh::BorshDeserialize; +use data_encoding::HEXLOWER; use itertools::Itertools; use namada::ledger::governance::storage as gov_storage; use namada::ledger::governance::utils::Votes; @@ -81,7 +82,7 @@ pub async fn query_raw_bytes(_ctx: Context, args: args::QueryRawBytes) { .unwrap(); match response.code { Code::Ok => { - println!("{}", hex::encode(&response.value)); + println!("{}", HEXLOWER.encode(&response.value)); } Code::Err(err) => { eprintln!( diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 5bd3dc803f..9c1fff3dcc 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -26,7 +26,7 @@ pub mod genesis_config { use std::path::Path; use std::str::FromStr; - use hex; + use data_encoding::HEXLOWER; use namada::ledger::governance::parameters::GovParams; use namada::ledger::parameters::{EpochDuration, Parameters}; use namada::ledger::pos::types::BasisPoints; @@ -50,12 +50,12 @@ pub mod genesis_config { impl HexString { pub fn to_bytes(&self) -> Result, HexKeyError> { - let bytes = hex::decode(&self.0)?; + let bytes = HEXLOWER.decode(self.0.as_ref())?; Ok(bytes) } pub fn to_sha256_bytes(&self) -> Result<[u8; 32], HexKeyError> { - let bytes = hex::decode(&self.0)?; + let bytes = HEXLOWER.decode(self.0.as_ref())?; let slice = bytes.as_slice(); let array: [u8; 32] = slice.try_into()?; Ok(array) @@ -76,15 +76,15 @@ pub mod genesis_config { #[derive(Error, Debug)] pub enum HexKeyError { #[error("Invalid hex string: {0:?}")] - InvalidHexString(hex::FromHexError), + InvalidHexString(data_encoding::DecodeError), #[error("Invalid sha256 checksum: {0}")] InvalidSha256(TryFromSliceError), #[error("Invalid public key: {0}")] InvalidPublicKey(ParsePublicKeyError), } - impl From for HexKeyError { - fn from(err: hex::FromHexError) -> Self { + impl From for HexKeyError { + fn from(err: data_encoding::DecodeError) -> Self { Self::InvalidHexString(err) } } diff --git a/apps/src/lib/node/gossip/p2p/identity.rs b/apps/src/lib/node/gossip/p2p/identity.rs index 42442054c8..3227060014 100644 --- a/apps/src/lib/node/gossip/p2p/identity.rs +++ b/apps/src/lib/node/gossip/p2p/identity.rs @@ -21,6 +21,7 @@ pub struct Identity { // TODO this is needed because libp2p does not export ed255519 serde // feature maybe a MR for libp2p to export theses functions ? mod keypair_serde { + use data_encoding::HEXLOWER; use libp2p::identity::ed25519::Keypair; use serde::de::Error; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -33,7 +34,7 @@ mod keypair_serde { S: Serializer, { let bytes = value.encode(); - let string = hex::encode(&bytes[..]); + let string = HEXLOWER.encode(&bytes[..]); string.serialize(serializer) } pub fn deserialize<'d, D>(deserializer: D) -> Result @@ -41,7 +42,8 @@ mod keypair_serde { D: Deserializer<'d>, { let string = String::deserialize(deserializer)?; - let mut bytes = hex::decode(&string).map_err(Error::custom)?; + let mut bytes = + HEXLOWER.decode(string.as_ref()).map_err(Error::custom)?; Keypair::decode(bytes.as_mut()).map_err(Error::custom) } } diff --git a/apps/src/lib/wallet/keys.rs b/apps/src/lib/wallet/keys.rs index 1c521e7515..e922c7df5a 100644 --- a/apps/src/lib/wallet/keys.rs +++ b/apps/src/lib/wallet/keys.rs @@ -5,6 +5,7 @@ use std::rc::Rc; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; +use data_encoding::HEXLOWER; use namada::types::key::*; use orion::{aead, kdf}; use serde::{Deserialize, Serialize}; @@ -108,15 +109,15 @@ pub struct EncryptedKeypair(Vec); impl Display for EncryptedKeypair { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(&self.0)) + write!(f, "{}", HEXLOWER.encode(self.0.as_ref())) } } impl FromStr for EncryptedKeypair { - type Err = hex::FromHexError; + type Err = data_encoding::DecodeError; fn from_str(s: &str) -> Result { - hex::decode(s).map(Self) + HEXLOWER.decode(s.as_ref()).map(Self) } } diff --git a/apps/src/lib/wasm_loader/mod.rs b/apps/src/lib/wasm_loader/mod.rs index b6cb424457..e41efdbf77 100644 --- a/apps/src/lib/wasm_loader/mod.rs +++ b/apps/src/lib/wasm_loader/mod.rs @@ -4,8 +4,8 @@ use std::collections::HashMap; use std::fs; use std::path::Path; +use data_encoding::HEXLOWER; use futures::future::join_all; -use hex; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use thiserror::Error; @@ -144,7 +144,7 @@ pub async fn pre_fetch_wasm(wasm_directory: impl AsRef) { Ok(bytes) => { let mut hasher = Sha256::new(); hasher.update(bytes); - let result = hex::encode(hasher.finalize()); + let result = HEXLOWER.encode(&hasher.finalize()); let derived_name = format!( "{}.{}.wasm", &name.split('.').collect::>()[0], diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 241fd034da..7bff772557 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -52,11 +52,11 @@ borsh = "0.9.0" chrono = "0.4.19" # Using unreleased commit on top of version 0.5.0 that adds Sync to the CLruCache clru = {git = "https://github.com/marmeladema/clru-rs.git", rev = "71ca566"} +data-encoding = "2.3.2" derivative = "2.2.0" ed25519-consensus = "1.2.0" ferveo = {optional = true, git = "https://github.com/anoma/ferveo"} ferveo-common = {git = "https://github.com/anoma/ferveo"} -hex = "0.4.3" tpke = {package = "group-threshold-cryptography", optional = true, git = "https://github.com/anoma/ferveo"} # TODO using the same version of tendermint-rs as we do here. ibc = {git = "https://github.com/heliaxdev/ibc-rs", rev = "30b3495ac56c6c37c99bc69ef9f2e84c3309c6cc", default-features = false} diff --git a/shared/src/proto/mod.rs b/shared/src/proto/mod.rs index aa971f0b96..1ee5da63c8 100644 --- a/shared/src/proto/mod.rs +++ b/shared/src/proto/mod.rs @@ -9,6 +9,7 @@ pub use types::{ #[cfg(test)] mod tests { + use data_encoding::HEXLOWER; use generated::types::Tx; use prost::Message; @@ -23,8 +24,8 @@ mod tests { }; let mut tx_bytes = vec![]; tx.encode(&mut tx_bytes).unwrap(); - let tx_hex = hex::encode(tx_bytes); - let tx_from_hex = hex::decode(tx_hex).unwrap(); + let tx_hex = HEXLOWER.encode(&tx_bytes); + let tx_from_hex = HEXLOWER.decode(tx_hex.as_ref()).unwrap(); let tx_from_bytes = Tx::decode(&tx_from_hex[..]).unwrap(); assert_eq!(tx, tx_from_bytes); } diff --git a/shared/src/proto/types.rs b/shared/src/proto/types.rs index 7a936dd58f..6c6aa23234 100644 --- a/shared/src/proto/types.rs +++ b/shared/src/proto/types.rs @@ -4,6 +4,7 @@ use std::fmt::Display; use std::hash::{Hash, Hasher}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use data_encoding::HEXLOWER; use prost::Message; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -374,7 +375,7 @@ impl>> From for IntentId { impl Display for IntentId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(&self.0)) + write!(f, "{}", HEXLOWER.encode(&self.0)) } } diff --git a/shared/src/types/key/common.rs b/shared/src/types/key/common.rs index 27e7c29b89..bc3257f06a 100644 --- a/shared/src/types/key/common.rs +++ b/shared/src/types/key/common.rs @@ -4,6 +4,7 @@ use std::fmt::Display; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use data_encoding::HEXLOWER; #[cfg(feature = "rand")] use rand::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; @@ -57,7 +58,7 @@ impl super::PublicKey for PublicKey { impl Display for PublicKey { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", hex::encode(&self.try_to_vec().unwrap())) + write!(f, "{}", HEXLOWER.encode(&self.try_to_vec().unwrap())) } } @@ -65,7 +66,9 @@ impl FromStr for PublicKey { type Err = ParsePublicKeyError; fn from_str(str: &str) -> Result { - let vec = hex::decode(str).map_err(ParsePublicKeyError::InvalidHex)?; + let vec = HEXLOWER + .decode(str.as_ref()) + .map_err(ParsePublicKeyError::InvalidHex)?; Self::try_from_slice(vec.as_slice()) .map_err(ParsePublicKeyError::InvalidEncoding) } @@ -152,7 +155,7 @@ impl RefTo for SecretKey { impl Display for SecretKey { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", hex::encode(&self.try_to_vec().unwrap())) + write!(f, "{}", HEXLOWER.encode(&self.try_to_vec().unwrap())) } } @@ -160,7 +163,9 @@ impl FromStr for SecretKey { type Err = ParseSecretKeyError; fn from_str(str: &str) -> Result { - let vec = hex::decode(str).map_err(ParseSecretKeyError::InvalidHex)?; + let vec = HEXLOWER + .decode(str.as_ref()) + .map_err(ParseSecretKeyError::InvalidHex)?; Self::try_from_slice(vec.as_slice()) .map_err(ParseSecretKeyError::InvalidEncoding) } diff --git a/shared/src/types/key/dkg_session_keys.rs b/shared/src/types/key/dkg_session_keys.rs index 26f6fffa00..f2cafb639c 100644 --- a/shared/src/types/key/dkg_session_keys.rs +++ b/shared/src/types/key/dkg_session_keys.rs @@ -7,6 +7,7 @@ use std::str::FromStr; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use data_encoding::HEXLOWER; use serde::{Deserialize, Serialize}; use crate::types::address::Address; @@ -142,7 +143,7 @@ impl Display for DkgPublicKey { let vec = self .try_to_vec() .expect("Encoding public key shouldn't fail"); - write!(f, "{}", hex::encode(&vec)) + write!(f, "{}", HEXLOWER.encode(&vec)) } } @@ -150,7 +151,9 @@ impl FromStr for DkgPublicKey { type Err = ParsePublicKeyError; fn from_str(s: &str) -> Result { - let vec = hex::decode(s).map_err(ParsePublicKeyError::InvalidHex)?; + let vec = HEXLOWER + .decode(s.as_ref()) + .map_err(ParsePublicKeyError::InvalidHex)?; BorshDeserialize::try_from_slice(&vec) .map_err(ParsePublicKeyError::InvalidEncoding) } diff --git a/shared/src/types/key/ed25519.rs b/shared/src/types/key/ed25519.rs index 12e5093bd2..90ab540f19 100644 --- a/shared/src/types/key/ed25519.rs +++ b/shared/src/types/key/ed25519.rs @@ -6,6 +6,7 @@ use std::io::Write; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use data_encoding::HEXLOWER; #[cfg(feature = "rand")] use rand::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; @@ -107,7 +108,7 @@ impl Ord for PublicKey { impl Display for PublicKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(&self.0.to_bytes())) + write!(f, "{}", HEXLOWER.encode(&self.0.to_bytes())) } } @@ -115,7 +116,9 @@ impl FromStr for PublicKey { type Err = ParsePublicKeyError; fn from_str(s: &str) -> Result { - let vec = hex::decode(s).map_err(ParsePublicKeyError::InvalidHex)?; + let vec = HEXLOWER + .decode(s.as_ref()) + .map_err(ParsePublicKeyError::InvalidHex)?; BorshDeserialize::try_from_slice(&vec) .map_err(ParsePublicKeyError::InvalidEncoding) } @@ -204,7 +207,7 @@ impl BorshSchema for SecretKey { impl Display for SecretKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(&self.0.to_bytes())) + write!(f, "{}", HEXLOWER.encode(&self.0.to_bytes())) } } @@ -212,7 +215,9 @@ impl FromStr for SecretKey { type Err = ParseSecretKeyError; fn from_str(s: &str) -> Result { - let vec = hex::decode(s).map_err(ParseSecretKeyError::InvalidHex)?; + let vec = HEXLOWER + .decode(s.as_ref()) + .map_err(ParseSecretKeyError::InvalidHex)?; BorshDeserialize::try_from_slice(&vec) .map_err(ParseSecretKeyError::InvalidEncoding) } diff --git a/shared/src/types/key/mod.rs b/shared/src/types/key/mod.rs index a1343cd7e5..28a0fe1bf6 100644 --- a/shared/src/types/key/mod.rs +++ b/shared/src/types/key/mod.rs @@ -79,7 +79,7 @@ pub enum VerifySigError { #[derive(Error, Debug)] pub enum ParsePublicKeyError { #[error("Invalid public key hex: {0}")] - InvalidHex(hex::FromHexError), + InvalidHex(data_encoding::DecodeError), #[error("Invalid public key encoding: {0}")] InvalidEncoding(std::io::Error), #[error("Parsed public key does not belong to desired scheme")] @@ -90,7 +90,7 @@ pub enum ParsePublicKeyError { #[derive(Error, Debug)] pub enum ParseSignatureError { #[error("Invalid signature hex: {0}")] - InvalidHex(hex::FromHexError), + InvalidHex(data_encoding::DecodeError), #[error("Invalid signature encoding: {0}")] InvalidEncoding(std::io::Error), #[error("Parsed signature does not belong to desired scheme")] @@ -101,7 +101,7 @@ pub enum ParseSignatureError { #[derive(Error, Debug)] pub enum ParseSecretKeyError { #[error("Invalid secret key hex: {0}")] - InvalidHex(hex::FromHexError), + InvalidHex(data_encoding::DecodeError), #[error("Invalid secret key encoding: {0}")] InvalidEncoding(std::io::Error), #[error("Parsed secret key does not belong to desired scheme")] diff --git a/tests/Cargo.toml b/tests/Cargo.toml index cc437b3686..6872e912b7 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -31,13 +31,13 @@ namada_apps = {path = "../apps", default-features = false, features = ["testing" assert_cmd = "1.0.7" borsh = "0.9.1" color-eyre = "0.5.11" +data-encoding = "2.3.2" # NOTE: enable "print" feature to see output from builds ran by e2e tests escargot = {version = "0.5.7"} # , features = ["print"]} expectrl = {version = "=0.5.2"} eyre = "0.6.5" file-serve = "0.2.0" fs_extra = "1.2.0" -hex = "0.4.3" itertools = "0.10.0" libp2p = "0.38.0" pretty_assertions = "0.7.2" diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 552e675e67..307112ac2a 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -17,6 +17,7 @@ use std::time::{Duration, Instant}; use borsh::BorshSerialize; use color_eyre::eyre::Result; +use data_encoding::HEXLOWER; use namada::types::token; use namada_apps::config::genesis::genesis_config::{ GenesisConfig, ParametersConfig, PosParamsConfig, @@ -386,7 +387,7 @@ fn ledger_txs_and_queries() -> Result<()> { &validator_one_rpc, ], // expect hex encoded of borsh encoded bytes - hex::encode(christel_balance.try_to_vec().unwrap()), + HEXLOWER.encode(&christel_balance.try_to_vec().unwrap()), ), ]; for (query_args, expected) in &query_args_and_expected_response { diff --git a/wasm/wasm_source/Cargo.lock b/wasm/wasm_source/Cargo.lock index 19a50588dc..0a186f3fd4 100644 --- a/wasm/wasm_source/Cargo.lock +++ b/wasm/wasm_source/Cargo.lock @@ -602,6 +602,12 @@ dependencies = [ "syn", ] +[[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + [[package]] name = "derivative" version = "2.2.0" @@ -1359,10 +1365,10 @@ dependencies = [ "borsh", "chrono", "clru", + "data-encoding", "derivative", "ed25519-consensus", "ferveo-common", - "hex", "ibc", "ibc-proto", "ics23", diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 3e7bbbe365..e82a61634b 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -603,6 +603,12 @@ dependencies = [ "syn", ] +[[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + [[package]] name = "derivative" version = "2.2.0" @@ -1370,10 +1376,10 @@ dependencies = [ "borsh", "chrono", "clru", + "data-encoding", "derivative", "ed25519-consensus", "ferveo-common", - "hex", "ibc", "ibc-proto", "ics23", From b47d1c1d16699a538fb81886615cf930046687b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 8 Sep 2022 17:12:52 +0200 Subject: [PATCH 41/81] shared/storage/key: add support for int/uint keys that maintain order --- shared/src/types/storage.rs | 71 ++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/shared/src/types/storage.rs b/shared/src/types/storage.rs index fc87bc8d51..1892334031 100644 --- a/shared/src/types/storage.rs +++ b/shared/src/types/storage.rs @@ -6,6 +6,7 @@ use std::ops::{Add, Div, Mul, Rem, Sub}; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use data_encoding::BASE32HEX_NOPAD; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -27,6 +28,8 @@ pub enum Error { ParseAddressFromKey, #[error("Reserved prefix or string is specified: {0}")] InvalidKeySeg(String), + #[error("Error parsing key segment {0}")] + ParseKeySeg(String), } /// Result for functions that may fail @@ -193,6 +196,7 @@ impl Header { BorshDeserialize, BorshSchema, Debug, + Default, Eq, PartialEq, Ord, @@ -446,14 +450,12 @@ impl KeySeg for String { impl KeySeg for BlockHeight { fn parse(string: String) -> Result { - let h = string.parse::().map_err(|e| Error::Temporary { - error: format!("Unexpected height value {}, {}", string, e), - })?; + let h: u64 = KeySeg::parse(string)?; Ok(BlockHeight(h)) } fn raw(&self) -> String { - format!("{}", self.0) + self.0.raw() } fn to_db_key(&self) -> DbKeySeg { @@ -481,6 +483,67 @@ impl KeySeg for Address { } } +/// Implement [`KeySeg`] for a type via base32hex of its BE bytes (using +/// `to_le_bytes()` and `from_le_bytes` methods) that maintains sort order of +/// the original data. +// TODO this could be a bit more efficient without the string conversion (atm +// with base32hex), if we can use bytes for storage key directly (which we can +// with rockDB, but atm, we're calling `to_string()` using the custom `Display` +// impl from here) +macro_rules! impl_int_key_seg { + ($unsigned:ty, $signed:ty, $len:literal) => { + impl KeySeg for $unsigned { + fn parse(string: String) -> Result { + let bytes = + BASE32HEX_NOPAD.decode(string.as_ref()).map_err(|err| { + Error::ParseKeySeg(format!( + "Failed parsing {} with {}", + string, err + )) + })?; + let mut fixed_bytes = [0; $len]; + fixed_bytes.copy_from_slice(&bytes); + Ok(<$unsigned>::from_be_bytes(fixed_bytes)) + } + + fn raw(&self) -> String { + BASE32HEX_NOPAD.encode(&self.to_be_bytes()) + } + + fn to_db_key(&self) -> DbKeySeg { + DbKeySeg::StringSeg(self.raw()) + } + } + + impl KeySeg for $signed { + fn parse(string: String) -> Result { + // get signed int from a unsigned int complemented with a min + // value + let complemented = <$unsigned>::parse(string)?; + let signed = (complemented as $signed) ^ <$signed>::MIN; + Ok(signed) + } + + fn raw(&self) -> String { + // signed int is converted to unsigned int that preserves the + // order by complementing it with a min value + let complemented = (*self ^ <$signed>::MIN) as $unsigned; + complemented.raw() + } + + fn to_db_key(&self) -> DbKeySeg { + DbKeySeg::StringSeg(self.raw()) + } + } + }; +} + +impl_int_key_seg!(u8, i8, 1); +impl_int_key_seg!(u16, i16, 2); +impl_int_key_seg!(u32, i32, 4); +impl_int_key_seg!(u64, i64, 8); +impl_int_key_seg!(u128, i128, 16); + /// Epoch identifier. Epochs are identified by consecutive numbers. #[derive( Clone, From e64808559e6f8673c482dd8a3081bb3bfda8a845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 8 Sep 2022 17:27:14 +0200 Subject: [PATCH 42/81] test/vm_host_env: check prefix iter order in tx and VP --- tests/src/vm_host_env/mod.rs | 67 +++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index 98f95e7fae..f846325c40 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -157,11 +157,14 @@ mod tests { should yield an empty iterator." ); - // Write some values directly into the storage first - let prefix = Key::parse("prefix").unwrap(); + let prefix = storage::Key::parse("prefix").unwrap(); + // We'll write sub-key in some random order to check prefix iter's order + let sub_keys = [2_i32, 1, i32::MAX, -1, 260, -2, i32::MIN, 5, 0]; + + // Write the values directly into the storage first tx_host_env::with(|env| { - for i in 0..10_i32 { - let key = prefix.join(&Key::parse(i.to_string()).unwrap()); + for i in sub_keys.iter() { + let key = prefix.push(i).unwrap(); let value = i.try_to_vec().unwrap(); env.storage.write(&key, value).unwrap(); } @@ -171,11 +174,14 @@ mod tests { // Then try to iterate over their prefix let iter = namada_tx_prelude::iter_prefix(tx::ctx(), &prefix) .unwrap() - .map(|item| item.unwrap()); - let expected = (0..10).map(|i| { - (storage::Key::parse(format!("{}/{}", prefix, i)).unwrap(), i) - }); - itertools::assert_equal(iter.sorted(), expected.sorted()); + .map(Result::unwrap); + + // The order has to be sorted by sub-key value + let expected = sub_keys + .iter() + .sorted() + .map(|i| (prefix.push(i).unwrap(), *i)); + itertools::assert_equal(iter, expected); } #[test] @@ -251,7 +257,7 @@ mod tests { // We can add some data to the environment let key_raw = "key"; - let key = Key::parse(key_raw).unwrap(); + let key = storage::Key::parse(key_raw).unwrap(); let value = "test".to_string(); let value_raw = value.try_to_vec().unwrap(); vp_host_env::with(|env| { @@ -269,7 +275,7 @@ mod tests { let mut tx_env = TestTxEnv::default(); let addr = address::testing::established_address_1(); - let addr_key = Key::from(addr.to_db_key()); + let addr_key = storage::Key::from(addr.to_db_key()); // Write some value to storage let existing_key = @@ -354,12 +360,15 @@ mod tests { let mut tx_env = TestTxEnv::default(); let addr = address::testing::established_address_1(); - let addr_key = Key::from(addr.to_db_key()); + let addr_key = storage::Key::from(addr.to_db_key()); - // Write some value to storage let prefix = addr_key.join(&Key::parse("prefix").unwrap()); - for i in 0..10_i32 { - let key = prefix.join(&Key::parse(i.to_string()).unwrap()); + // We'll write sub-key in some random order to check prefix iter's order + let sub_keys = [2_i32, 1, i32::MAX, -1, 260, -2, i32::MIN, 5, 0]; + + // Write some values to storage + for i in sub_keys.iter() { + let key = prefix.push(i).unwrap(); let value = i.try_to_vec().unwrap(); tx_env.storage.write(&key, value).unwrap(); } @@ -367,8 +376,8 @@ mod tests { // In a transaction, write override the existing key's value and add // another key-value - let existing_key = prefix.join(&Key::parse(5.to_string()).unwrap()); - let new_key = prefix.join(&Key::parse(11.to_string()).unwrap()); + let existing_key = prefix.push(&5).unwrap(); + let new_key = prefix.push(&11).unwrap(); // Initialize the VP environment via a transaction vp_host_env::init_from_tx(addr, tx_env, |_addr| { @@ -383,23 +392,25 @@ mod tests { let iter_pre = namada_vp_prelude::iter_prefix(&ctx_pre, &prefix) .unwrap() .map(|item| item.unwrap()); - let expected_pre = (0..10).map(|i| { - (storage::Key::parse(format!("{}/{}", prefix, i)).unwrap(), i) - }); - itertools::assert_equal(iter_pre.sorted(), expected_pre.sorted()); + + // The order in pre has to be sorted by sub-key value + let expected_pre = sub_keys + .iter() + .sorted() + .map(|i| (prefix.push(i).unwrap(), *i)); + itertools::assert_equal(iter_pre, expected_pre); let ctx_post = vp::CTX.post(); let iter_post = namada_vp_prelude::iter_prefix(&ctx_post, &prefix) .unwrap() .map(|item| item.unwrap()); - let expected_post = (0..10).map(|i| { - let val = if i == 5 { 100 } else { i }; - ( - storage::Key::parse(format!("{}/{}", prefix, i)).unwrap(), - val, - ) + + // The order in post also has to be sorted + let expected_post = sub_keys.iter().sorted().map(|i| { + let val = if *i == 5 { 100 } else { *i }; + (prefix.push(i).unwrap(), val) }); - itertools::assert_equal(iter_post.sorted(), expected_post.sorted()); + itertools::assert_equal(iter_post, expected_post); } #[test] From 1429b929923c5973be8e3ea1e8970e1a3d63e090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 8 Sep 2022 18:04:55 +0200 Subject: [PATCH 43/81] add support for rev_iter_prefix in storage and VP and tx envs --- apps/src/lib/node/ledger/storage/rocksdb.rs | 42 ++++++---- shared/src/ledger/native_vp.rs | 25 ++++++ shared/src/ledger/storage/mockdb.rs | 48 +++++++++-- shared/src/ledger/storage/mod.rs | 25 +++++- shared/src/ledger/storage_api/mod.rs | 89 +++++++++++++++++++-- shared/src/ledger/vp_env.rs | 30 ++++++- shared/src/vm/host_env.rs | 68 +++++++++++++++- shared/src/vm/wasm/host_env.rs | 2 + tests/src/vm_host_env/tx.rs | 1 + tests/src/vm_host_env/vp.rs | 1 + tx_prelude/src/lib.rs | 14 +++- vm_env/src/lib.rs | 20 ++++- vp_prelude/src/lib.rs | 37 ++++++++- 13 files changed, 364 insertions(+), 38 deletions(-) diff --git a/apps/src/lib/node/ledger/storage/rocksdb.rs b/apps/src/lib/node/ledger/storage/rocksdb.rs index 71ed305ea5..44f55fa8e3 100644 --- a/apps/src/lib/node/ledger/storage/rocksdb.rs +++ b/apps/src/lib/node/ledger/storage/rocksdb.rs @@ -806,24 +806,36 @@ impl<'iter> DBIter<'iter> for RocksDB { &'iter self, prefix: &Key, ) -> PersistentPrefixIterator<'iter> { - let db_prefix = "subspace/".to_owned(); - let prefix = format!("{}{}", db_prefix, prefix); + iter_prefix(self, prefix, Direction::Forward) + } - let mut read_opts = ReadOptions::default(); - // don't use the prefix bloom filter - read_opts.set_total_order_seek(true); - let mut upper_prefix = prefix.clone().into_bytes(); - if let Some(last) = upper_prefix.pop() { - upper_prefix.push(last + 1); - } - read_opts.set_iterate_upper_bound(upper_prefix); + fn rev_iter_prefix(&'iter self, prefix: &Key) -> Self::PrefixIter { + iter_prefix(self, prefix, Direction::Reverse) + } +} - let iter = self.0.iterator_opt( - IteratorMode::From(prefix.as_bytes(), Direction::Forward), - read_opts, - ); - PersistentPrefixIterator(PrefixIterator::new(iter, db_prefix)) +fn iter_prefix<'iter>( + db: &'iter RocksDB, + prefix: &Key, + direction: Direction, +) -> PersistentPrefixIterator<'iter> { + let db_prefix = "subspace/".to_owned(); + let prefix = format!("{}{}", db_prefix, prefix); + + let mut read_opts = ReadOptions::default(); + // don't use the prefix bloom filter + read_opts.set_total_order_seek(true); + let mut upper_prefix = prefix.clone().into_bytes(); + if let Some(last) = upper_prefix.pop() { + upper_prefix.push(last + 1); } + read_opts.set_iterate_upper_bound(upper_prefix); + + let iter = db.0.iterator_opt( + IteratorMode::From(prefix.as_bytes(), direction), + read_opts, + ); + PersistentPrefixIterator(PrefixIterator::new(iter, db_prefix)) } #[derive(Debug)] diff --git a/shared/src/ledger/native_vp.rs b/shared/src/ledger/native_vp.rs index 5cbab7d639..845a44c0ed 100644 --- a/shared/src/ledger/native_vp.rs +++ b/shared/src/ledger/native_vp.rs @@ -186,6 +186,13 @@ where self.ctx.iter_prefix(prefix).into_storage_result() } + fn rev_iter_prefix( + &self, + prefix: &crate::types::storage::Key, + ) -> storage_api::Result { + self.ctx.rev_iter_prefix(prefix).into_storage_result() + } + fn iter_next( &self, iter: &mut Self::PrefixIter, @@ -247,6 +254,13 @@ where self.ctx.iter_prefix(prefix).into_storage_result() } + fn rev_iter_prefix( + &self, + prefix: &crate::types::storage::Key, + ) -> storage_api::Result { + self.ctx.rev_iter_prefix(prefix).into_storage_result() + } + fn iter_next( &self, iter: &mut Self::PrefixIter, @@ -400,6 +414,17 @@ where ) } + fn rev_iter_prefix( + &self, + prefix: &Key, + ) -> Result { + vp_env::rev_iter_prefix( + &mut *self.gas_meter.borrow_mut(), + self.storage, + prefix, + ) + } + fn iter_pre_next( &self, iter: &mut Self::PrefixIter, diff --git a/shared/src/ledger/storage/mockdb.rs b/shared/src/ledger/storage/mockdb.rs index 3cc7e8863c..a9e3e8057b 100644 --- a/shared/src/ledger/storage/mockdb.rs +++ b/shared/src/ledger/storage/mockdb.rs @@ -431,7 +431,28 @@ impl<'iter> DBIter<'iter> for MockDB { let db_prefix = "subspace/".to_owned(); let prefix = format!("{}{}", db_prefix, prefix); let iter = self.0.borrow().clone().into_iter(); - MockPrefixIterator::new(MockIterator { prefix, iter }, db_prefix) + MockPrefixIterator::new( + MockIterator { + prefix, + iter, + reverse_order: false, + }, + db_prefix, + ) + } + + fn rev_iter_prefix(&'iter self, prefix: &Key) -> Self::PrefixIter { + let db_prefix = "subspace/".to_owned(); + let prefix = format!("{}{}", db_prefix, prefix); + let iter = self.0.borrow().clone().into_iter(); + MockPrefixIterator::new( + MockIterator { + prefix, + iter, + reverse_order: true, + }, + db_prefix, + ) } } @@ -441,6 +462,8 @@ pub struct MockIterator { prefix: String, /// The concrete iterator pub iter: btree_map::IntoIter>, + /// Is the iterator in reverse order? + reverse_order: bool, } /// A prefix iterator for the [`MockDB`]. @@ -450,12 +473,23 @@ impl Iterator for MockIterator { type Item = KVBytes; fn next(&mut self) -> Option { - for (key, val) in &mut self.iter { - if key.starts_with(&self.prefix) { - return Some(( - Box::from(key.as_bytes()), - Box::from(val.as_slice()), - )); + if self.reverse_order { + for (key, val) in (&mut self.iter).rev() { + if key.starts_with(&self.prefix) { + return Some(( + Box::from(key.as_bytes()), + Box::from(val.as_slice()), + )); + } + } + } else { + for (key, val) in &mut self.iter { + if key.starts_with(&self.prefix) { + return Some(( + Box::from(key.as_bytes()), + Box::from(val.as_slice()), + )); + } } } None diff --git a/shared/src/ledger/storage/mod.rs b/shared/src/ledger/storage/mod.rs index d8c8e28091..347d0596cf 100644 --- a/shared/src/ledger/storage/mod.rs +++ b/shared/src/ledger/storage/mod.rs @@ -243,8 +243,13 @@ pub trait DBIter<'iter> { /// The concrete type of the iterator type PrefixIter: Debug + Iterator, u64)>; - /// Read account subspace key value pairs with the given prefix from the DB + /// Read account subspace key value pairs with the given prefix from the DB, + /// ordered by the storage keys. fn iter_prefix(&'iter self, prefix: &Key) -> Self::PrefixIter; + + /// Read account subspace key value pairs with the given prefix from the DB, + /// reverse ordered by the storage keys. + fn rev_iter_prefix(&'iter self, prefix: &Key) -> Self::PrefixIter; } /// Atomic batch write. @@ -411,7 +416,7 @@ where } } - /// Returns a prefix iterator and the gas cost + /// Returns a prefix iterator, ordered by storage keys, and the gas cost pub fn iter_prefix( &self, prefix: &Key, @@ -419,6 +424,15 @@ where (self.db.iter_prefix(prefix), prefix.len() as _) } + /// Returns a prefix iterator, reverse ordered by storage keys, and the gas + /// cost + pub fn rev_iter_prefix( + &self, + prefix: &Key, + ) -> (>::PrefixIter, u64) { + (self.db.rev_iter_prefix(prefix), prefix.len() as _) + } + /// Write a value to the specified subspace and returns the gas cost and the /// size difference pub fn write( @@ -729,6 +743,13 @@ where Ok(self.db.iter_prefix(prefix)) } + fn rev_iter_prefix( + &'iter self, + prefix: &crate::types::storage::Key, + ) -> std::result::Result { + Ok(self.db.rev_iter_prefix(prefix)) + } + fn iter_next( &self, iter: &mut Self::PrefixIter, diff --git a/shared/src/ledger/storage_api/mod.rs b/shared/src/ledger/storage_api/mod.rs index ab2f73784f..39b44d24d9 100644 --- a/shared/src/ledger/storage_api/mod.rs +++ b/shared/src/ledger/storage_api/mod.rs @@ -45,8 +45,8 @@ pub trait StorageRead<'iter> { /// Storage `has_key` in. It will try to read from the storage. fn has_key(&self, key: &storage::Key) -> Result; - /// Storage prefix iterator. It will try to get an iterator from the - /// storage. + /// Storage prefix iterator ordered by the storage keys. It will try to get + /// an iterator from the storage. /// /// For a more user-friendly iterator API, use [`fn@iter_prefix`] or /// [`fn@iter_prefix_bytes`] instead. @@ -55,7 +55,17 @@ pub trait StorageRead<'iter> { prefix: &storage::Key, ) -> Result; - /// Storage prefix iterator for. It will try to read from the storage. + /// Storage prefix iterator in reverse order of the storage keys. It will + /// try to get an iterator from the storage. + /// + /// For a more user-friendly iterator API, use [`fn@rev_iter_prefix`] or + /// [`fn@rev_iter_prefix_bytes`] instead. + fn rev_iter_prefix( + &'iter self, + prefix: &storage::Key, + ) -> Result; + + /// Storage prefix iterator. It will try to read from the storage. fn iter_next( &self, iter: &mut Self::PrefixIter, @@ -97,7 +107,7 @@ pub trait StorageWrite { fn delete(&mut self, key: &storage::Key) -> Result<()>; } -/// Iterate items matching the given prefix. +/// Iterate items matching the given prefix, ordered by the storage keys. pub fn iter_prefix_bytes<'a>( storage: &'a impl StorageRead<'a>, prefix: &crate::types::storage::Key, @@ -125,7 +135,8 @@ pub fn iter_prefix_bytes<'a>( Ok(iter) } -/// Iterate Borsh encoded items matching the given prefix. +/// Iterate Borsh encoded items matching the given prefix, ordered by the +/// storage keys. pub fn iter_prefix<'a, T>( storage: &'a impl StorageRead<'a>, prefix: &crate::types::storage::Key, @@ -162,3 +173,71 @@ where }); Ok(iter) } + +/// Iterate items matching the given prefix, reverse ordered by the storage +/// keys. +pub fn rev_iter_prefix_bytes<'a>( + storage: &'a impl StorageRead<'a>, + prefix: &crate::types::storage::Key, +) -> Result)>> + 'a> { + let iter = storage.rev_iter_prefix(prefix)?; + let iter = itertools::unfold(iter, |iter| { + match storage.iter_next(iter) { + Ok(Some((key, val))) => { + let key = match storage::Key::parse(key).into_storage_result() { + Ok(key) => key, + Err(err) => { + // Propagate key encoding errors into Iterator's Item + return Some(Err(err)); + } + }; + Some(Ok((key, val))) + } + Ok(None) => None, + Err(err) => { + // Propagate `iter_next` errors into Iterator's Item + Some(Err(err)) + } + } + }); + Ok(iter) +} + +/// Iterate Borsh encoded items matching the given prefix, reverse ordered by +/// the storage keys. +pub fn rev_iter_prefix<'a, T>( + storage: &'a impl StorageRead<'a>, + prefix: &crate::types::storage::Key, +) -> Result> + 'a> +where + T: BorshDeserialize, +{ + let iter = storage.rev_iter_prefix(prefix)?; + let iter = itertools::unfold(iter, |iter| { + match storage.iter_next(iter) { + Ok(Some((key, val))) => { + let key = match storage::Key::parse(key).into_storage_result() { + Ok(key) => key, + Err(err) => { + // Propagate key encoding errors into Iterator's Item + return Some(Err(err)); + } + }; + let val = match T::try_from_slice(&val).into_storage_result() { + Ok(val) => val, + Err(err) => { + // Propagate val encoding errors into Iterator's Item + return Some(Err(err)); + } + }; + Some(Ok((key, val))) + } + Ok(None) => None, + Err(err) => { + // Propagate `iter_next` errors into Iterator's Item + Some(Err(err)) + } + } + }); + Ok(iter) +} diff --git a/shared/src/ledger/vp_env.rs b/shared/src/ledger/vp_env.rs index 95e7c48782..9af4a992d3 100644 --- a/shared/src/ledger/vp_env.rs +++ b/shared/src/ledger/vp_env.rs @@ -91,13 +91,20 @@ pub trait VpEnv { /// current transaction is being applied. fn get_block_epoch(&self) -> Result; - /// Storage prefix iterator. It will try to get an iterator from the - /// storage. + /// Storage prefix iterator, ordered by storage keys. It will try to get an + /// iterator from the storage. fn iter_prefix( &self, prefix: &Key, ) -> Result; + /// Storage prefix iterator, reverse ordered by storage keys. It will try to + /// get an iterator from the storage. + fn rev_iter_prefix( + &self, + prefix: &Key, + ) -> Result; + /// Storage prefix iterator for prior state (before tx execution). It will /// try to read from the storage. fn iter_pre_next( @@ -396,7 +403,8 @@ where Ok(epoch) } -/// Storage prefix iterator. It will try to get an iterator from the storage. +/// Storage prefix iterator, ordered by storage keys. It will try to get an +/// iterator from the storage. pub fn iter_prefix<'a, DB, H>( gas_meter: &mut VpGasMeter, storage: &'a Storage, @@ -411,6 +419,22 @@ where Ok(iter) } +/// Storage prefix iterator, reverse ordered by storage keys. It will try to get +/// an iterator from the storage. +pub fn rev_iter_prefix<'a, DB, H>( + gas_meter: &mut VpGasMeter, + storage: &'a Storage, + prefix: &Key, +) -> EnvResult<>::PrefixIter> +where + DB: storage::DB + for<'iter> storage::DBIter<'iter>, + H: StorageHasher, +{ + let (iter, gas) = storage.rev_iter_prefix(prefix); + add_gas(gas_meter, gas)?; + Ok(iter) +} + /// Storage prefix iterator for prior state (before tx execution). It will try /// to read from the storage. pub fn iter_pre_next( diff --git a/shared/src/vm/host_env.rs b/shared/src/vm/host_env.rs index fc7fd33e0c..533a05d94e 100644 --- a/shared/src/vm/host_env.rs +++ b/shared/src/vm/host_env.rs @@ -665,7 +665,7 @@ where /// Storage prefix iterator function exposed to the wasm VM Tx environment. /// It will try to get an iterator from the storage and return the corresponding -/// ID of the iterator. +/// ID of the iterator, ordered by storage keys. pub fn tx_iter_prefix( env: &TxVmEnv, prefix_ptr: u64, @@ -695,6 +695,38 @@ where Ok(iterators.insert(iter).id()) } +/// Storage prefix iterator function exposed to the wasm VM Tx environment. +/// It will try to get an iterator from the storage and return the corresponding +/// ID of the iterator, reverse ordered by storage keys. +pub fn tx_rev_iter_prefix( + env: &TxVmEnv, + prefix_ptr: u64, + prefix_len: u64, +) -> TxResult +where + MEM: VmMemory, + DB: storage::DB + for<'iter> storage::DBIter<'iter>, + H: StorageHasher, + CA: WasmCacheAccess, +{ + let (prefix, gas) = env + .memory + .read_string(prefix_ptr, prefix_len as _) + .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; + tx_add_gas(env, gas)?; + + tracing::debug!("tx_rev_iter_prefix {}, prefix {}", prefix, prefix_ptr); + + let prefix = + Key::parse(prefix).map_err(TxRuntimeError::StorageDataError)?; + + let storage = unsafe { env.ctx.storage.get() }; + let iterators = unsafe { env.ctx.iterators.get() }; + let (iter, gas) = storage.rev_iter_prefix(&prefix); + tx_add_gas(env, gas)?; + Ok(iterators.insert(iter).id()) +} + /// Storage prefix iterator next function exposed to the wasm VM Tx environment. /// It will try to read from the write log first and if no entry found then from /// the storage. @@ -1195,7 +1227,7 @@ where /// Storage prefix iterator function exposed to the wasm VM VP environment. /// It will try to get an iterator from the storage and return the corresponding -/// ID of the iterator. +/// ID of the iterator, ordered by storage keys. pub fn vp_iter_prefix( env: &VpVmEnv, prefix_ptr: u64, @@ -1225,6 +1257,38 @@ where Ok(iterators.insert(iter).id()) } +/// Storage prefix iterator function exposed to the wasm VM VP environment. +/// It will try to get an iterator from the storage and return the corresponding +/// ID of the iterator, reverse ordered by storage keys. +pub fn vp_rev_iter_prefix( + env: &VpVmEnv, + prefix_ptr: u64, + prefix_len: u64, +) -> vp_env::EnvResult +where + MEM: VmMemory, + DB: storage::DB + for<'iter> storage::DBIter<'iter>, + H: StorageHasher, + EVAL: VpEvaluator, + CA: WasmCacheAccess, +{ + let (prefix, gas) = env + .memory + .read_string(prefix_ptr, prefix_len as _) + .map_err(|e| vp_env::RuntimeError::MemoryError(Box::new(e)))?; + let gas_meter = unsafe { env.ctx.gas_meter.get() }; + vp_env::add_gas(gas_meter, gas)?; + + let prefix = + Key::parse(prefix).map_err(vp_env::RuntimeError::StorageDataError)?; + tracing::debug!("vp_rev_iter_prefix {}", prefix); + + let storage = unsafe { env.ctx.storage.get() }; + let iter = vp_env::rev_iter_prefix(gas_meter, storage, &prefix)?; + let iterators = unsafe { env.ctx.iterators.get() }; + Ok(iterators.insert(iter).id()) +} + /// Storage prefix iterator for prior state (before tx execution) function /// exposed to the wasm VM VP environment. It will try to read from the storage. /// diff --git a/shared/src/vm/wasm/host_env.rs b/shared/src/vm/wasm/host_env.rs index 1a50c5533d..06d45fa4f9 100644 --- a/shared/src/vm/wasm/host_env.rs +++ b/shared/src/vm/wasm/host_env.rs @@ -67,6 +67,7 @@ where "anoma_tx_write_temp" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_write_temp), "anoma_tx_delete" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_delete), "anoma_tx_iter_prefix" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_iter_prefix), + "anoma_tx_rev_iter_prefix" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_rev_iter_prefix), "anoma_tx_iter_next" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_iter_next), "anoma_tx_insert_verifier" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_insert_verifier), "anoma_tx_update_validity_predicate" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_update_validity_predicate), @@ -107,6 +108,7 @@ where "anoma_vp_has_key_pre" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_has_key_pre), "anoma_vp_has_key_post" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_has_key_post), "anoma_vp_iter_prefix" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_iter_prefix), + "anoma_vp_rev_iter_prefix" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_rev_iter_prefix), "anoma_vp_iter_pre_next" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_iter_pre_next), "anoma_vp_iter_post_next" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_iter_post_next), "anoma_vp_get_chain_id" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_get_chain_id), diff --git a/tests/src/vm_host_env/tx.rs b/tests/src/vm_host_env/tx.rs index 346cb6bdd4..728c488bca 100644 --- a/tests/src/vm_host_env/tx.rs +++ b/tests/src/vm_host_env/tx.rs @@ -335,6 +335,7 @@ mod native_tx_host_env { )); native_host_fn!(tx_delete(key_ptr: u64, key_len: u64)); native_host_fn!(tx_iter_prefix(prefix_ptr: u64, prefix_len: u64) -> u64); + native_host_fn!(tx_rev_iter_prefix(prefix_ptr: u64, prefix_len: u64) -> u64); native_host_fn!(tx_iter_next(iter_id: u64) -> i64); native_host_fn!(tx_insert_verifier(addr_ptr: u64, addr_len: u64)); native_host_fn!(tx_update_validity_predicate( diff --git a/tests/src/vm_host_env/vp.rs b/tests/src/vm_host_env/vp.rs index d849a11487..88aa63d530 100644 --- a/tests/src/vm_host_env/vp.rs +++ b/tests/src/vm_host_env/vp.rs @@ -326,6 +326,7 @@ mod native_vp_host_env { native_host_fn!(vp_has_key_pre(key_ptr: u64, key_len: u64) -> i64); native_host_fn!(vp_has_key_post(key_ptr: u64, key_len: u64) -> i64); native_host_fn!(vp_iter_prefix(prefix_ptr: u64, prefix_len: u64) -> u64); + native_host_fn!(vp_rev_iter_prefix(prefix_ptr: u64, prefix_len: u64) -> u64); native_host_fn!(vp_iter_pre_next(iter_id: u64) -> i64); native_host_fn!(vp_iter_post_next(iter_id: u64) -> i64); native_host_fn!(vp_get_chain_id(result_ptr: u64)); diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index bd1833ec58..c609f944dd 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -24,7 +24,8 @@ pub use namada::ledger::parameters::storage as parameters_storage; pub use namada::ledger::storage::types::encode; use namada::ledger::storage_api; pub use namada::ledger::storage_api::{ - iter_prefix, iter_prefix_bytes, StorageRead, StorageWrite, + iter_prefix, iter_prefix_bytes, rev_iter_prefix, rev_iter_prefix_bytes, + StorageRead, StorageWrite, }; pub use namada::ledger::treasury::storage as treasury_storage; pub use namada::ledger::tx_env::TxEnv; @@ -178,6 +179,17 @@ impl StorageRead<'_> for Ctx { Ok(KeyValIterator(iter_id, PhantomData)) } + fn rev_iter_prefix( + &self, + prefix: &storage::Key, + ) -> storage_api::Result { + let prefix = prefix.to_string(); + let iter_id = unsafe { + anoma_tx_rev_iter_prefix(prefix.as_ptr() as _, prefix.len() as _) + }; + Ok(KeyValIterator(iter_id, PhantomData)) + } + fn iter_next( &self, iter: &mut Self::PrefixIter, diff --git a/vm_env/src/lib.rs b/vm_env/src/lib.rs index 53c594dbab..1421dbde48 100644 --- a/vm_env/src/lib.rs +++ b/vm_env/src/lib.rs @@ -48,9 +48,17 @@ pub mod tx { // Delete the given key and its value pub fn anoma_tx_delete(key_ptr: u64, key_len: u64); - // Get an ID of a data iterator with key prefix + // Get an ID of a data iterator with key prefix, ordered by storage + // keys. pub fn anoma_tx_iter_prefix(prefix_ptr: u64, prefix_len: u64) -> u64; + // Get an ID of a data iterator with key prefix, reverse ordered by + // storage keys. + pub fn anoma_tx_rev_iter_prefix( + prefix_ptr: u64, + prefix_len: u64, + ) -> u64; + // Returns the size of the value (can be 0), or -1 if there's no next // value. If a value is found, it will be placed in the read // cache, because we cannot allocate a buffer for it before we know @@ -133,9 +141,17 @@ pub mod vp { // Returns 1 if the key is present in posterior state, -1 otherwise. pub fn anoma_vp_has_key_post(key_ptr: u64, key_len: u64) -> i64; - // Get an ID of a data iterator with key prefix + // Get an ID of a data iterator with key prefix, ordered by storage + // keys. pub fn anoma_vp_iter_prefix(prefix_ptr: u64, prefix_len: u64) -> u64; + // Get an ID of a data iterator with key prefix, reverse ordered by + // storage keys. + pub fn anoma_vp_rev_iter_prefix( + prefix_ptr: u64, + prefix_len: u64, + ) -> u64; + // Read variable-length prior state when we don't know the size // up-front, returns the size of the value (can be 0), or -1 if // the key is not present. If a value is found, it will be placed in the diff --git a/vp_prelude/src/lib.rs b/vp_prelude/src/lib.rs index 3ee761f78f..862071084a 100644 --- a/vp_prelude/src/lib.rs +++ b/vp_prelude/src/lib.rs @@ -23,7 +23,8 @@ pub use borsh::{BorshDeserialize, BorshSerialize}; pub use error::*; pub use namada::ledger::governance::storage as gov_storage; pub use namada::ledger::storage_api::{ - self, iter_prefix, iter_prefix_bytes, StorageRead, + self, iter_prefix, iter_prefix_bytes, rev_iter_prefix, + rev_iter_prefix_bytes, StorageRead, }; pub use namada::ledger::vp_env::VpEnv; pub use namada::ledger::{parameters, pos as proof_of_stake}; @@ -238,6 +239,14 @@ impl VpEnv for Ctx { self.pre().iter_prefix(prefix).into_env_result() } + fn rev_iter_prefix( + &self, + prefix: &storage::Key, + ) -> Result { + // Both `CtxPreStorageRead` and `CtxPostStorageRead` have the same impl + self.pre().rev_iter_prefix(prefix).into_env_result() + } + fn iter_pre_next( &self, iter: &mut Self::PrefixIter, @@ -339,6 +348,14 @@ impl StorageRead<'_> for CtxPreStorageRead<'_> { iter_prefix_impl(prefix) } + fn rev_iter_prefix( + &self, + prefix: &storage::Key, + ) -> storage_api::Result { + // Note that this is the same as `CtxPostStorageRead` + rev_iter_prefix_impl(prefix) + } + fn iter_next( &self, iter: &mut Self::PrefixIter, @@ -416,6 +433,14 @@ impl StorageRead<'_> for CtxPostStorageRead<'_> { iter_prefix_impl(prefix) } + fn rev_iter_prefix( + &self, + prefix: &storage::Key, + ) -> storage_api::Result { + // Note that this is the same as `CtxPreStorageRead` + rev_iter_prefix_impl(prefix) + } + fn iter_next( &self, iter: &mut Self::PrefixIter, @@ -454,6 +479,16 @@ fn iter_prefix_impl( Ok(KeyValIterator(iter_id, PhantomData)) } +fn rev_iter_prefix_impl( + prefix: &storage::Key, +) -> Result)>, storage_api::Error> { + let prefix = prefix.to_string(); + let iter_id = unsafe { + anoma_vp_rev_iter_prefix(prefix.as_ptr() as _, prefix.len() as _) + }; + Ok(KeyValIterator(iter_id, PhantomData)) +} + fn get_chain_id() -> Result { let result = Vec::with_capacity(CHAIN_ID_LENGTH); unsafe { From 881c93cbd9406bf8f48062f6fafbbb3750f91e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 8 Sep 2022 18:13:13 +0200 Subject: [PATCH 44/81] tests: extend prefix iter tests for reverse order --- tests/src/vm_host_env/mod.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index f846325c40..e585ab7924 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -182,6 +182,19 @@ mod tests { .sorted() .map(|i| (prefix.push(i).unwrap(), *i)); itertools::assert_equal(iter, expected); + + // Try to iterate over their prefix in reverse + let iter = namada_tx_prelude::rev_iter_prefix(tx::ctx(), &prefix) + .unwrap() + .map(Result::unwrap); + + // The order has to be reverse sorted by sub-key value + let expected = sub_keys + .iter() + .sorted() + .rev() + .map(|i| (prefix.push(i).unwrap(), *i)); + itertools::assert_equal(iter, expected); } #[test] @@ -411,6 +424,19 @@ mod tests { (prefix.push(i).unwrap(), val) }); itertools::assert_equal(iter_post, expected_post); + + // Try to iterate over their prefix in reverse + let iter_pre = namada_vp_prelude::rev_iter_prefix(&ctx_pre, &prefix) + .unwrap() + .map(|item| item.unwrap()); + + // The order in has to be reverse sorted by sub-key value + let expected_pre = sub_keys + .iter() + .sorted() + .rev() + .map(|i| (prefix.push(i).unwrap(), *i)); + itertools::assert_equal(iter_pre, expected_pre); } #[test] From d786a4aa979195ad65d381ac0ab61119db2f9712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 8 Sep 2022 18:16:51 +0200 Subject: [PATCH 45/81] changelog: add #458 --- .changelog/unreleased/improvements/409-sorted-prefix-iter.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/unreleased/improvements/409-sorted-prefix-iter.md diff --git a/.changelog/unreleased/improvements/409-sorted-prefix-iter.md b/.changelog/unreleased/improvements/409-sorted-prefix-iter.md new file mode 100644 index 0000000000..2f95505960 --- /dev/null +++ b/.changelog/unreleased/improvements/409-sorted-prefix-iter.md @@ -0,0 +1,3 @@ +- Fix order of prefix iterator to be sorted by storage + keys and add support for a reverse order prefix iterator. + ([#409](https://github.com/anoma/namada/issues/409)) \ No newline at end of file From 1d2f1dd15364890a6001139d592f4cda1a396f80 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 9 Sep 2022 05:57:59 +0000 Subject: [PATCH 46/81] [ci skip] wasm checksums update --- wasm/checksums.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index 0c7b7cf504..f5295c8f1a 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,19 +1,19 @@ { - "tx_bond.wasm": "tx_bond.d80c5ac518c223eea92a51eeb033462e9ea4498530d12de5b6e6ca6b3fa59ea8.wasm", - "tx_from_intent.wasm": "tx_from_intent.49565974c6e2c3d64a05729bd57217b244d804fc9f4e5369d1f3515aeaa8397f.wasm", - "tx_ibc.wasm": "tx_ibc.0d9d639037a8dc54c53ecbedc8396ea103ee7c8f485790ddf7223f4e7e6a9779.wasm", - "tx_init_account.wasm": "tx_init_account.97bfee0b78c87abc217c58351f1d6242990a06c7379b27f948950f04f36b49a2.wasm", - "tx_init_nft.wasm": "tx_init_nft.6207eabda37cd356b6093d069f388bd84463b5e1b8860811a36a9b63da84951f.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.1073d69f69172276c61c104f7b4678019723df19ddb30aedf6293a00de973846.wasm", - "tx_init_validator.wasm": "tx_init_validator.40f0152c1bd59f46ec26123d98d0b49a0a458335b6012818cf8504b7708bf625.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.f1c8039a6fb5e01e7441cfa2e3fb2f84a1cb60ed660745ddc172d0d01f1b0ab1.wasm", - "tx_transfer.wasm": "tx_transfer.191d28c340900e6dc297e66b054cd83b690ae0357a8e13b37962b265b2e17da8.wasm", - "tx_unbond.wasm": "tx_unbond.ea73369f68abef405c4f7a3a09c3da6aa68493108d48b1e4e243d26766f00283.wasm", - "tx_update_vp.wasm": "tx_update_vp.a304b3c70361cde5fda458ba5637d6825eb002198e73990a1c74351113b83d43.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.5e51a66746df8c7e786cc6837a74f87d239442ace67d1c4ef4e6751aa725514f.wasm", - "tx_withdraw.wasm": "tx_withdraw.f776265133f972e6705797561b6bb37f9e21de07f3611b23cfdd6e39cb252c0f.wasm", - "vp_nft.wasm": "vp_nft.1a32d37b32c616119d156128560a0eeb43206e91069e2c3875e74211ed3ad01f.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.c30baa6d2dab2c336a6b2485e3ccf41cd548b135ca5245b80fab15858c70365c.wasm", - "vp_token.wasm": "vp_token.9f27c2e823691487bb6631fbfecb39837c1f404ef9b263e810a5a2a76f4c5957.wasm", - "vp_user.wasm": "vp_user.59b7a9262c951ff451d3aec0604ae3dd0fc4729f8d994c114be6320ce5d38712.wasm" + "tx_bond.wasm": "tx_bond.bef50a074940c1b8cd788b93376162553bc11ec3521ffbbc0e8913ae27edfac1.wasm", + "tx_from_intent.wasm": "tx_from_intent.41b56839c7b797407b71a9b44b1b58b0b9f98069ccfbbdba1d5ce9501a78ddea.wasm", + "tx_ibc.wasm": "tx_ibc.b0ab86c7b67a612ed559b57e2277e307139223f3fed245b62e6d0524e5785cad.wasm", + "tx_init_account.wasm": "tx_init_account.913c7218e5f24ee4d86b89d9020624e54340bd597278b0596baba721a2944e70.wasm", + "tx_init_nft.wasm": "tx_init_nft.2f2c198643996a99ef0621f7a1c5ad824e176258f8f5755209378cf5b7770f64.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.d4332ff996775b3e243afacdf311be7e0b3f32dc3c15edfa89d0ce9ee6b6bb6b.wasm", + "tx_init_validator.wasm": "tx_init_validator.5f6b566d549ab182874688a635f1c2900605d7a7a8d6850fa4c084ce5277d212.wasm", + "tx_mint_nft.wasm": "tx_mint_nft.19a9c2b62ff8301979cb038e2ad26fcf8f67c089ec22cb915e1c17e4f95adf1e.wasm", + "tx_transfer.wasm": "tx_transfer.c90e697f22ce49d30e9f8120f5e37452bcd8bf5b7d0f65426da7283a9c141ab9.wasm", + "tx_unbond.wasm": "tx_unbond.f01fea78e3225ae0029b99a735f72e9309a9e68c80bf8cd3e620456898edd49c.wasm", + "tx_update_vp.wasm": "tx_update_vp.f8eef21b09d932da44babf0faf2a4137fa075d952ea92bb0b652ce141475ad42.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.fb2f1b95798704a8df8860c18777e59ad8bf0b69e31f928f1bb5ed0c8c0eed55.wasm", + "tx_withdraw.wasm": "tx_withdraw.843be20010c36500170b231f10fdf80c4486b7afef81217c4f0ac2600229db67.wasm", + "vp_nft.wasm": "vp_nft.10393706d5d717a17285fc16128fee8703f2a0b07bf952e275c8836b7df4b1fd.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.f86a33e5e023a07f420f4d44a01f0c686e84989790a9c47c718cb83b6fce65fb.wasm", + "vp_token.wasm": "vp_token.b2d86a59d1024992ade25ae2a513abdddd41a3f13f33fff0eea35bec0e5faca0.wasm", + "vp_user.wasm": "vp_user.f66dae6078241f8a9a564bf58928bf9f005a5ee1318e245850c8cbfb45474a5c.wasm" } \ No newline at end of file From a700e4902a177408f8945f1014e557a3d62b84c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 13 Sep 2022 15:31:10 +0200 Subject: [PATCH 47/81] ledger: use storage_api::Error in VpEnv and TxEnv instead of generic This inverts the wrapping of errors, storage_api::Error now wraps NativeVp:Error and some other IBC and PoS custom errors --- shared/src/ledger/ibc/handler.rs | 9 ++ shared/src/ledger/native_vp.rs | 55 ++++++----- shared/src/ledger/pos/mod.rs | 36 ++++++- shared/src/ledger/pos/vp.rs | 12 +-- shared/src/ledger/storage_api/error.rs | 23 +++++ shared/src/ledger/storage_api/mod.rs | 2 +- shared/src/ledger/tx_env.rs | 25 ++--- shared/src/ledger/vp_env.rs | 67 ++++++------- tx_prelude/src/error.rs | 112 ---------------------- tx_prelude/src/ibc.rs | 7 -- tx_prelude/src/lib.rs | 30 +++--- tx_prelude/src/proof_of_stake.rs | 47 ++------- vp_prelude/src/error.rs | 110 --------------------- vp_prelude/src/lib.rs | 88 ++++++++--------- wasm/wasm_source/src/tx_bond.rs | 4 +- wasm/wasm_source/src/tx_from_intent.rs | 4 +- wasm/wasm_source/src/tx_ibc.rs | 2 +- wasm/wasm_source/src/tx_init_account.rs | 4 +- wasm/wasm_source/src/tx_init_nft.rs | 4 +- wasm/wasm_source/src/tx_init_proposal.rs | 4 +- wasm/wasm_source/src/tx_init_validator.rs | 4 +- wasm/wasm_source/src/tx_mint_nft.rs | 4 +- wasm/wasm_source/src/tx_transfer.rs | 4 +- wasm/wasm_source/src/tx_unbond.rs | 4 +- wasm/wasm_source/src/tx_update_vp.rs | 4 +- wasm/wasm_source/src/tx_vote_proposal.rs | 4 +- wasm/wasm_source/src/tx_withdraw.rs | 4 +- wasm_for_tests/wasm_source/src/lib.rs | 4 +- 28 files changed, 237 insertions(+), 440 deletions(-) delete mode 100644 tx_prelude/src/error.rs delete mode 100644 vp_prelude/src/error.rs diff --git a/shared/src/ledger/ibc/handler.rs b/shared/src/ledger/ibc/handler.rs index 5cbee20756..4a3fe528a9 100644 --- a/shared/src/ledger/ibc/handler.rs +++ b/shared/src/ledger/ibc/handler.rs @@ -70,6 +70,7 @@ use crate::ibc::events::IbcEvent; use crate::ibc::mock::client_state::{MockClientState, MockConsensusState}; use crate::ibc::timestamp::Timestamp; use crate::ledger::ibc::storage; +use crate::ledger::storage_api; use crate::tendermint::Time; use crate::tendermint_proto::{Error as ProtoError, Protobuf}; use crate::types::address::{Address, InternalAddress}; @@ -117,6 +118,14 @@ pub enum Error { ReceivingToken(String), } +// This is needed to use `ibc::Handler::Error` with `IbcActions` in +// `tx_prelude/src/ibc.rs` +impl From for storage_api::Error { + fn from(err: Error) -> Self { + storage_api::Error::new(err) + } +} + /// for handling IBC modules pub type Result = std::result::Result; diff --git a/shared/src/ledger/native_vp.rs b/shared/src/ledger/native_vp.rs index f17b339086..b98794d12c 100644 --- a/shared/src/ledger/native_vp.rs +++ b/shared/src/ledger/native_vp.rs @@ -17,7 +17,9 @@ use crate::vm::prefix_iter::PrefixIterators; use crate::vm::WasmCacheAccess; /// Possible error in a native VP host function call -pub type Error = vp_env::RuntimeError; +/// The `storage_api::Error` may wrap the `vp_env::RuntimeError` and can +/// be extended with other custom errors when using `trait VpEnv`. +pub type Error = storage_api::Error; /// A native VP module should implement its validation logic using this trait. pub trait NativeVp { @@ -201,23 +203,23 @@ where &self, prefix: &crate::types::storage::Key, ) -> Result { - self.ctx.iter_prefix(prefix).into_storage_result() + self.ctx.iter_prefix(prefix) } fn get_chain_id(&self) -> Result { - self.ctx.get_chain_id().into_storage_result() + self.ctx.get_chain_id() } fn get_block_height(&self) -> Result { - self.ctx.get_block_height().into_storage_result() + self.ctx.get_block_height() } fn get_block_hash(&self) -> Result { - self.ctx.get_block_hash().into_storage_result() + self.ctx.get_block_hash() } fn get_block_epoch(&self) -> Result { - self.ctx.get_block_epoch().into_storage_result() + self.ctx.get_block_epoch() } } @@ -275,23 +277,23 @@ where &self, prefix: &crate::types::storage::Key, ) -> Result { - self.ctx.iter_prefix(prefix).into_storage_result() + self.ctx.iter_prefix(prefix) } fn get_chain_id(&self) -> Result { - self.ctx.get_chain_id().into_storage_result() + self.ctx.get_chain_id() } fn get_block_height(&self) -> Result { - self.ctx.get_block_height().into_storage_result() + self.ctx.get_block_height() } fn get_block_hash(&self) -> Result { - self.ctx.get_block_hash().into_storage_result() + self.ctx.get_block_hash() } fn get_block_epoch(&self) -> Result { - self.ctx.get_block_epoch().into_storage_result() + self.ctx.get_block_epoch() } } @@ -301,7 +303,6 @@ where H: 'static + StorageHasher, CA: 'static + WasmCacheAccess, { - type Error = Error; type Post = CtxPostStorageRead<'view, 'a, DB, H, CA>; type Pre = CtxPreStorageRead<'view, 'a, DB, H, CA>; type PrefixIter = >::PrefixIter; @@ -317,61 +318,70 @@ where fn read_temp( &self, key: &Key, - ) -> Result, Self::Error> { + ) -> Result, storage_api::Error> { vp_env::read_temp( &mut *self.gas_meter.borrow_mut(), self.write_log, key, ) .map(|data| data.and_then(|t| T::try_from_slice(&t[..]).ok())) + .into_storage_result() } fn read_bytes_temp( &self, key: &Key, - ) -> Result>, Self::Error> { + ) -> Result>, storage_api::Error> { vp_env::read_temp( &mut *self.gas_meter.borrow_mut(), self.write_log, key, ) + .into_storage_result() } - fn get_chain_id(&'view self) -> Result { + fn get_chain_id(&'view self) -> Result { vp_env::get_chain_id(&mut *self.gas_meter.borrow_mut(), self.storage) + .into_storage_result() } - fn get_block_height(&'view self) -> Result { + fn get_block_height( + &'view self, + ) -> Result { vp_env::get_block_height( &mut *self.gas_meter.borrow_mut(), self.storage, ) + .into_storage_result() } - fn get_block_hash(&'view self) -> Result { + fn get_block_hash(&'view self) -> Result { vp_env::get_block_hash(&mut *self.gas_meter.borrow_mut(), self.storage) + .into_storage_result() } - fn get_block_epoch(&'view self) -> Result { + fn get_block_epoch(&'view self) -> Result { vp_env::get_block_epoch(&mut *self.gas_meter.borrow_mut(), self.storage) + .into_storage_result() } fn iter_prefix( &'view self, prefix: &Key, - ) -> Result { + ) -> Result { vp_env::iter_prefix( &mut *self.gas_meter.borrow_mut(), self.storage, prefix, ) + .into_storage_result() } fn eval( &self, vp_code: Vec, input_data: Vec, - ) -> Result { + ) -> Result { #[cfg(feature = "wasm-runtime")] { use std::marker::PhantomData; @@ -429,11 +439,12 @@ where &self, pk: &crate::types::key::common::PublicKey, sig: &crate::types::key::common::Signature, - ) -> Result { + ) -> Result { Ok(self.tx.verify_sig(pk, sig).is_ok()) } - fn get_tx_code_hash(&self) -> Result { + fn get_tx_code_hash(&self) -> Result { vp_env::get_tx_code_hash(&mut *self.gas_meter.borrow_mut(), self.tx) + .into_storage_result() } } diff --git a/shared/src/ledger/pos/mod.rs b/shared/src/ledger/pos/mod.rs index a9d72e84bb..3b498727df 100644 --- a/shared/src/ledger/pos/mod.rs +++ b/shared/src/ledger/pos/mod.rs @@ -13,6 +13,7 @@ use namada_proof_of_stake::PosBase; pub use storage::*; pub use vp::PosVP; +use super::storage_api; use crate::ledger::storage::{self as ledger_storage, Storage, StorageHasher}; use crate::types::address::{self, Address, InternalAddress}; use crate::types::storage::Epoch; @@ -88,6 +89,40 @@ impl From for Epoch { } } +// The error conversions are needed to implement `PosActions` in +// `tx_prelude/src/proof_of_stake.rs` +impl From> + for storage_api::Error +{ + fn from(err: namada_proof_of_stake::BecomeValidatorError
) -> Self { + Self::new(err) + } +} + +impl From> for storage_api::Error { + fn from(err: namada_proof_of_stake::BondError
) -> Self { + Self::new(err) + } +} + +impl From> + for storage_api::Error +{ + fn from( + err: namada_proof_of_stake::UnbondError, + ) -> Self { + Self::new(err) + } +} + +impl From> + for storage_api::Error +{ + fn from(err: namada_proof_of_stake::WithdrawError
) -> Self { + Self::new(err) + } +} + #[macro_use] mod macros { /// Implement `PosReadOnly` for a type that implements @@ -115,7 +150,6 @@ mod macros { $( $any )* { type Address = $crate::types::address::Address; - // type Error = $crate::ledger::native_vp::Error; type $error = $err_ty; type PublicKey = $crate::types::key::common::PublicKey; type TokenAmount = $crate::types::token::Amount; diff --git a/shared/src/ledger/pos/vp.rs b/shared/src/ledger/pos/vp.rs index 2f19b6d1a4..0551c5de7f 100644 --- a/shared/src/ledger/pos/vp.rs +++ b/shared/src/ledger/pos/vp.rs @@ -47,8 +47,6 @@ use crate::vm::WasmCacheAccess; pub enum Error { #[error("Native VP error: {0}")] NativeVpError(native_vp::Error), - #[error("Storage error: {0}")] - StorageApi(storage_api::Error), } /// PoS functions result @@ -324,7 +322,7 @@ where } impl_pos_read_only! { - type Error = native_vp::Error; + type Error = storage_api::Error; impl<'f, 'a, DB, H, CA> PosReadOnly for CtxPreStorageRead<'f, 'a, DB, H, CA> where DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter> +'static, @@ -333,7 +331,7 @@ impl_pos_read_only! { } impl_pos_read_only! { - type Error = native_vp::Error; + type Error = storage_api::Error; impl<'f, 'a, DB, H, CA> PosReadOnly for CtxPostStorageRead<'f, 'a, DB, H, CA> where DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter> +'static, @@ -346,9 +344,3 @@ impl From for Error { Self::NativeVpError(err) } } - -impl From for Error { - fn from(err: storage_api::Error) -> Self { - Self::StorageApi(err) - } -} diff --git a/shared/src/ledger/storage_api/error.rs b/shared/src/ledger/storage_api/error.rs index d01fbfd287..8af95be723 100644 --- a/shared/src/ledger/storage_api/error.rs +++ b/shared/src/ledger/storage_api/error.rs @@ -6,6 +6,8 @@ use thiserror::Error; #[allow(missing_docs)] #[derive(Error, Debug)] pub enum Error { + #[error("{0}")] + SimpleMessage(&'static str), #[error("{0}")] Custom(CustomError), #[error("{0}: {1}")] @@ -48,6 +50,12 @@ impl Error { Self::Custom(CustomError(error.into())) } + /// Create an [`enum@Error`] from a static message. + #[inline] + pub const fn new_const(msg: &'static str) -> Self { + Self::SimpleMessage(msg) + } + /// Wrap another [`std::error::Error`] with a static message. pub fn wrap(msg: &'static str, error: E) -> Self where @@ -66,3 +74,18 @@ impl std::fmt::Display for CustomError { self.0.fmt(f) } } + +/// An extension to [`Option`] to allow turning `None` case to an Error from a +/// static string (handy for WASM). +pub trait OptionExt { + /// Transforms the [`Option`] into a [`Result`], mapping + /// [`Some(v)`] to [`Ok(v)`] and [`None`] to the given static error + /// message. + fn ok_or_err_msg(self, msg: &'static str) -> Result; +} + +impl OptionExt for Option { + fn ok_or_err_msg(self, msg: &'static str) -> Result { + self.ok_or_else(|| Error::new_const(msg)) + } +} diff --git a/shared/src/ledger/storage_api/mod.rs b/shared/src/ledger/storage_api/mod.rs index 94be4d8568..5dfec7e76e 100644 --- a/shared/src/ledger/storage_api/mod.rs +++ b/shared/src/ledger/storage_api/mod.rs @@ -4,7 +4,7 @@ mod error; use borsh::{BorshDeserialize, BorshSerialize}; -pub use error::{CustomError, Error, Result, ResultExt}; +pub use error::{CustomError, Error, OptionExt, Result, ResultExt}; use crate::types::storage::{self, BlockHash, BlockHeight, Epoch}; diff --git a/shared/src/ledger/tx_env.rs b/shared/src/ledger/tx_env.rs index 1db8fad09b..7672ac6505 100644 --- a/shared/src/ledger/tx_env.rs +++ b/shared/src/ledger/tx_env.rs @@ -3,7 +3,7 @@ use borsh::BorshSerialize; -use crate::ledger::storage_api::{StorageRead, StorageWrite}; +use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; use crate::types::address::Address; use crate::types::ibc::IbcEvent; use crate::types::storage; @@ -11,23 +11,20 @@ use crate::types::time::Rfc3339String; /// Transaction host functions pub trait TxEnv<'iter>: StorageRead<'iter> + StorageWrite { - /// Host env functions possible errors - type Error; - /// Write a temporary value to be encoded with Borsh at the given key to /// storage. fn write_temp( &mut self, key: &storage::Key, val: T, - ) -> Result<(), Self::Error>; + ) -> Result<(), storage_api::Error>; /// Write a temporary value as bytes at the given key to storage. fn write_bytes_temp( &mut self, key: &storage::Key, val: impl AsRef<[u8]>, - ) -> Result<(), Self::Error>; + ) -> Result<(), storage_api::Error>; /// Insert a verifier address. This address must exist on chain, otherwise /// the transaction will be rejected. @@ -35,26 +32,32 @@ pub trait TxEnv<'iter>: StorageRead<'iter> + StorageWrite { /// Validity predicates of each verifier addresses inserted in the /// transaction will validate the transaction and will receive all the /// changed storage keys and initialized accounts in their inputs. - fn insert_verifier(&mut self, addr: &Address) -> Result<(), Self::Error>; + fn insert_verifier( + &mut self, + addr: &Address, + ) -> Result<(), storage_api::Error>; /// Initialize a new account generates a new established address and /// writes the given code as its validity predicate into the storage. fn init_account( &mut self, code: impl AsRef<[u8]>, - ) -> Result; + ) -> Result; /// Update a validity predicate fn update_validity_predicate( &mut self, addr: &Address, code: impl AsRef<[u8]>, - ) -> Result<(), Self::Error>; + ) -> Result<(), storage_api::Error>; /// Emit an IBC event. There can be only one event per transaction. On /// multiple calls, only the last emitted event will be used. - fn emit_ibc_event(&mut self, event: &IbcEvent) -> Result<(), Self::Error>; + fn emit_ibc_event( + &mut self, + event: &IbcEvent, + ) -> Result<(), storage_api::Error>; /// Get time of the current block header as rfc 3339 string - fn get_block_time(&self) -> Result; + fn get_block_time(&self) -> Result; } diff --git a/shared/src/ledger/vp_env.rs b/shared/src/ledger/vp_env.rs index e1f72d8505..8398f37cda 100644 --- a/shared/src/ledger/vp_env.rs +++ b/shared/src/ledger/vp_env.rs @@ -22,9 +22,6 @@ pub trait VpEnv<'view> { /// Storage read prefix iterator type PrefixIter; - /// Host functions possible errors, extensible with custom user errors. - type Error: From; - /// Type to read storage state before the transaction execution type Pre: StorageRead<'view, PrefixIter = Self::PrefixIter>; @@ -43,36 +40,37 @@ pub trait VpEnv<'view> { fn read_temp( &self, key: &Key, - ) -> Result, Self::Error>; + ) -> Result, storage_api::Error>; /// Storage read temporary state raw bytes (after tx execution). It will try /// to read from only the write log. fn read_bytes_temp( &self, key: &Key, - ) -> Result>, Self::Error>; + ) -> Result>, storage_api::Error>; /// Getting the chain ID. - fn get_chain_id(&'view self) -> Result; + fn get_chain_id(&'view self) -> Result; /// Getting the block height. The height is that of the block to which the /// current transaction is being applied. - fn get_block_height(&'view self) -> Result; + fn get_block_height(&'view self) + -> Result; /// Getting the block hash. The height is that of the block to which the /// current transaction is being applied. - fn get_block_hash(&'view self) -> Result; + fn get_block_hash(&'view self) -> Result; /// Getting the block epoch. The epoch is that of the block to which the /// current transaction is being applied. - fn get_block_epoch(&'view self) -> Result; + fn get_block_epoch(&'view self) -> Result; /// Storage prefix iterator. It will try to get an iterator from the /// storage. fn iter_prefix( &'view self, prefix: &Key, - ) -> Result; + ) -> Result; /// Evaluate a validity predicate with given data. The address, changed /// storage keys and verifiers will have the same values as the input to @@ -84,7 +82,7 @@ pub trait VpEnv<'view> { &self, vp_code: Vec, input_data: Vec, - ) -> Result; + ) -> Result; /// Verify a transaction signature. The signature is expected to have been /// produced on the encoded transaction [`crate::proto::Tx`] @@ -93,10 +91,10 @@ pub trait VpEnv<'view> { &self, pk: &common::PublicKey, sig: &common::Signature, - ) -> Result; + ) -> Result; /// Get a tx hash - fn get_tx_code_hash(&self) -> Result; + fn get_tx_code_hash(&self) -> Result; // ---- Methods below have default implementation via `pre/post` ---- @@ -105,8 +103,8 @@ pub trait VpEnv<'view> { fn read_pre( &'view self, key: &Key, - ) -> Result, Self::Error> { - self.pre().read(key).map_err(Into::into) + ) -> Result, storage_api::Error> { + self.pre().read(key) } /// Storage read prior state raw bytes (before tx execution). It @@ -114,8 +112,8 @@ pub trait VpEnv<'view> { fn read_bytes_pre( &'view self, key: &Key, - ) -> Result>, Self::Error> { - self.pre().read_bytes(key).map_err(Into::into) + ) -> Result>, storage_api::Error> { + self.pre().read_bytes(key) } /// Storage read posterior state Borsh encoded value (after tx execution). @@ -124,8 +122,8 @@ pub trait VpEnv<'view> { fn read_post( &'view self, key: &Key, - ) -> Result, Self::Error> { - self.post().read(key).map_err(Into::into) + ) -> Result, storage_api::Error> { + self.post().read(key) } /// Storage read posterior state raw bytes (after tx execution). It will try @@ -134,20 +132,23 @@ pub trait VpEnv<'view> { fn read_bytes_post( &'view self, key: &Key, - ) -> Result>, Self::Error> { - self.post().read_bytes(key).map_err(Into::into) + ) -> Result>, storage_api::Error> { + self.post().read_bytes(key) } /// Storage `has_key` in prior state (before tx execution). It will try to /// read from the storage. - fn has_key_pre(&'view self, key: &Key) -> Result { - self.pre().has_key(key).map_err(Into::into) + fn has_key_pre(&'view self, key: &Key) -> Result { + self.pre().has_key(key) } /// Storage `has_key` in posterior state (after tx execution). It will try /// to check the write log first and if no entry found then the storage. - fn has_key_post(&'view self, key: &Key) -> Result { - self.post().has_key(key).map_err(Into::into) + fn has_key_post( + &'view self, + key: &Key, + ) -> Result { + self.post().has_key(key) } /// Storage prefix iterator for prior state (before tx execution). It will @@ -155,8 +156,8 @@ pub trait VpEnv<'view> { fn iter_pre_next( &'view self, iter: &mut Self::PrefixIter, - ) -> Result)>, Self::Error> { - self.pre().iter_next(iter).map_err(Into::into) + ) -> Result)>, storage_api::Error> { + self.pre().iter_next(iter) } /// Storage prefix iterator next for posterior state (after tx execution). @@ -165,8 +166,8 @@ pub trait VpEnv<'view> { fn iter_post_next( &'view self, iter: &mut Self::PrefixIter, - ) -> Result)>, Self::Error> { - self.post().iter_next(iter).map_err(Into::into) + ) -> Result)>, storage_api::Error> { + self.post().iter_next(iter) } } @@ -190,8 +191,6 @@ pub enum RuntimeError { ReadTemporaryValueError, #[error("Trying to read a permament value with read_temp")] ReadPermanentValueError, - #[error("Storage error: {0}")] - StorageApi(storage_api::Error), } /// VP environment function result @@ -495,9 +494,3 @@ where } Ok(None) } - -impl From for RuntimeError { - fn from(err: storage_api::Error) -> Self { - Self::StorageApi(err) - } -} diff --git a/tx_prelude/src/error.rs b/tx_prelude/src/error.rs deleted file mode 100644 index ce7b9fa5e9..0000000000 --- a/tx_prelude/src/error.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Helpers for error handling in WASM -//! -//! This module is currently duplicated in tx_prelude and vp_prelude crates to -//! be able to implement `From` conversion on error types from other crates, -//! avoiding `error[E0117]: only traits defined in the current crate can be -//! implemented for arbitrary types` - -use namada::ledger::storage_api; -use thiserror::Error; - -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - SimpleMessage(&'static str), - #[error("{0}")] - Custom(CustomError), - #[error("{0}: {1}")] - CustomWithMessage(&'static str, CustomError), -} - -/// Result of transaction or VP. -pub type EnvResult = Result; - -pub trait ResultExt { - /// Replace a possible error with a static message in [`EnvResult`]. - fn err_msg(self, msg: &'static str) -> EnvResult; -} - -// This is separate from `ResultExt`, because the implementation requires -// different bounds for `T`. -pub trait ResultExt2 { - /// Convert a [`Result`] into [`EnvResult`]. - fn into_env_result(self) -> EnvResult; - - /// Add a static message to a possible error in [`EnvResult`]. - fn wrap_err(self, msg: &'static str) -> EnvResult; -} - -pub trait OptionExt { - /// Transforms the [`Option`] into a [`EnvResult`], mapping - /// [`Some(v)`] to [`Ok(v)`] and [`None`] to the given static error - /// message. - fn ok_or_err_msg(self, msg: &'static str) -> EnvResult; -} - -impl ResultExt for Result { - fn err_msg(self, msg: &'static str) -> EnvResult { - self.map_err(|_err| Error::new_const(msg)) - } -} - -impl ResultExt2 for Result -where - E: std::error::Error + Send + Sync + 'static, -{ - fn into_env_result(self) -> EnvResult { - self.map_err(Error::new) - } - - fn wrap_err(self, msg: &'static str) -> EnvResult { - self.map_err(|err| Error::wrap(msg, err)) - } -} - -impl OptionExt for Option { - fn ok_or_err_msg(self, msg: &'static str) -> EnvResult { - self.ok_or_else(|| Error::new_const(msg)) - } -} - -impl Error { - /// Create an [`enum@Error`] from a static message. - #[inline] - pub const fn new_const(msg: &'static str) -> Self { - Self::SimpleMessage(msg) - } - - /// Create an [`enum@Error`] from another [`std::error::Error`]. - pub fn new(error: E) -> Self - where - E: Into>, - { - Self::Custom(CustomError(error.into())) - } - - /// Wrap another [`std::error::Error`] with a static message. - pub fn wrap(msg: &'static str, error: E) -> Self - where - E: Into>, - { - Self::CustomWithMessage(msg, CustomError(error.into())) - } -} - -/// A custom error -#[derive(Debug)] -pub struct CustomError(Box); - -impl std::fmt::Display for CustomError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl From for Error { - fn from(err: storage_api::Error) -> Self { - // storage_api::Error::Custom(CustomError {Box}) - // Error:Custom(storage_api::Error::Custom(CustomError {Box})) - Self::new(err) - } -} diff --git a/tx_prelude/src/ibc.rs b/tx_prelude/src/ibc.rs index 4a8a3ef3a9..494d5e7cd3 100644 --- a/tx_prelude/src/ibc.rs +++ b/tx_prelude/src/ibc.rs @@ -12,13 +12,6 @@ use namada::types::token::Amount; use crate::token::transfer; use crate::Ctx; -// This is needed to use `ibc::Handler::Error` with `IbcActions` below -impl From for crate::Error { - fn from(err: Error) -> Self { - crate::Error::new(err) - } -} - impl IbcActions for Ctx { type Error = crate::Error; diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index ade290bf65..a18d59c9a8 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)] -mod error; pub mod governance; pub mod ibc; pub mod intent; @@ -18,12 +17,13 @@ use core::slice; use std::marker::PhantomData; pub use borsh::{BorshDeserialize, BorshSerialize}; -pub use error::*; pub use namada::ledger::governance::storage as gov_storage; pub use namada::ledger::parameters::storage as parameters_storage; pub use namada::ledger::storage::types::encode; use namada::ledger::storage_api; -pub use namada::ledger::storage_api::{StorageRead, StorageWrite}; +pub use namada::ledger::storage_api::{ + Error, OptionExt, ResultExt, StorageRead, StorageWrite, +}; pub use namada::ledger::treasury::storage as treasury_storage; pub use namada::ledger::tx_env::TxEnv; pub use namada::proto::{Signed, SignedTxData}; @@ -89,6 +89,10 @@ impl Ctx { } } +/// Result of `TxEnv`, `storage_api::StorageRead` or `storage_api::StorageWrite` +/// method call +pub type EnvResult = Result; + /// Transaction result pub type TxResult = EnvResult<()>; @@ -101,7 +105,7 @@ impl StorageRead<'_> for Ctx { fn read_bytes( &self, key: &namada::types::storage::Key, - ) -> Result>, storage_api::Error> { + ) -> Result>, Error> { let key = key.to_string(); let read_result = unsafe { anoma_tx_read(key.as_ptr() as _, key.len() as _) }; @@ -111,14 +115,14 @@ impl StorageRead<'_> for Ctx { fn has_key( &self, key: &namada::types::storage::Key, - ) -> Result { + ) -> Result { let key = key.to_string(); let found = unsafe { anoma_tx_has_key(key.as_ptr() as _, key.len() as _) }; Ok(HostEnvResult::is_success(found)) } - fn get_chain_id(&self) -> Result { + fn get_chain_id(&self) -> Result { let result = Vec::with_capacity(CHAIN_ID_LENGTH); unsafe { anoma_tx_get_chain_id(result.as_ptr() as _); @@ -131,13 +135,13 @@ impl StorageRead<'_> for Ctx { fn get_block_height( &self, - ) -> Result { + ) -> Result { Ok(BlockHeight(unsafe { anoma_tx_get_block_height() })) } fn get_block_hash( &self, - ) -> Result { + ) -> Result { let result = Vec::with_capacity(BLOCK_HASH_LENGTH); unsafe { anoma_tx_get_block_hash(result.as_ptr() as _); @@ -148,16 +152,14 @@ impl StorageRead<'_> for Ctx { Ok(BlockHash::try_from(slice).expect("Cannot convert the hash")) } - fn get_block_epoch( - &self, - ) -> Result { + fn get_block_epoch(&self) -> Result { Ok(Epoch(unsafe { anoma_tx_get_block_epoch() })) } fn iter_prefix( &self, prefix: &namada::types::storage::Key, - ) -> Result { + ) -> Result { let prefix = prefix.to_string(); let iter_id = unsafe { anoma_tx_iter_prefix(prefix.as_ptr() as _, prefix.len() as _) @@ -168,7 +170,7 @@ impl StorageRead<'_> for Ctx { fn iter_next( &self, iter: &mut Self::PrefixIter, - ) -> Result)>, storage_api::Error> { + ) -> Result)>, Error> { let read_result = unsafe { anoma_tx_iter_next(iter.0) }; Ok(read_key_val_bytes_from_buffer( read_result, @@ -206,8 +208,6 @@ impl StorageWrite for Ctx { } impl TxEnv<'_> for Ctx { - type Error = Error; - fn get_block_time(&self) -> Result { let read_result = unsafe { anoma_tx_get_block_time() }; let time_value = read_from_buffer(read_result, anoma_tx_result_buffer) diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index 65a6c3f6cd..97a258365c 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -118,34 +118,6 @@ namada::impl_pos_read_only! { impl namada_proof_of_stake::PosReadOnly for Ctx } -impl From> for Error { - fn from(err: namada_proof_of_stake::BecomeValidatorError
) -> Self { - Self::new(err) - } -} - -impl From> for Error { - fn from(err: namada_proof_of_stake::BondError
) -> Self { - Self::new(err) - } -} - -impl From> - for Error -{ - fn from( - err: namada_proof_of_stake::UnbondError, - ) -> Self { - Self::new(err) - } -} - -impl From> for Error { - fn from(err: namada_proof_of_stake::WithdrawError
) -> Self { - Self::new(err) - } -} - impl namada_proof_of_stake::PosActions for Ctx { type BecomeValidatorError = crate::Error; type BondError = crate::Error; @@ -156,7 +128,7 @@ impl namada_proof_of_stake::PosActions for Ctx { &mut self, params: &PosParams, ) -> Result<(), Self::Error> { - self.write(¶ms_key(), params).into_env_result() + self.write(¶ms_key(), params) } fn write_validator_address_raw_hash( @@ -165,7 +137,6 @@ impl namada_proof_of_stake::PosActions for Ctx { ) -> Result<(), Self::Error> { let raw_hash = address.raw_hash().unwrap().to_owned(); self.write(&validator_address_raw_hash_key(raw_hash), address) - .into_env_result() } fn write_validator_staking_reward_address( @@ -174,7 +145,6 @@ impl namada_proof_of_stake::PosActions for Ctx { value: Self::Address, ) -> Result<(), Self::Error> { self.write(&validator_staking_reward_address_key(key), &value) - .into_env_result() } fn write_validator_consensus_key( @@ -183,7 +153,6 @@ impl namada_proof_of_stake::PosActions for Ctx { value: ValidatorConsensusKeys, ) -> Result<(), Self::Error> { self.write(&validator_consensus_key_key(key), &value) - .into_env_result() } fn write_validator_state( @@ -192,7 +161,6 @@ impl namada_proof_of_stake::PosActions for Ctx { value: ValidatorStates, ) -> Result<(), Self::Error> { self.write(&validator_state_key(key), &value) - .into_env_result() } fn write_validator_total_deltas( @@ -201,7 +169,6 @@ impl namada_proof_of_stake::PosActions for Ctx { value: ValidatorTotalDeltas, ) -> Result<(), Self::Error> { self.write(&validator_total_deltas_key(key), &value) - .into_env_result() } fn write_validator_voting_power( @@ -210,7 +177,6 @@ impl namada_proof_of_stake::PosActions for Ctx { value: ValidatorVotingPowers, ) -> Result<(), Self::Error> { self.write(&validator_voting_power_key(key), &value) - .into_env_result() } fn write_bond( @@ -218,7 +184,7 @@ impl namada_proof_of_stake::PosActions for Ctx { key: &BondId, value: Bonds, ) -> Result<(), Self::Error> { - self.write(&bond_key(key), &value).into_env_result() + self.write(&bond_key(key), &value) } fn write_unbond( @@ -226,14 +192,14 @@ impl namada_proof_of_stake::PosActions for Ctx { key: &BondId, value: Unbonds, ) -> Result<(), Self::Error> { - self.write(&unbond_key(key), &value).into_env_result() + self.write(&unbond_key(key), &value) } fn write_validator_set( &mut self, value: ValidatorSets, ) -> Result<(), Self::Error> { - self.write(&validator_set_key(), &value).into_env_result() + self.write(&validator_set_key(), &value) } fn write_total_voting_power( @@ -241,15 +207,14 @@ impl namada_proof_of_stake::PosActions for Ctx { value: TotalVotingPowers, ) -> Result<(), Self::Error> { self.write(&total_voting_power_key(), &value) - .into_env_result() } fn delete_bond(&mut self, key: &BondId) -> Result<(), Self::Error> { - self.delete(&bond_key(key)).into_env_result() + self.delete(&bond_key(key)) } fn delete_unbond(&mut self, key: &BondId) -> Result<(), Self::Error> { - self.delete(&unbond_key(key)).into_env_result() + self.delete(&unbond_key(key)) } fn transfer( diff --git a/vp_prelude/src/error.rs b/vp_prelude/src/error.rs deleted file mode 100644 index 099ae2540a..0000000000 --- a/vp_prelude/src/error.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! Helpers for error handling in WASM -//! -//! This module is currently duplicated in tx_prelude and vp_prelude crates to -//! be able to implement `From` conversion on error types from other crates, -//! avoiding `error[E0117]: only traits defined in the current crate can be -//! implemented for arbitrary types` - -use namada::ledger::storage_api; -use thiserror::Error; - -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - SimpleMessage(&'static str), - #[error("{0}")] - Custom(CustomError), - #[error("{0}: {1}")] - CustomWithMessage(&'static str, CustomError), -} - -/// Result of transaction or VP. -pub type EnvResult = Result; - -pub trait ResultExt { - /// Replace a possible error with a static message in [`EnvResult`]. - fn err_msg(self, msg: &'static str) -> EnvResult; -} - -// This is separate from `ResultExt`, because the implementation requires -// different bounds for `T`. -pub trait ResultExt2 { - /// Convert a [`Result`] into [`EnvResult`]. - fn into_env_result(self) -> EnvResult; - - /// Add a static message to a possible error in [`EnvResult`]. - fn wrap_err(self, msg: &'static str) -> EnvResult; -} - -pub trait OptionExt { - /// Transforms the [`Option`] into a [`EnvResult`], mapping - /// [`Some(v)`] to [`Ok(v)`] and [`None`] to the given static error - /// message. - fn ok_or_err_msg(self, msg: &'static str) -> EnvResult; -} - -impl ResultExt for Result { - fn err_msg(self, msg: &'static str) -> EnvResult { - self.map_err(|_err| Error::new_const(msg)) - } -} - -impl ResultExt2 for Result -where - E: std::error::Error + Send + Sync + 'static, -{ - fn into_env_result(self) -> EnvResult { - self.map_err(Error::new) - } - - fn wrap_err(self, msg: &'static str) -> EnvResult { - self.map_err(|err| Error::wrap(msg, err)) - } -} - -impl OptionExt for Option { - fn ok_or_err_msg(self, msg: &'static str) -> EnvResult { - self.ok_or_else(|| Error::new_const(msg)) - } -} - -impl Error { - /// Create an [`enum@Error`] from a static message. - #[inline] - pub const fn new_const(msg: &'static str) -> Self { - Self::SimpleMessage(msg) - } - - /// Create an [`enum@Error`] from another [`std::error::Error`]. - pub fn new(error: E) -> Self - where - E: Into>, - { - Self::Custom(CustomError(error.into())) - } - - /// Wrap another [`std::error::Error`] with a static message. - pub fn wrap(msg: &'static str, error: E) -> Self - where - E: Into>, - { - Self::CustomWithMessage(msg, CustomError(error.into())) - } -} - -/// A custom error -#[derive(Debug)] -pub struct CustomError(Box); - -impl std::fmt::Display for CustomError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl From for Error { - fn from(err: storage_api::Error) -> Self { - Self::new(err) - } -} diff --git a/vp_prelude/src/lib.rs b/vp_prelude/src/lib.rs index d1d3845a48..46432a3b52 100644 --- a/vp_prelude/src/lib.rs +++ b/vp_prelude/src/lib.rs @@ -6,7 +6,6 @@ #![deny(rustdoc::broken_intra_doc_links)] #![deny(rustdoc::private_intra_doc_links)] -mod error; pub mod intent; pub mod key; pub mod nft; @@ -20,9 +19,10 @@ use std::convert::TryFrom; use std::marker::PhantomData; pub use borsh::{BorshDeserialize, BorshSerialize}; -pub use error::*; pub use namada::ledger::governance::storage as gov_storage; -pub use namada::ledger::storage_api::{self, StorageRead}; +pub use namada::ledger::storage_api::{ + self, Error, OptionExt, ResultExt, StorageRead, +}; pub use namada::ledger::vp_env::VpEnv; pub use namada::ledger::{parameters, pos as proof_of_stake}; pub use namada::proto::{Signed, SignedTxData}; @@ -134,6 +134,9 @@ pub struct CtxPostStorageRead<'a> { _ctx: &'a Ctx, } +/// Result of `VpEnv` or `storage_api::StorageRead` method call +pub type EnvResult = Result; + /// Validity predicate result pub type VpResult = EnvResult; @@ -151,7 +154,6 @@ pub fn reject() -> VpResult { pub struct KeyValIterator(pub u64, pub PhantomData); impl<'view> VpEnv<'view> for Ctx { - type Error = Error; type Post = CtxPostStorageRead<'view>; type Pre = CtxPreStorageRead<'view>; type PrefixIter = KeyValIterator<(String, Vec)>; @@ -167,7 +169,7 @@ impl<'view> VpEnv<'view> for Ctx { fn read_temp( &self, key: &storage::Key, - ) -> Result, Self::Error> { + ) -> Result, Error> { let key = key.to_string(); let read_result = unsafe { anoma_vp_read_temp(key.as_ptr() as _, key.len() as _) }; @@ -178,46 +180,46 @@ impl<'view> VpEnv<'view> for Ctx { fn read_bytes_temp( &self, key: &storage::Key, - ) -> Result>, Self::Error> { + ) -> Result>, Error> { let key = key.to_string(); let read_result = unsafe { anoma_vp_read_temp(key.as_ptr() as _, key.len() as _) }; Ok(read_from_buffer(read_result, anoma_vp_result_buffer)) } - fn get_chain_id(&'view self) -> Result { + fn get_chain_id(&'view self) -> Result { // Both `CtxPreStorageRead` and `CtxPostStorageRead` have the same impl - get_chain_id().into_env_result() + get_chain_id() } - fn get_block_height(&'view self) -> Result { + fn get_block_height(&'view self) -> Result { // Both `CtxPreStorageRead` and `CtxPostStorageRead` have the same impl - get_block_height().into_env_result() + get_block_height() } - fn get_block_hash(&'view self) -> Result { + fn get_block_hash(&'view self) -> Result { // Both `CtxPreStorageRead` and `CtxPostStorageRead` have the same impl - get_block_hash().into_env_result() + get_block_hash() } - fn get_block_epoch(&'view self) -> Result { + fn get_block_epoch(&'view self) -> Result { // Both `CtxPreStorageRead` and `CtxPostStorageRead` have the same impl - get_block_epoch().into_env_result() + get_block_epoch() } fn iter_prefix( &self, prefix: &storage::Key, - ) -> Result { + ) -> Result { // Both `CtxPreStorageRead` and `CtxPostStorageRead` have the same impl - iter_prefix(prefix).into_env_result() + iter_prefix(prefix) } fn eval( &self, vp_code: Vec, input_data: Vec, - ) -> Result { + ) -> Result { let result = unsafe { anoma_vp_eval( vp_code.as_ptr() as _, @@ -233,7 +235,7 @@ impl<'view> VpEnv<'view> for Ctx { &self, pk: &common::PublicKey, sig: &common::Signature, - ) -> Result { + ) -> Result { let pk = BorshSerialize::try_to_vec(pk).unwrap(); let sig = BorshSerialize::try_to_vec(sig).unwrap(); let valid = unsafe { @@ -247,7 +249,7 @@ impl<'view> VpEnv<'view> for Ctx { Ok(HostEnvResult::is_success(valid)) } - fn get_tx_code_hash(&self) -> Result { + fn get_tx_code_hash(&self) -> Result { let result = Vec::with_capacity(HASH_LENGTH); unsafe { anoma_vp_get_tx_code_hash(result.as_ptr() as _); @@ -261,17 +263,14 @@ impl<'view> VpEnv<'view> for Ctx { impl StorageRead<'_> for CtxPreStorageRead<'_> { type PrefixIter = KeyValIterator<(String, Vec)>; - fn read_bytes( - &self, - key: &storage::Key, - ) -> Result>, storage_api::Error> { + fn read_bytes(&self, key: &storage::Key) -> Result>, Error> { let key = key.to_string(); let read_result = unsafe { anoma_vp_read_pre(key.as_ptr() as _, key.len() as _) }; Ok(read_from_buffer(read_result, anoma_vp_result_buffer)) } - fn has_key(&self, key: &storage::Key) -> Result { + fn has_key(&self, key: &storage::Key) -> Result { let key = key.to_string(); let found = unsafe { anoma_vp_has_key_pre(key.as_ptr() as _, key.len() as _) }; @@ -281,7 +280,7 @@ impl StorageRead<'_> for CtxPreStorageRead<'_> { fn iter_next( &self, iter: &mut Self::PrefixIter, - ) -> Result)>, storage_api::Error> { + ) -> Result)>, Error> { let read_result = unsafe { anoma_vp_iter_pre_next(iter.0) }; Ok(read_key_val_bytes_from_buffer( read_result, @@ -294,23 +293,23 @@ impl StorageRead<'_> for CtxPreStorageRead<'_> { fn iter_prefix( &self, prefix: &storage::Key, - ) -> Result { + ) -> Result { iter_prefix(prefix) } - fn get_chain_id(&self) -> Result { + fn get_chain_id(&self) -> Result { get_chain_id() } - fn get_block_height(&self) -> Result { + fn get_block_height(&self) -> Result { get_block_height() } - fn get_block_hash(&self) -> Result { + fn get_block_hash(&self) -> Result { get_block_hash() } - fn get_block_epoch(&self) -> Result { + fn get_block_epoch(&self) -> Result { get_block_epoch() } } @@ -318,17 +317,14 @@ impl StorageRead<'_> for CtxPreStorageRead<'_> { impl StorageRead<'_> for CtxPostStorageRead<'_> { type PrefixIter = KeyValIterator<(String, Vec)>; - fn read_bytes( - &self, - key: &storage::Key, - ) -> Result>, storage_api::Error> { + fn read_bytes(&self, key: &storage::Key) -> Result>, Error> { let key = key.to_string(); let read_result = unsafe { anoma_vp_read_post(key.as_ptr() as _, key.len() as _) }; Ok(read_from_buffer(read_result, anoma_vp_result_buffer)) } - fn has_key(&self, key: &storage::Key) -> Result { + fn has_key(&self, key: &storage::Key) -> Result { let key = key.to_string(); let found = unsafe { anoma_vp_has_key_post(key.as_ptr() as _, key.len() as _) }; @@ -338,7 +334,7 @@ impl StorageRead<'_> for CtxPostStorageRead<'_> { fn iter_next( &self, iter: &mut Self::PrefixIter, - ) -> Result)>, storage_api::Error> { + ) -> Result)>, Error> { let read_result = unsafe { anoma_vp_iter_post_next(iter.0) }; Ok(read_key_val_bytes_from_buffer( read_result, @@ -351,30 +347,30 @@ impl StorageRead<'_> for CtxPostStorageRead<'_> { fn iter_prefix( &self, prefix: &storage::Key, - ) -> Result { + ) -> Result { iter_prefix(prefix) } - fn get_chain_id(&self) -> Result { + fn get_chain_id(&self) -> Result { get_chain_id() } - fn get_block_height(&self) -> Result { + fn get_block_height(&self) -> Result { get_block_height() } - fn get_block_hash(&self) -> Result { + fn get_block_hash(&self) -> Result { get_block_hash() } - fn get_block_epoch(&self) -> Result { + fn get_block_epoch(&self) -> Result { get_block_epoch() } } fn iter_prefix( prefix: &storage::Key, -) -> Result)>, storage_api::Error> { +) -> Result)>, Error> { let prefix = prefix.to_string(); let iter_id = unsafe { anoma_vp_iter_prefix(prefix.as_ptr() as _, prefix.len() as _) @@ -382,7 +378,7 @@ fn iter_prefix( Ok(KeyValIterator(iter_id, PhantomData)) } -fn get_chain_id() -> Result { +fn get_chain_id() -> Result { let result = Vec::with_capacity(CHAIN_ID_LENGTH); unsafe { anoma_vp_get_chain_id(result.as_ptr() as _); @@ -395,11 +391,11 @@ fn get_chain_id() -> Result { ) } -fn get_block_height() -> Result { +fn get_block_height() -> Result { Ok(BlockHeight(unsafe { anoma_vp_get_block_height() })) } -fn get_block_hash() -> Result { +fn get_block_hash() -> Result { let result = Vec::with_capacity(BLOCK_HASH_LENGTH); unsafe { anoma_vp_get_block_hash(result.as_ptr() as _); @@ -409,6 +405,6 @@ fn get_block_hash() -> Result { Ok(BlockHash::try_from(slice).expect("Cannot convert the hash")) } -fn get_block_epoch() -> Result { +fn get_block_epoch() -> Result { Ok(Epoch(unsafe { anoma_vp_get_block_epoch() })) } diff --git a/wasm/wasm_source/src/tx_bond.rs b/wasm/wasm_source/src/tx_bond.rs index 9b04e2a939..e880380802 100644 --- a/wasm/wasm_source/src/tx_bond.rs +++ b/wasm/wasm_source/src/tx_bond.rs @@ -5,10 +5,10 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]) - .err_msg("failed to decode SignedTxData")?; + .wrap_err("failed to decode SignedTxData")?; let data = signed.data.ok_or_err_msg("Missing data")?; let bond = transaction::pos::Bond::try_from_slice(&data[..]) - .err_msg("failed to decode Bond")?; + .wrap_err("failed to decode Bond")?; ctx.bond_tokens(bond.source.as_ref(), &bond.validator, bond.amount) } diff --git a/wasm/wasm_source/src/tx_from_intent.rs b/wasm/wasm_source/src/tx_from_intent.rs index a299070393..deeb5f3eb0 100644 --- a/wasm/wasm_source/src/tx_from_intent.rs +++ b/wasm/wasm_source/src/tx_from_intent.rs @@ -7,10 +7,10 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]) - .err_msg("failed to decode SignedTxData")?; + .wrap_err("failed to decode SignedTxData")?; let data = signed.data.ok_or_err_msg("Missing data")?; let tx_data = intent::IntentTransfers::try_from_slice(&data[..]) - .err_msg("failed to decode IntentTransfers")?; + .wrap_err("failed to decode IntentTransfers")?; // make sure that the matchmaker has to validate this tx ctx.insert_verifier(&tx_data.source)?; diff --git a/wasm/wasm_source/src/tx_ibc.rs b/wasm/wasm_source/src/tx_ibc.rs index 08b3c60d60..79cbc6cf96 100644 --- a/wasm/wasm_source/src/tx_ibc.rs +++ b/wasm/wasm_source/src/tx_ibc.rs @@ -8,7 +8,7 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]) - .err_msg("failed to decode SignedTxData")?; + .wrap_err("failed to decode SignedTxData")?; let data = signed.data.ok_or_err_msg("Missing data")?; ctx.dispatch_ibc_action(&data) } diff --git a/wasm/wasm_source/src/tx_init_account.rs b/wasm/wasm_source/src/tx_init_account.rs index 05789751b2..e0fe700d63 100644 --- a/wasm/wasm_source/src/tx_init_account.rs +++ b/wasm/wasm_source/src/tx_init_account.rs @@ -6,10 +6,10 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]) - .err_msg("failed to decode SignedTxData")?; + .wrap_err("failed to decode SignedTxData")?; let data = signed.data.ok_or_err_msg("Missing data")?; let tx_data = transaction::InitAccount::try_from_slice(&data[..]) - .err_msg("failed to decode InitAccount")?; + .wrap_err("failed to decode InitAccount")?; debug_log!("apply_tx called to init a new established account"); let address = ctx.init_account(&tx_data.vp_code)?; diff --git a/wasm/wasm_source/src/tx_init_nft.rs b/wasm/wasm_source/src/tx_init_nft.rs index ace54fc161..de67dfbb53 100644 --- a/wasm/wasm_source/src/tx_init_nft.rs +++ b/wasm/wasm_source/src/tx_init_nft.rs @@ -5,10 +5,10 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]) - .err_msg("failed to decode SignedTxData")?; + .wrap_err("failed to decode SignedTxData")?; let data = signed.data.ok_or_err_msg("Missing data")?; let tx_data = transaction::nft::CreateNft::try_from_slice(&data[..]) - .err_msg("failed to decode CreateNft")?; + .wrap_err("failed to decode CreateNft")?; log_string("apply_tx called to create a new NFT"); let _address = nft::init_nft(ctx, tx_data)?; diff --git a/wasm/wasm_source/src/tx_init_proposal.rs b/wasm/wasm_source/src/tx_init_proposal.rs index 728d7613ae..cb7fe9ffbb 100644 --- a/wasm/wasm_source/src/tx_init_proposal.rs +++ b/wasm/wasm_source/src/tx_init_proposal.rs @@ -5,11 +5,11 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]) - .err_msg("failed to decode SignedTxData")?; + .wrap_err("failed to decode SignedTxData")?; let data = signed.data.ok_or_err_msg("Missing data")?; let tx_data = transaction::governance::InitProposalData::try_from_slice(&data[..]) - .err_msg("failed to decode InitProposalData")?; + .wrap_err("failed to decode InitProposalData")?; log_string("apply_tx called to create a new governance proposal"); governance::init_proposal(ctx, tx_data) diff --git a/wasm/wasm_source/src/tx_init_validator.rs b/wasm/wasm_source/src/tx_init_validator.rs index 2bfed44fac..2d5f1a6256 100644 --- a/wasm/wasm_source/src/tx_init_validator.rs +++ b/wasm/wasm_source/src/tx_init_validator.rs @@ -7,10 +7,10 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]) - .err_msg("failed to decode SignedTxData")?; + .wrap_err("failed to decode SignedTxData")?; let data = signed.data.ok_or_err_msg("Missing data")?; let init_validator = InitValidator::try_from_slice(&data[..]) - .err_msg("failed to decode InitValidator")?; + .wrap_err("failed to decode InitValidator")?; debug_log!("apply_tx called to init a new validator account"); // Register the validator in PoS diff --git a/wasm/wasm_source/src/tx_mint_nft.rs b/wasm/wasm_source/src/tx_mint_nft.rs index f132b74158..d3ab17e7ad 100644 --- a/wasm/wasm_source/src/tx_mint_nft.rs +++ b/wasm/wasm_source/src/tx_mint_nft.rs @@ -5,10 +5,10 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]) - .err_msg("failed to decode SignedTxData")?; + .wrap_err("failed to decode SignedTxData")?; let data = signed.data.ok_or_err_msg("Missing data")?; let tx_data = transaction::nft::MintNft::try_from_slice(&data[..]) - .err_msg("failed to decode MintNft")?; + .wrap_err("failed to decode MintNft")?; log_string("apply_tx called to mint a new NFT tokens"); nft::mint_tokens(ctx, tx_data) diff --git a/wasm/wasm_source/src/tx_transfer.rs b/wasm/wasm_source/src/tx_transfer.rs index 5731612888..eccddee2f0 100644 --- a/wasm/wasm_source/src/tx_transfer.rs +++ b/wasm/wasm_source/src/tx_transfer.rs @@ -7,10 +7,10 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]) - .err_msg("failed to decode SignedTxData")?; + .wrap_err("failed to decode SignedTxData")?; let data = signed.data.ok_or_err_msg("Missing data")?; let transfer = token::Transfer::try_from_slice(&data[..]) - .err_msg("failed to decode token::Transfer")?; + .wrap_err("failed to decode token::Transfer")?; debug_log!("apply_tx called with transfer: {:#?}", transfer); let token::Transfer { source, diff --git a/wasm/wasm_source/src/tx_unbond.rs b/wasm/wasm_source/src/tx_unbond.rs index c5ffc1ab6e..5d1765bb38 100644 --- a/wasm/wasm_source/src/tx_unbond.rs +++ b/wasm/wasm_source/src/tx_unbond.rs @@ -6,10 +6,10 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]) - .err_msg("failed to decode SignedTxData")?; + .wrap_err("failed to decode SignedTxData")?; let data = signed.data.ok_or_err_msg("Missing data")?; let unbond = transaction::pos::Unbond::try_from_slice(&data[..]) - .err_msg("failed to decode Unbond")?; + .wrap_err("failed to decode Unbond")?; ctx.unbond_tokens(unbond.source.as_ref(), &unbond.validator, unbond.amount) } diff --git a/wasm/wasm_source/src/tx_update_vp.rs b/wasm/wasm_source/src/tx_update_vp.rs index d0c41d3bd9..0bb819f026 100644 --- a/wasm/wasm_source/src/tx_update_vp.rs +++ b/wasm/wasm_source/src/tx_update_vp.rs @@ -7,10 +7,10 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]) - .err_msg("failed to decode SignedTxData")?; + .wrap_err("failed to decode SignedTxData")?; let data = signed.data.ok_or_err_msg("Missing data")?; let update_vp = transaction::UpdateVp::try_from_slice(&data[..]) - .err_msg("failed to decode UpdateVp")?; + .wrap_err("failed to decode UpdateVp")?; debug_log!("update VP for: {:#?}", update_vp.addr); diff --git a/wasm/wasm_source/src/tx_vote_proposal.rs b/wasm/wasm_source/src/tx_vote_proposal.rs index 614e4a9fa1..92c7af4c7f 100644 --- a/wasm/wasm_source/src/tx_vote_proposal.rs +++ b/wasm/wasm_source/src/tx_vote_proposal.rs @@ -5,11 +5,11 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]) - .err_msg("failed to decode SignedTxData")?; + .wrap_err("failed to decode SignedTxData")?; let data = signed.data.ok_or_err_msg("Missing data")?; let tx_data = transaction::governance::VoteProposalData::try_from_slice(&data[..]) - .err_msg("failed to decode VoteProposalData")?; + .wrap_err("failed to decode VoteProposalData")?; debug_log!("apply_tx called to vote a governance proposal"); diff --git a/wasm/wasm_source/src/tx_withdraw.rs b/wasm/wasm_source/src/tx_withdraw.rs index bcb64b4af0..3c841d88b0 100644 --- a/wasm/wasm_source/src/tx_withdraw.rs +++ b/wasm/wasm_source/src/tx_withdraw.rs @@ -6,10 +6,10 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]) - .err_msg("failed to decode SignedTxData")?; + .wrap_err("failed to decode SignedTxData")?; let data = signed.data.ok_or_err_msg("Missing data")?; let withdraw = transaction::pos::Withdraw::try_from_slice(&data[..]) - .err_msg("failed to decode Withdraw")?; + .wrap_err("failed to decode Withdraw")?; let slashed = ctx.withdraw_tokens(withdraw.source.as_ref(), &withdraw.validator)?; diff --git a/wasm_for_tests/wasm_source/src/lib.rs b/wasm_for_tests/wasm_source/src/lib.rs index 2b6ef24242..4731ef60be 100644 --- a/wasm_for_tests/wasm_source/src/lib.rs +++ b/wasm_for_tests/wasm_source/src/lib.rs @@ -88,7 +88,7 @@ pub mod main { #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]) - .err_msg("failed to decode SignedTxData")?; + .wrap_err("failed to decode SignedTxData")?; let data = match signed.data { Some(data) => { log(&format!("got data ({} bytes)", data.len())); @@ -134,7 +134,7 @@ pub mod main { #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let signed = SignedTxData::try_from_slice(&tx_data[..]) - .err_msg("failed to decode SignedTxData")?; + .wrap_err("failed to decode SignedTxData")?; let transfer = token::Transfer::try_from_slice(&signed.data.unwrap()[..]).unwrap(); log_string(format!("apply_tx called to mint tokens: {:#?}", transfer)); From 45dddbeaed67c8bbc2720c8b7109a4e7dc0fd971 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 13 Sep 2022 15:51:26 +0000 Subject: [PATCH 48/81] [ci skip] wasm checksums update --- wasm/checksums.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index 1c5fec452f..06f33fcda7 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,19 +1,19 @@ { - "tx_bond.wasm": "tx_bond.078d5be45d839fc1b6c034c4fdfe2055add709daa05868826682d3b6abf27ac4.wasm", - "tx_from_intent.wasm": "tx_from_intent.dcdfc49a02c76f277afac84e3511ad47c70b3bf54d1273a15c6dc75648316937.wasm", - "tx_ibc.wasm": "tx_ibc.3957053e0ea9caaa49128994711339aea6ede77a99f150688df3de870c8b17d4.wasm", - "tx_init_account.wasm": "tx_init_account.92e59887817a6d789dc8a85bb1eff3c86bd0a9bd1ddc8a9e0e5de5c9e9c2ddc6.wasm", - "tx_init_nft.wasm": "tx_init_nft.4125bdf149533cd201895cfaf6cdfbb9616182c187137029fe3c9214030f1877.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.8da848905d5a8ad1b15046d5e59e04f307bde73dcc8d2ab0b6d8d235a31a8b52.wasm", - "tx_init_validator.wasm": "tx_init_validator.364786e78253bd9ce72d089cc1539a882eb8ef6fd8c818616d003241b47ac010.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.b2c34b21a2f0b533e3dcd4b2ee64e45ca00ccad8b3f22c410474c7a67c3302e8.wasm", - "tx_transfer.wasm": "tx_transfer.6fac9a68bcd1a50d9cec64605f8637dfa897ce3be242edc344858cf4075fc100.wasm", - "tx_unbond.wasm": "tx_unbond.eeaf8ff32984275288b0a9a36c9579dace6f3ecfaf59255af769acf57a00df4a.wasm", - "tx_update_vp.wasm": "tx_update_vp.a304b3c70361cde5fda458ba5637d6825eb002198e73990a1c74351113b83d43.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.8f306e1d1bcc9ca7f47a39108554fc847d4ac6df7a8d87a6effdffeae6adc4c4.wasm", - "tx_withdraw.wasm": "tx_withdraw.c288861e842f0b1ac6fbb95b14b33c8819ac587bbd5b483f2cdd0ea184206c65.wasm", - "vp_nft.wasm": "vp_nft.379f0a9fdbc9611ba9afc8b03ea17eb1e7c63992be3c2ecd5dd506a0ec3809f3.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.9d28df0e4eea55c98ac05638cfc6211aaf8e1a4b9489f7057634c66d39835c36.wasm", - "vp_token.wasm": "vp_token.6506aea021bb6de9186e96fa0d9ea2ad35bcb66777d6ecf890a66cfe36a74f23.wasm", - "vp_user.wasm": "vp_user.77a0d0d406e300b2d5b9bc1c13bc50f233b6923d369db939ac82c8e10b64543c.wasm" + "tx_bond.wasm": "tx_bond.b3529e7dcdaf314353fa43b04eba22c44deb6143f8243933f66d20554565fa51.wasm", + "tx_from_intent.wasm": "tx_from_intent.6985cd22aa16334b008262beda3051d16f0522e019ba178c20b2cf0c92ef3cf5.wasm", + "tx_ibc.wasm": "tx_ibc.3ffa1662cb15d178be631adaeb060e96deaa44dfca8ee2978fc7d9ea09776977.wasm", + "tx_init_account.wasm": "tx_init_account.81c82e4f85575244dd833f8de566844de926f8869f1cce1dd86a1d69fe527a2f.wasm", + "tx_init_nft.wasm": "tx_init_nft.cde4c8c381369a61c6f0363c6748118d064202541c41c69c4e534f3ebbba2567.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.bfc77114ad453d9791d115fd97d681f4bbba956c77fcdb97c619637bb807a7c8.wasm", + "tx_init_validator.wasm": "tx_init_validator.7ecc353a6668788e01aac3a5799e9baa7f83979cdb8c82011225a53f1952672d.wasm", + "tx_mint_nft.wasm": "tx_mint_nft.3bc71754956503b44f764dc27cd51796e0613435864dc3f10d089e3b99bda21e.wasm", + "tx_transfer.wasm": "tx_transfer.38b4c4ec9d949b72e51547ab55737c42db383f5e04385cffafccb36fa5a5dccb.wasm", + "tx_unbond.wasm": "tx_unbond.174499645c2aa9242f29050406a0c835d78e6c030b1f7978b5d5d1a602e88e99.wasm", + "tx_update_vp.wasm": "tx_update_vp.4ee18fa789b30215220f6edfd317314abb575baab34e9b0147afa65cbfec4543.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.17c3d54babf760367f63609f4e7aa6cf4500b80a5ff01c2917b54b4cfdf78df4.wasm", + "tx_withdraw.wasm": "tx_withdraw.010c8141fe8dfe470887d80f77db482ddc7081e318496180208490226060787e.wasm", + "vp_nft.wasm": "vp_nft.bfc7e3a5a33226ee19111cc0cc627da4da7db3d863ce1955d195873b36ba6374.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.b8d0069f0a287c57513b64daab378084d49c7e2cd52c44b0f5aa1a3ded262c88.wasm", + "vp_token.wasm": "vp_token.9f27451dba52d022ff02bbdf2421895e285e79ce2867dd1193a27fbfd2bff4fc.wasm", + "vp_user.wasm": "vp_user.eac794136d3c148b9997cc4e6fb458dcfc5e7c1f2edea49dad7c8b6d35b9c306.wasm" } \ No newline at end of file From b9406a224ddc314ee2e4e00e49b8500a30ef7eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 13 Sep 2022 18:01:44 +0200 Subject: [PATCH 49/81] fixup! Merge branch 'namada/tomas/sorted-prefix-iter' (#458) --- shared/src/types/key/secp256k1.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/shared/src/types/key/secp256k1.rs b/shared/src/types/key/secp256k1.rs index 99bcbb3f67..889b4de258 100644 --- a/shared/src/types/key/secp256k1.rs +++ b/shared/src/types/key/secp256k1.rs @@ -7,6 +7,7 @@ use std::io::{ErrorKind, Write}; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use data_encoding::HEXLOWER; #[cfg(feature = "rand")] use rand::{CryptoRng, RngCore}; use serde::de::{Error, SeqAccess, Visitor}; @@ -113,7 +114,7 @@ impl Ord for PublicKey { impl Display for PublicKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(&self.0.serialize_compressed())) + write!(f, "{}", HEXLOWER.encode(&self.0.serialize_compressed())) } } @@ -121,7 +122,9 @@ impl FromStr for PublicKey { type Err = ParsePublicKeyError; fn from_str(s: &str) -> Result { - let vec = hex::decode(s).map_err(ParsePublicKeyError::InvalidHex)?; + let vec = HEXLOWER + .decode(s.as_bytes()) + .map_err(ParsePublicKeyError::InvalidHex)?; BorshDeserialize::try_from_slice(&vec) .map_err(ParsePublicKeyError::InvalidEncoding) } @@ -226,7 +229,7 @@ impl BorshSchema for SecretKey { impl Display for SecretKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(&self.0.serialize())) + write!(f, "{}", HEXLOWER.encode(&self.0.serialize())) } } @@ -234,7 +237,9 @@ impl FromStr for SecretKey { type Err = ParseSecretKeyError; fn from_str(s: &str) -> Result { - let vec = hex::decode(s).map_err(ParseSecretKeyError::InvalidHex)?; + let vec = HEXLOWER + .decode(s.as_bytes()) + .map_err(ParseSecretKeyError::InvalidHex)?; BorshDeserialize::try_from_slice(&vec) .map_err(ParseSecretKeyError::InvalidEncoding) } From 16d9aa6c8ad6b3f1b8fc4be1ecba6f9b31f17f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 13 Sep 2022 18:42:46 +0200 Subject: [PATCH 50/81] changelog: add #465 --- .../unreleased/improvements/465-vp-tx-env-conrete-error.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/improvements/465-vp-tx-env-conrete-error.md diff --git a/.changelog/unreleased/improvements/465-vp-tx-env-conrete-error.md b/.changelog/unreleased/improvements/465-vp-tx-env-conrete-error.md new file mode 100644 index 0000000000..e40ff76a17 --- /dev/null +++ b/.changelog/unreleased/improvements/465-vp-tx-env-conrete-error.md @@ -0,0 +1,2 @@ +- Re-use `storage_api::Error` type that supports wrapping custom error in `VpEnv` and `TxEnv` traits. + ([#465](https://github.com/anoma/namada/pull/465)) From a0f193ad8fc21470646c45c049d563f40463bb2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 30 Aug 2022 15:16:48 +0200 Subject: [PATCH 51/81] rustdoc: resolve ambiguous link --- shared/src/ledger/storage_api/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/ledger/storage_api/error.rs b/shared/src/ledger/storage_api/error.rs index 8af95be723..f99539bc87 100644 --- a/shared/src/ledger/storage_api/error.rs +++ b/shared/src/ledger/storage_api/error.rs @@ -17,7 +17,7 @@ pub enum Error { /// Result of a storage API call. pub type Result = std::result::Result; -/// Result extension to easily wrap custom errors into [`Error`]. +/// Result extension to easily wrap custom errors into [`enum@Error`]. // This is separate from `ResultExt`, because the implementation requires // different bounds for `T`. pub trait ResultExt { From d7c30ac20f2d2c391eeb029f0a2092d0b2d4ed48 Mon Sep 17 00:00:00 2001 From: brentstone Date: Fri, 12 Aug 2022 16:08:07 -0400 Subject: [PATCH 52/81] create lazy data structures for storage access --- shared/src/ledger/storage_api/collections/mod.rs | 12 ++++++++++++ shared/src/ledger/storage_api/mod.rs | 1 + 2 files changed, 13 insertions(+) create mode 100644 shared/src/ledger/storage_api/collections/mod.rs diff --git a/shared/src/ledger/storage_api/collections/mod.rs b/shared/src/ledger/storage_api/collections/mod.rs new file mode 100644 index 0000000000..982ebbd99a --- /dev/null +++ b/shared/src/ledger/storage_api/collections/mod.rs @@ -0,0 +1,12 @@ +//! Lazy data structures for storage access where elements are not all loaded +//! into memory. This serves to minimize gas costs, avoid unbounded iteration +//! in some cases, and ease the validation of storage changes in the VP. +//! +//! Rather than finding the diff of the state before and after, the VP will +//! just receive the storage sub-keys that have experienced changes. +//! +//! CONTINUE TO UPDATE THE ABOVE + +pub mod lazy_map; +pub mod lazy_set; +pub mod lazy_vec; \ No newline at end of file diff --git a/shared/src/ledger/storage_api/mod.rs b/shared/src/ledger/storage_api/mod.rs index 06ec8361f5..5e8c570cd3 100644 --- a/shared/src/ledger/storage_api/mod.rs +++ b/shared/src/ledger/storage_api/mod.rs @@ -2,6 +2,7 @@ //! and VPs (both native and WASM). mod error; +pub mod collections; use borsh::{BorshDeserialize, BorshSerialize}; pub use error::{CustomError, Error, OptionExt, Result, ResultExt}; From b07b84495cb01df90f2e1849bb38fa7a8b2523c6 Mon Sep 17 00:00:00 2001 From: brentstone Date: Fri, 12 Aug 2022 16:08:35 -0400 Subject: [PATCH 53/81] add lazy vector --- .../storage_api/collections/lazy_vec.rs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 shared/src/ledger/storage_api/collections/lazy_vec.rs diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs new file mode 100644 index 0000000000..fe97683c49 --- /dev/null +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -0,0 +1,86 @@ +//! Lazy vec + +use std::marker::PhantomData; + +use borsh::{BorshSerialize, BorshDeserialize}; +use crate::{types::{storage}, ledger::storage_api::{StorageWrite, StorageRead}}; +use super::super::Result; + +/// Subkey pointing to the length of the LazyVec +pub const LEN_SUBKEY: &str = "len"; +/// Subkey corresponding to the data elements of the LazyVec +pub const DATA_SUBKEY: &str = "data"; + +/// LazyVec ! fill in ! +pub struct LazyVec { + key: storage::Key, + phantom: PhantomData, +} + + +impl LazyVec where T: BorshSerialize + BorshDeserialize { + + /// new + pub fn new(key: storage::Key) -> Self { + Self { key, phantom: PhantomData} + } + + /// push + pub fn push(&self, val: T, storage_read: &impl StorageRead, storage_write: &mut impl StorageWrite) -> Result<()> { + let len = self.read_len(storage_read)?; + + let sub_index = len.unwrap_or(0); + let len = sub_index + 1; + + let data_key = self.get_data_key(sub_index); + + storage_write.write(&data_key, val)?; + storage_write.write(&self.get_len_key(), len)?; + + Ok(()) + } + + /// pop + pub fn pop(&self, storage_read: &impl StorageRead, storage_write: &mut impl StorageWrite) -> Result> { + let len = self.read_len(storage_read)?; + match len { + Some(0) | None => Ok(None), + Some(len) => { + let sub_index = len - 1; + let data_key = self.get_data_key(sub_index); + if len == 1 { + storage_write.delete(&self.get_len_key())?; + + } else { + storage_write.write(&self.get_len_key(), sub_index)?; + + } + let popped_val = storage_read.read(&data_key)?; + storage_write.delete(&data_key)?; + Ok(popped_val) + }, + } + + } + + /// get the length subkey + fn get_len_key(&self) -> storage::Key { + self.key.push(&LEN_SUBKEY.to_owned()).unwrap() + } + + /// read the length of the LazyVec + pub fn read_len(&self, storage_read: &impl StorageRead) -> Result> { + storage_read.read(&self.get_len_key()) + } + + /// get the data subkey + fn get_data_key(&self, sub_index: u64) -> storage::Key { + self.key.push(&DATA_SUBKEY.to_owned()).unwrap().push(&sub_index.to_string()).unwrap() + } + + /// get the data held at a specific index within the data subkey + fn get(&self, sub_index: u64, storage_read: &impl StorageRead) -> Result> { + storage_read.read(&self.get_data_key(sub_index)) + } + +} \ No newline at end of file From 13173b3c3c102d945e2467d338c8ee989a19e6c7 Mon Sep 17 00:00:00 2001 From: brentstone Date: Fri, 12 Aug 2022 17:09:59 -0400 Subject: [PATCH 54/81] add lazy set (WIP), make LazyVec.get public --- .../storage_api/collections/lazy_set.rs | 69 +++++++++++++++++++ .../storage_api/collections/lazy_vec.rs | 2 +- 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 shared/src/ledger/storage_api/collections/lazy_set.rs diff --git a/shared/src/ledger/storage_api/collections/lazy_set.rs b/shared/src/ledger/storage_api/collections/lazy_set.rs new file mode 100644 index 0000000000..143f234e3e --- /dev/null +++ b/shared/src/ledger/storage_api/collections/lazy_set.rs @@ -0,0 +1,69 @@ +//! Lazy hash set + +use std::{marker::PhantomData, hash::Hash, hash::Hasher}; +use borsh::{BorshSerialize, BorshDeserialize}; +use std::collections::hash_map::DefaultHasher; + +use crate::{types::{storage}, ledger::storage_api::{StorageWrite, StorageRead}}; +use super::super::Result; + +/// Subkey corresponding to the data elements of the LazyVec +pub const DATA_SUBKEY: &str = "data"; + +/// lazy hash set +pub struct LazySet { + key: storage::Key, + phantom: PhantomData +} + +impl LazySet where T: BorshSerialize + BorshDeserialize + Hash { + + /// new + pub fn new(key: storage::Key) -> Self { + Self { key, phantom: PhantomData} + } + + /// insert + pub fn insert(&self, val: &T, storage_write: &mut impl StorageWrite) -> Result<()> { + + // Do we need to read to see if this val is already in the set? + + let data_key = self.get_data_key(val); + storage_write.write(&data_key, &val)?; + Ok(()) + } + + /// remove + pub fn remove(&self, val: &T, storage_write: &mut impl StorageWrite) -> Result<()> { + let data_key = self.get_data_key(val); + storage_write.delete(&data_key)?; + Ok(()) + } + + /// check if the hash set contains a value + pub fn contains(&self, val: &T, storage_read: &impl StorageRead) -> Result { + let digest: Option = storage_read.read(&self.get_data_key(val))?; + match digest { + Some(_) => Ok(true), + None => Ok(false), + } + } + + /// check if hash set is empty + pub fn is_empty(&self) { + todo!(); + } + + fn hash_val(&self, val: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + val.hash(&mut hasher); + hasher.finish() + } + + /// get the data subkey + fn get_data_key(&self, val: &T) -> storage::Key { + let hash_str = self.hash_val(val).to_string(); + self.key.push(&DATA_SUBKEY.to_owned()).unwrap().push(&hash_str).unwrap() + } + +} \ No newline at end of file diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index fe97683c49..31bc7e98db 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -79,7 +79,7 @@ impl LazyVec where T: BorshSerialize + BorshDeserialize { } /// get the data held at a specific index within the data subkey - fn get(&self, sub_index: u64, storage_read: &impl StorageRead) -> Result> { + pub fn get(&self, sub_index: u64, storage_read: &impl StorageRead) -> Result> { storage_read.read(&self.get_data_key(sub_index)) } From 72e48e69fad7aca0c9a3f0e1292a3ae7fe3d1059 Mon Sep 17 00:00:00 2001 From: brentstone Date: Sun, 14 Aug 2022 03:15:02 -0400 Subject: [PATCH 55/81] lazy hash map first commit --- .../storage_api/collections/lazy_map.rs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 shared/src/ledger/storage_api/collections/lazy_map.rs diff --git a/shared/src/ledger/storage_api/collections/lazy_map.rs b/shared/src/ledger/storage_api/collections/lazy_map.rs new file mode 100644 index 0000000000..321ef6362d --- /dev/null +++ b/shared/src/ledger/storage_api/collections/lazy_map.rs @@ -0,0 +1,68 @@ +//! Lazy map + +use borsh::{BorshSerialize, BorshDeserialize}; +use std::{marker::PhantomData, hash::Hash, hash::Hasher}; +use std::collections::hash_map::DefaultHasher; + +use crate::{types::{storage}, ledger::storage_api::{StorageWrite, StorageRead}}; +use super::super::Result; + + +/// Subkey corresponding to the data elements of the LazyMap +pub const DATA_SUBKEY: &str = "data"; + +/// LazyMap ! fill in ! +pub struct LazyMap { + key: storage::Key, + phantom_h: PhantomData, + phantom_t: PhantomData +} + +impl LazyMap where H: BorshDeserialize + BorshSerialize + Hash, +T: BorshDeserialize + BorshSerialize { + + /// insert + pub fn insert(&self, elem_key: &H, elem_val: T, storage_write: &mut impl StorageWrite) -> Result<()> { + + // TODO: Check to see if map element exists already ?? + + let data_key = self.get_data_key(elem_key); + storage_write.write(&data_key, (elem_key, elem_val))?; + + + Ok(()) + } + + /// remove + pub fn remove(&self, elem_key: &H, storage_write: &mut impl StorageWrite) -> Result<()> { + + let data_key = self.get_data_key(elem_key); + storage_write.delete(&data_key)?; + + Ok(()) + } + + /// get value + pub fn get_val(&self, elem_key: &H, storage_read: &mut impl StorageRead) -> Result> { + + // check if elem_key exists in the first place? + + let data_key = self.get_data_key(elem_key); + storage_read.read(&data_key) + + } + + /// hash + fn hash(&self, elem_key: &H) -> u64 { + let mut hasher = DefaultHasher::new(); + elem_key.hash(&mut hasher); + hasher.finish() + } + + /// get the data subkey + fn get_data_key(&self, elem_key: &H) -> storage::Key { + let hash_str = self.hash(elem_key).to_string(); + self.key.push(&DATA_SUBKEY.to_owned()).unwrap().push(&hash_str).unwrap() + } + +} \ No newline at end of file From d964880226ab41de852c6986a39265dd05b78de7 Mon Sep 17 00:00:00 2001 From: brentstone Date: Sun, 14 Aug 2022 04:38:32 -0400 Subject: [PATCH 56/81] add fn get_elem_key_by_hash to LazyMap --- .../storage_api/collections/lazy_map.rs | 76 ++++++++++++++----- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/shared/src/ledger/storage_api/collections/lazy_map.rs b/shared/src/ledger/storage_api/collections/lazy_map.rs index 321ef6362d..731cafd3b1 100644 --- a/shared/src/ledger/storage_api/collections/lazy_map.rs +++ b/shared/src/ledger/storage_api/collections/lazy_map.rs @@ -1,12 +1,14 @@ //! Lazy map -use borsh::{BorshSerialize, BorshDeserialize}; -use std::{marker::PhantomData, hash::Hash, hash::Hasher}; use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; -use crate::{types::{storage}, ledger::storage_api::{StorageWrite, StorageRead}}; -use super::super::Result; +use borsh::{BorshDeserialize, BorshSerialize}; +use super::super::Result; +use crate::ledger::storage_api::{StorageRead, StorageWrite}; +use crate::types::storage; /// Subkey corresponding to the data elements of the LazyMap pub const DATA_SUBKEY: &str = "data"; @@ -15,27 +17,35 @@ pub const DATA_SUBKEY: &str = "data"; pub struct LazyMap { key: storage::Key, phantom_h: PhantomData, - phantom_t: PhantomData + phantom_t: PhantomData, } -impl LazyMap where H: BorshDeserialize + BorshSerialize + Hash, -T: BorshDeserialize + BorshSerialize { - +impl LazyMap +where + H: BorshDeserialize + BorshSerialize + Hash, + T: BorshDeserialize + BorshSerialize, +{ /// insert - pub fn insert(&self, elem_key: &H, elem_val: T, storage_write: &mut impl StorageWrite) -> Result<()> { - + pub fn insert( + &self, + elem_key: &H, + elem_val: T, + storage_write: &mut impl StorageWrite, + ) -> Result<()> { // TODO: Check to see if map element exists already ?? let data_key = self.get_data_key(elem_key); storage_write.write(&data_key, (elem_key, elem_val))?; - Ok(()) } /// remove - pub fn remove(&self, elem_key: &H, storage_write: &mut impl StorageWrite) -> Result<()> { - + pub fn remove( + &self, + elem_key: &H, + storage_write: &mut impl StorageWrite, + ) -> Result<()> { let data_key = self.get_data_key(elem_key); storage_write.delete(&data_key)?; @@ -43,13 +53,38 @@ T: BorshDeserialize + BorshSerialize { } /// get value - pub fn get_val(&self, elem_key: &H, storage_read: &mut impl StorageRead) -> Result> { - + pub fn get( + &self, + elem_key: &H, + storage_read: &mut impl StorageRead, + ) -> Result> { // check if elem_key exists in the first place? let data_key = self.get_data_key(elem_key); - storage_read.read(&data_key) + let res: Option<(H, T)> = storage_read.read(&data_key)?; + match res { + Some(pair) => Ok(Some(pair.1)), + None => Ok(None), + } + } + /// get the element key by its hash + pub fn get_elem_key_by_hash( + &self, + elem_key_hash: &str, + storage_read: &mut impl StorageRead, + ) -> Result> { + let data_key = self + .key + .push(&DATA_SUBKEY.to_owned()) + .unwrap() + .push(&elem_key_hash.to_string()) + .unwrap(); + let res: Option<(H, T)> = storage_read.read(&data_key)?; + match res { + Some(pair) => Ok(Some(pair.0)), + None => Ok(None), + } } /// hash @@ -62,7 +97,10 @@ T: BorshDeserialize + BorshSerialize { /// get the data subkey fn get_data_key(&self, elem_key: &H) -> storage::Key { let hash_str = self.hash(elem_key).to_string(); - self.key.push(&DATA_SUBKEY.to_owned()).unwrap().push(&hash_str).unwrap() + self.key + .push(&DATA_SUBKEY.to_owned()) + .unwrap() + .push(&hash_str) + .unwrap() } - -} \ No newline at end of file +} From 6752132b06f2128a5e26360e04173ef53a0fc4a0 Mon Sep 17 00:00:00 2001 From: brentstone Date: Sun, 14 Aug 2022 04:42:40 -0400 Subject: [PATCH 57/81] fmt && clippy --- .../storage_api/collections/lazy_set.rs | 59 +++++++++++++------ .../storage_api/collections/lazy_vec.rs | 58 ++++++++++++------ .../src/ledger/storage_api/collections/mod.rs | 6 +- shared/src/ledger/storage_api/mod.rs | 2 +- 4 files changed, 85 insertions(+), 40 deletions(-) diff --git a/shared/src/ledger/storage_api/collections/lazy_set.rs b/shared/src/ledger/storage_api/collections/lazy_set.rs index 143f234e3e..187f5b5123 100644 --- a/shared/src/ledger/storage_api/collections/lazy_set.rs +++ b/shared/src/ledger/storage_api/collections/lazy_set.rs @@ -1,31 +1,42 @@ //! Lazy hash set -use std::{marker::PhantomData, hash::Hash, hash::Hasher}; -use borsh::{BorshSerialize, BorshDeserialize}; use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; + +use borsh::{BorshDeserialize, BorshSerialize}; -use crate::{types::{storage}, ledger::storage_api::{StorageWrite, StorageRead}}; use super::super::Result; +use crate::ledger::storage_api::{StorageRead, StorageWrite}; +use crate::types::storage; -/// Subkey corresponding to the data elements of the LazyVec +/// Subkey corresponding to the data elements of the LazySet pub const DATA_SUBKEY: &str = "data"; /// lazy hash set pub struct LazySet { key: storage::Key, - phantom: PhantomData + phantom: PhantomData, } -impl LazySet where T: BorshSerialize + BorshDeserialize + Hash { - +impl LazySet +where + T: BorshSerialize + BorshDeserialize + Hash, +{ /// new pub fn new(key: storage::Key) -> Self { - Self { key, phantom: PhantomData} - } + Self { + key, + phantom: PhantomData, + } + } /// insert - pub fn insert(&self, val: &T, storage_write: &mut impl StorageWrite) -> Result<()> { - + pub fn insert( + &self, + val: &T, + storage_write: &mut impl StorageWrite, + ) -> Result<()> { // Do we need to read to see if this val is already in the set? let data_key = self.get_data_key(val); @@ -34,14 +45,22 @@ impl LazySet where T: BorshSerialize + BorshDeserialize + Hash { } /// remove - pub fn remove(&self, val: &T, storage_write: &mut impl StorageWrite) -> Result<()> { + pub fn remove( + &self, + val: &T, + storage_write: &mut impl StorageWrite, + ) -> Result<()> { let data_key = self.get_data_key(val); storage_write.delete(&data_key)?; Ok(()) } /// check if the hash set contains a value - pub fn contains(&self, val: &T, storage_read: &impl StorageRead) -> Result { + pub fn contains( + &self, + val: &T, + storage_read: &impl StorageRead, + ) -> Result { let digest: Option = storage_read.read(&self.get_data_key(val))?; match digest { Some(_) => Ok(true), @@ -49,7 +68,8 @@ impl LazySet where T: BorshSerialize + BorshDeserialize + Hash { } } - /// check if hash set is empty + /// check if hash set is empty (if we want to do this we prob need a length + /// field) pub fn is_empty(&self) { todo!(); } @@ -60,10 +80,13 @@ impl LazySet where T: BorshSerialize + BorshDeserialize + Hash { hasher.finish() } - /// get the data subkey + /// get the data subkey fn get_data_key(&self, val: &T) -> storage::Key { let hash_str = self.hash_val(val).to_string(); - self.key.push(&DATA_SUBKEY.to_owned()).unwrap().push(&hash_str).unwrap() + self.key + .push(&DATA_SUBKEY.to_owned()) + .unwrap() + .push(&hash_str) + .unwrap() } - -} \ No newline at end of file +} diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index 31bc7e98db..4c08f906d9 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -2,9 +2,11 @@ use std::marker::PhantomData; -use borsh::{BorshSerialize, BorshDeserialize}; -use crate::{types::{storage}, ledger::storage_api::{StorageWrite, StorageRead}}; +use borsh::{BorshDeserialize, BorshSerialize}; + use super::super::Result; +use crate::ledger::storage_api::{StorageRead, StorageWrite}; +use crate::types::storage; /// Subkey pointing to the length of the LazyVec pub const LEN_SUBKEY: &str = "len"; @@ -17,16 +19,25 @@ pub struct LazyVec { phantom: PhantomData, } - -impl LazyVec where T: BorshSerialize + BorshDeserialize { - +impl LazyVec +where + T: BorshSerialize + BorshDeserialize, +{ /// new pub fn new(key: storage::Key) -> Self { - Self { key, phantom: PhantomData} + Self { + key, + phantom: PhantomData, + } } /// push - pub fn push(&self, val: T, storage_read: &impl StorageRead, storage_write: &mut impl StorageWrite) -> Result<()> { + pub fn push( + &self, + val: T, + storage_read: &impl StorageRead, + storage_write: &mut impl StorageWrite, + ) -> Result<()> { let len = self.read_len(storage_read)?; let sub_index = len.unwrap_or(0); @@ -36,12 +47,16 @@ impl LazyVec where T: BorshSerialize + BorshDeserialize { storage_write.write(&data_key, val)?; storage_write.write(&self.get_len_key(), len)?; - + Ok(()) } /// pop - pub fn pop(&self, storage_read: &impl StorageRead, storage_write: &mut impl StorageWrite) -> Result> { + pub fn pop( + &self, + storage_read: &impl StorageRead, + storage_write: &mut impl StorageWrite, + ) -> Result> { let len = self.read_len(storage_read)?; match len { Some(0) | None => Ok(None), @@ -50,17 +65,14 @@ impl LazyVec where T: BorshSerialize + BorshDeserialize { let data_key = self.get_data_key(sub_index); if len == 1 { storage_write.delete(&self.get_len_key())?; - } else { storage_write.write(&self.get_len_key(), sub_index)?; - } let popped_val = storage_read.read(&data_key)?; storage_write.delete(&data_key)?; Ok(popped_val) - }, + } } - } /// get the length subkey @@ -69,18 +81,28 @@ impl LazyVec where T: BorshSerialize + BorshDeserialize { } /// read the length of the LazyVec - pub fn read_len(&self, storage_read: &impl StorageRead) -> Result> { + pub fn read_len( + &self, + storage_read: &impl StorageRead, + ) -> Result> { storage_read.read(&self.get_len_key()) } /// get the data subkey fn get_data_key(&self, sub_index: u64) -> storage::Key { - self.key.push(&DATA_SUBKEY.to_owned()).unwrap().push(&sub_index.to_string()).unwrap() + self.key + .push(&DATA_SUBKEY.to_owned()) + .unwrap() + .push(&sub_index.to_string()) + .unwrap() } /// get the data held at a specific index within the data subkey - pub fn get(&self, sub_index: u64, storage_read: &impl StorageRead) -> Result> { + pub fn get( + &self, + sub_index: u64, + storage_read: &impl StorageRead, + ) -> Result> { storage_read.read(&self.get_data_key(sub_index)) } - -} \ No newline at end of file +} diff --git a/shared/src/ledger/storage_api/collections/mod.rs b/shared/src/ledger/storage_api/collections/mod.rs index 982ebbd99a..c3784c2525 100644 --- a/shared/src/ledger/storage_api/collections/mod.rs +++ b/shared/src/ledger/storage_api/collections/mod.rs @@ -1,12 +1,12 @@ //! Lazy data structures for storage access where elements are not all loaded //! into memory. This serves to minimize gas costs, avoid unbounded iteration //! in some cases, and ease the validation of storage changes in the VP. -//! +//! //! Rather than finding the diff of the state before and after, the VP will //! just receive the storage sub-keys that have experienced changes. -//! +//! //! CONTINUE TO UPDATE THE ABOVE pub mod lazy_map; pub mod lazy_set; -pub mod lazy_vec; \ No newline at end of file +pub mod lazy_vec; diff --git a/shared/src/ledger/storage_api/mod.rs b/shared/src/ledger/storage_api/mod.rs index 5e8c570cd3..8cb04434e2 100644 --- a/shared/src/ledger/storage_api/mod.rs +++ b/shared/src/ledger/storage_api/mod.rs @@ -1,8 +1,8 @@ //! The common storage read trait is implemented in the storage, client RPC, tx //! and VPs (both native and WASM). -mod error; pub mod collections; +mod error; use borsh::{BorshDeserialize, BorshSerialize}; pub use error::{CustomError, Error, OptionExt, Result, ResultExt}; From 68cd35a5475ea80e0720d711906632ac208bf07f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 15 Aug 2022 14:09:19 +0200 Subject: [PATCH 58/81] refactored lazy collections, replaced hasher, added iter --- .../ledger/storage_api/collections/hasher.rs | 9 + .../storage_api/collections/lazy_map.rs | 202 ++++++++++++------ .../storage_api/collections/lazy_set.rs | 116 ++++++---- .../storage_api/collections/lazy_vec.rs | 147 +++++++------ .../src/ledger/storage_api/collections/mod.rs | 1 + 5 files changed, 300 insertions(+), 175 deletions(-) create mode 100644 shared/src/ledger/storage_api/collections/hasher.rs diff --git a/shared/src/ledger/storage_api/collections/hasher.rs b/shared/src/ledger/storage_api/collections/hasher.rs new file mode 100644 index 0000000000..0f864259f5 --- /dev/null +++ b/shared/src/ledger/storage_api/collections/hasher.rs @@ -0,0 +1,9 @@ +use borsh::BorshSerialize; + +/// Hash borsh encoded data into a storage sub-key. +/// This is a sha256 as an uppercase hexadecimal string. +pub fn hash_for_storage_key(data: impl BorshSerialize) -> String { + let bytes = data.try_to_vec().unwrap(); + let hash = crate::types::hash::Hash::sha256(bytes); + hash.to_string() +} diff --git a/shared/src/ledger/storage_api/collections/lazy_map.rs b/shared/src/ledger/storage_api/collections/lazy_map.rs index 731cafd3b1..25b4cd4d5d 100644 --- a/shared/src/ledger/storage_api/collections/lazy_map.rs +++ b/shared/src/ledger/storage_api/collections/lazy_map.rs @@ -1,106 +1,170 @@ //! Lazy map -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; use std::marker::PhantomData; use borsh::{BorshDeserialize, BorshSerialize}; use super::super::Result; -use crate::ledger::storage_api::{StorageRead, StorageWrite}; +use super::hasher::hash_for_storage_key; +use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; use crate::types::storage; /// Subkey corresponding to the data elements of the LazyMap pub const DATA_SUBKEY: &str = "data"; /// LazyMap ! fill in ! -pub struct LazyMap { +pub struct LazyMap { key: storage::Key, - phantom_h: PhantomData, - phantom_t: PhantomData, + phantom_h: PhantomData, + phantom_t: PhantomData, } -impl LazyMap +#[derive(Debug, BorshSerialize, BorshDeserialize)] +struct KeyVal { + key: K, + val: V, +} + +impl LazyMap where - H: BorshDeserialize + BorshSerialize + Hash, - T: BorshDeserialize + BorshSerialize, + K: BorshDeserialize + BorshSerialize, + V: BorshDeserialize + BorshSerialize, { - /// insert - pub fn insert( + /// Create or use an existing map with the given storage `key`. + pub fn new(key: storage::Key) -> Self { + Self { + key, + phantom_h: PhantomData, + phantom_t: PhantomData, + } + } + + /// Inserts a key-value pair into the map. + /// + /// If the map did not have this key present, `None` is returned. + /// If the map did have this key present, the value is updated, and the old + /// value is returned. Unlike in `std::collection::HashMap`, the key is also + /// updated; this matters for types that can be `==` without being + /// identical. + pub fn insert( &self, - elem_key: &H, - elem_val: T, - storage_write: &mut impl StorageWrite, - ) -> Result<()> { - // TODO: Check to see if map element exists already ?? + storage: &mut S, + key: K, + val: V, + ) -> Result> + where + S: StorageWrite + StorageRead, + { + let previous = self.get(storage, &key)?; - let data_key = self.get_data_key(elem_key); - storage_write.write(&data_key, (elem_key, elem_val))?; + let data_key = self.get_data_key(&key); + Self::write_key_val(storage, &data_key, key, val)?; - Ok(()) + Ok(previous) } - /// remove - pub fn remove( - &self, - elem_key: &H, - storage_write: &mut impl StorageWrite, - ) -> Result<()> { - let data_key = self.get_data_key(elem_key); - storage_write.delete(&data_key)?; + /// Removes a key from the map, returning the value at the key if the key + /// was previously in the map. + pub fn remove(&self, storage: &mut S, key: &K) -> Result> + where + S: StorageWrite + StorageRead, + { + let value = self.get(storage, key)?; + + let data_key = self.get_data_key(key); + storage.delete(&data_key)?; - Ok(()) + Ok(value) } - /// get value + /// Returns the value corresponding to the key, if any. pub fn get( &self, - elem_key: &H, - storage_read: &mut impl StorageRead, - ) -> Result> { - // check if elem_key exists in the first place? - - let data_key = self.get_data_key(elem_key); - let res: Option<(H, T)> = storage_read.read(&data_key)?; - match res { - Some(pair) => Ok(Some(pair.1)), - None => Ok(None), - } + storage: &impl StorageRead, + key: &K, + ) -> Result> { + let res = self.get_key_val(storage, key)?; + Ok(res.map(|elem| elem.1)) } - /// get the element key by its hash - pub fn get_elem_key_by_hash( + /// Returns the key-value corresponding to the key, if any. + pub fn get_key_val( &self, - elem_key_hash: &str, - storage_read: &mut impl StorageRead, - ) -> Result> { - let data_key = self - .key - .push(&DATA_SUBKEY.to_owned()) - .unwrap() - .push(&elem_key_hash.to_string()) - .unwrap(); - let res: Option<(H, T)> = storage_read.read(&data_key)?; - match res { - Some(pair) => Ok(Some(pair.0)), - None => Ok(None), - } + storage: &impl StorageRead, + key: &K, + ) -> Result> { + let data_key = self.get_data_key(key); + Self::read_key_val(storage, &data_key) + } + + /// Returns the key-value corresponding to the given hash of a key, if any. + pub fn get_key_val_by_hash( + &self, + storage: &impl StorageRead, + key_hash: &str, + ) -> Result> { + let data_key = + self.get_data_prefix().push(&key_hash.to_string()).unwrap(); + Self::read_key_val(storage, &data_key) + } + + /// An iterator visiting all key-value elements. The iterator element type + /// is `Result<(K, V)>`, because iterator's call to `next` may fail with + /// e.g. out of gas or data decoding error. + /// + /// Note that this function shouldn't be used in transactions and VPs code + /// on unbounded sets to avoid gas usage increasing with the length of the + /// map. + pub fn iter<'a>( + &self, + storage: &'a impl StorageRead, + ) -> Result> + 'a> { + let iter = storage.iter_prefix(&self.get_data_prefix())?; + let iter = itertools::unfold(iter, |iter| { + match storage.iter_next(iter) { + Ok(Some((_key, value))) => { + match KeyVal::::try_from_slice(&value[..]) { + Ok(KeyVal { key, val }) => Some(Ok((key, val))), + Err(err) => Some(Err(storage_api::Error::new(err))), + } + } + Ok(None) => None, + Err(err) => { + // Propagate errors into Iterator's Item + Some(Err(err)) + } + } + }); + Ok(iter) + } + + /// Reads a key-value from storage + fn read_key_val( + storage: &impl StorageRead, + storage_key: &storage::Key, + ) -> Result> { + let res = storage.read(storage_key)?; + Ok(res.map(|KeyVal { key, val }| (key, val))) + } + + /// Write a key-value into storage + fn write_key_val( + storage: &mut impl StorageWrite, + storage_key: &storage::Key, + key: K, + val: V, + ) -> Result<()> { + storage.write(storage_key, KeyVal { key, val }) } - /// hash - fn hash(&self, elem_key: &H) -> u64 { - let mut hasher = DefaultHasher::new(); - elem_key.hash(&mut hasher); - hasher.finish() + /// Get the prefix of set's elements storage + fn get_data_prefix(&self) -> storage::Key { + self.key.push(&DATA_SUBKEY.to_owned()).unwrap() } - /// get the data subkey - fn get_data_key(&self, elem_key: &H) -> storage::Key { - let hash_str = self.hash(elem_key).to_string(); - self.key - .push(&DATA_SUBKEY.to_owned()) - .unwrap() - .push(&hash_str) - .unwrap() + /// Get the sub-key of a given element + fn get_data_key(&self, key: &K) -> storage::Key { + let hash_str = hash_for_storage_key(key); + self.get_data_prefix().push(&hash_str).unwrap() } } diff --git a/shared/src/ledger/storage_api/collections/lazy_set.rs b/shared/src/ledger/storage_api/collections/lazy_set.rs index 187f5b5123..862485b687 100644 --- a/shared/src/ledger/storage_api/collections/lazy_set.rs +++ b/shared/src/ledger/storage_api/collections/lazy_set.rs @@ -1,13 +1,12 @@ //! Lazy hash set -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; use std::marker::PhantomData; use borsh::{BorshDeserialize, BorshSerialize}; use super::super::Result; -use crate::ledger::storage_api::{StorageRead, StorageWrite}; +use super::hasher::hash_for_storage_key; +use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; use crate::types::storage; /// Subkey corresponding to the data elements of the LazySet @@ -21,9 +20,9 @@ pub struct LazySet { impl LazySet where - T: BorshSerialize + BorshDeserialize + Hash, + T: BorshSerialize + BorshDeserialize, { - /// new + /// Create or use an existing set with the given storage `key`. pub fn new(key: storage::Key) -> Self { Self { key, @@ -31,62 +30,87 @@ where } } - /// insert - pub fn insert( - &self, - val: &T, - storage_write: &mut impl StorageWrite, - ) -> Result<()> { - // Do we need to read to see if this val is already in the set? - - let data_key = self.get_data_key(val); - storage_write.write(&data_key, &val)?; - Ok(()) + /// Adds a value to the set. If the set did not have this value present, + /// `Ok(true)` is returned, `Ok(false)` otherwise. + pub fn insert(&self, storage: &mut S, val: &T) -> Result + where + S: StorageWrite + StorageRead, + { + if self.contains(storage, val)? { + Ok(false) + } else { + let data_key = self.get_data_key(val); + storage.write(&data_key, &val)?; + Ok(true) + } } - /// remove - pub fn remove( - &self, - val: &T, - storage_write: &mut impl StorageWrite, - ) -> Result<()> { + /// Removes a value from the set. Returns whether the value was present in + /// the set. + pub fn remove(&self, storage: &mut S, val: &T) -> Result + where + S: StorageWrite + StorageRead, + { let data_key = self.get_data_key(val); - storage_write.delete(&data_key)?; - Ok(()) + let value: Option = storage.read(&data_key)?; + storage.delete(&data_key)?; + Ok(value.is_some()) } - /// check if the hash set contains a value + /// Returns whether the set contains a value. pub fn contains( &self, + storage: &impl StorageRead, val: &T, - storage_read: &impl StorageRead, ) -> Result { - let digest: Option = storage_read.read(&self.get_data_key(val))?; - match digest { - Some(_) => Ok(true), - None => Ok(false), - } + let value: Option = storage.read(&self.get_data_key(val))?; + Ok(value.is_some()) } - /// check if hash set is empty (if we want to do this we prob need a length - /// field) - pub fn is_empty(&self) { - todo!(); + /// Returns whether the set contains no elements. + pub fn is_empty(&self, storage: &impl StorageRead) -> Result { + let mut iter = storage.iter_prefix(&self.get_data_prefix())?; + Ok(storage.iter_next(&mut iter)?.is_none()) + } + + /// An iterator visiting all elements. The iterator element type is + /// `Result`, because iterator's call to `next` may fail with e.g. out of + /// gas or data decoding error. + /// + /// Note that this function shouldn't be used in transactions and VPs code + /// on unbounded sets to avoid gas usage increasing with the length of the + /// set. + pub fn iter<'a>( + &self, + storage: &'a impl StorageRead, + ) -> Result> + 'a> { + let iter = storage.iter_prefix(&self.get_data_prefix())?; + let iter = itertools::unfold(iter, |iter| { + match storage.iter_next(iter) { + Ok(Some((_key, value))) => { + match T::try_from_slice(&value[..]) { + Ok(decoded_value) => Some(Ok(decoded_value)), + Err(err) => Some(Err(storage_api::Error::new(err))), + } + } + Ok(None) => None, + Err(err) => { + // Propagate errors into Iterator's Item + Some(Err(err)) + } + } + }); + Ok(iter) } - fn hash_val(&self, val: &T) -> u64 { - let mut hasher = DefaultHasher::new(); - val.hash(&mut hasher); - hasher.finish() + /// Get the prefix of set's elements storage + fn get_data_prefix(&self) -> storage::Key { + self.key.push(&DATA_SUBKEY.to_owned()).unwrap() } - /// get the data subkey + /// Get the sub-key of a given element fn get_data_key(&self, val: &T) -> storage::Key { - let hash_str = self.hash_val(val).to_string(); - self.key - .push(&DATA_SUBKEY.to_owned()) - .unwrap() - .push(&hash_str) - .unwrap() + let hash_str = hash_for_storage_key(val); + self.get_data_prefix().push(&hash_str).unwrap() } } diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index 4c08f906d9..c55c39e516 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -5,7 +5,7 @@ use std::marker::PhantomData; use borsh::{BorshDeserialize, BorshSerialize}; use super::super::Result; -use crate::ledger::storage_api::{StorageRead, StorageWrite}; +use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; use crate::types::storage; /// Subkey pointing to the length of the LazyVec @@ -23,7 +23,7 @@ impl LazyVec where T: BorshSerialize + BorshDeserialize, { - /// new + /// Create or use an existing vector with the given storage `key`. pub fn new(key: storage::Key) -> Self { Self { key, @@ -31,78 +31,105 @@ where } } - /// push - pub fn push( - &self, - val: T, - storage_read: &impl StorageRead, - storage_write: &mut impl StorageWrite, - ) -> Result<()> { - let len = self.read_len(storage_read)?; + /// Appends an element to the back of a collection. + pub fn push(&self, storage: &mut S, val: T) -> Result<()> + where + S: StorageWrite + StorageRead, + { + let len = self.len(storage)?; + let data_key = self.get_data_key(len); + storage.write(&data_key, val)?; + storage.write(&self.get_len_key(), len + 1) + } - let sub_index = len.unwrap_or(0); - let len = sub_index + 1; + /// Removes the last element from a vector and returns it, or `Ok(None)` if + /// it is empty. + + /// Note that an empty vector is completely removed from storage. + pub fn pop(&self, storage: &mut S) -> Result> + where + S: StorageWrite + StorageRead, + { + let len = self.len(storage)?; + if len == 0 { + Ok(None) + } else { + let index = len - 1; + let data_key = self.get_data_key(index); + if len == 1 { + storage.delete(&self.get_len_key())?; + } else { + storage.write(&self.get_len_key(), index)?; + } + let popped_val = storage.read(&data_key)?; + storage.delete(&data_key)?; + Ok(popped_val) + } + } - let data_key = self.get_data_key(sub_index); + /// Read an element at the index or `Ok(None)` if out of bounds. + pub fn get( + &self, + storage: &impl StorageRead, + index: u64, + ) -> Result> { + storage.read(&self.get_data_key(index)) + } - storage_write.write(&data_key, val)?; - storage_write.write(&self.get_len_key(), len)?; + /// Reads the number of elements in the vector. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self, storage: &impl StorageRead) -> Result { + let len = storage.read(&self.get_len_key())?; + Ok(len.unwrap_or_default()) + } - Ok(()) + /// Returns `true` if the vector contains no elements. + pub fn is_empty(&self, storage: &impl StorageRead) -> Result { + Ok(self.len(storage)? == 0) } - /// pop - pub fn pop( + /// An iterator visiting all elements. The iterator element type is + /// `Result`, because iterator's call to `next` may fail with e.g. out of + /// gas or data decoding error. + /// + /// Note that this function shouldn't be used in transactions and VPs code + /// on unbounded sets to avoid gas usage increasing with the length of the + /// set. + pub fn iter<'a>( &self, - storage_read: &impl StorageRead, - storage_write: &mut impl StorageWrite, - ) -> Result> { - let len = self.read_len(storage_read)?; - match len { - Some(0) | None => Ok(None), - Some(len) => { - let sub_index = len - 1; - let data_key = self.get_data_key(sub_index); - if len == 1 { - storage_write.delete(&self.get_len_key())?; - } else { - storage_write.write(&self.get_len_key(), sub_index)?; + storage: &'a impl StorageRead, + ) -> Result> + 'a> { + let iter = storage.iter_prefix(&self.get_data_prefix())?; + let iter = itertools::unfold(iter, |iter| { + match storage.iter_next(iter) { + Ok(Some((_key, value))) => { + match T::try_from_slice(&value[..]) { + Ok(decoded_value) => Some(Ok(decoded_value)), + Err(err) => Some(Err(storage_api::Error::new(err))), + } + } + Ok(None) => None, + Err(err) => { + // Propagate errors into Iterator's Item + Some(Err(err)) } - let popped_val = storage_read.read(&data_key)?; - storage_write.delete(&data_key)?; - Ok(popped_val) } - } - } - - /// get the length subkey - fn get_len_key(&self) -> storage::Key { - self.key.push(&LEN_SUBKEY.to_owned()).unwrap() + }); + Ok(iter) } - /// read the length of the LazyVec - pub fn read_len( - &self, - storage_read: &impl StorageRead, - ) -> Result> { - storage_read.read(&self.get_len_key()) + /// Get the prefix of set's elements storage + fn get_data_prefix(&self) -> storage::Key { + self.key.push(&DATA_SUBKEY.to_owned()).unwrap() } - /// get the data subkey - fn get_data_key(&self, sub_index: u64) -> storage::Key { - self.key - .push(&DATA_SUBKEY.to_owned()) - .unwrap() - .push(&sub_index.to_string()) - .unwrap() + /// Get the sub-key of vector's elements storage + fn get_data_key(&self, index: u64) -> storage::Key { + self.get_data_prefix().push(&index.to_string()).unwrap() } - /// get the data held at a specific index within the data subkey - pub fn get( - &self, - sub_index: u64, - storage_read: &impl StorageRead, - ) -> Result> { - storage_read.read(&self.get_data_key(sub_index)) + /// Get the sub-key of vector's length storage + fn get_len_key(&self) -> storage::Key { + self.key.push(&LEN_SUBKEY.to_owned()).unwrap() } } diff --git a/shared/src/ledger/storage_api/collections/mod.rs b/shared/src/ledger/storage_api/collections/mod.rs index c3784c2525..b0dc43779b 100644 --- a/shared/src/ledger/storage_api/collections/mod.rs +++ b/shared/src/ledger/storage_api/collections/mod.rs @@ -7,6 +7,7 @@ //! //! CONTINUE TO UPDATE THE ABOVE +mod hasher; pub mod lazy_map; pub mod lazy_set; pub mod lazy_vec; From bc5f615c437e2f4f894988552857fbdb61eb5bde Mon Sep 17 00:00:00 2001 From: brentstone Date: Mon, 15 Aug 2022 16:38:10 -0400 Subject: [PATCH 59/81] add lazy map without hashing --- .../storage_api/collections/lazy_hashmap.rs | 171 ++++++++++++++++++ .../storage_api/collections/lazy_map.rs | 66 ++----- 2 files changed, 191 insertions(+), 46 deletions(-) create mode 100644 shared/src/ledger/storage_api/collections/lazy_hashmap.rs diff --git a/shared/src/ledger/storage_api/collections/lazy_hashmap.rs b/shared/src/ledger/storage_api/collections/lazy_hashmap.rs new file mode 100644 index 0000000000..405262b33d --- /dev/null +++ b/shared/src/ledger/storage_api/collections/lazy_hashmap.rs @@ -0,0 +1,171 @@ +//! Lazy hash map + +use std::marker::PhantomData; + +use borsh::{BorshDeserialize, BorshSerialize}; + +use super::super::Result; +use super::hasher::hash_for_storage_key; +use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; +use crate::types::storage; + +/// Subkey corresponding to the data elements of the LazyMap +pub const DATA_SUBKEY: &str = "data"; + +/// LazyHashmap ! fill in ! +pub struct LazyHashMap { + key: storage::Key, + phantom_k: PhantomData, + phantom_v: PhantomData, +} + +/// Struct to hold a key-value pair +#[derive(Debug, BorshSerialize, BorshDeserialize)] +struct KeyVal { + key: K, + val: V, +} + +impl LazyMap +where + K: BorshDeserialize + BorshSerialize, + V: BorshDeserialize + BorshSerialize, +{ + /// Create or use an existing map with the given storage `key`. + pub fn new(key: storage::Key) -> Self { + Self { + key, + phantom_k: PhantomData, + phantom_v: PhantomData, + } + } + + /// Inserts a key-value pair into the map. + /// + /// If the map did not have this key present, `None` is returned. + /// If the map did have this key present, the value is updated, and the old + /// value is returned. Unlike in `std::collection::HashMap`, the key is also + /// updated; this matters for types that can be `==` without being + /// identical. + pub fn insert( + &self, + storage: &mut S, + key: K, + val: V, + ) -> Result> + where + S: StorageWrite + StorageRead, + { + let previous = self.get(storage, &key)?; + + let data_key = self.get_data_key(&key); + Self::write_key_val(storage, &data_key, key, val)?; + + Ok(previous) + } + + /// Removes a key from the map, returning the value at the key if the key + /// was previously in the map. + pub fn remove(&self, storage: &mut S, key: &K) -> Result> + where + S: StorageWrite + StorageRead, + { + let value = self.get(storage, key)?; + + let data_key = self.get_data_key(key); + storage.delete(&data_key)?; + + Ok(value) + } + + /// Returns the value corresponding to the key, if any. + pub fn get( + &self, + storage: &impl StorageRead, + key: &K, + ) -> Result> { + let res = self.get_key_val(storage, key)?; + Ok(res.map(|elem| elem.1)) + } + + /// Returns the key-value corresponding to the key, if any. + pub fn get_key_val( + &self, + storage: &impl StorageRead, + key: &K, + ) -> Result> { + let data_key = self.get_data_key(key); + Self::read_key_val(storage, &data_key) + } + + /// Returns the key-value corresponding to the given hash of a key, if any. + pub fn get_key_val_by_hash( + &self, + storage: &impl StorageRead, + key_hash: &str, + ) -> Result> { + let data_key = + self.get_data_prefix().push(&key_hash.to_string()).unwrap(); + Self::read_key_val(storage, &data_key) + } + + /// An iterator visiting all key-value elements. The iterator element type + /// is `Result<(K, V)>`, because iterator's call to `next` may fail with + /// e.g. out of gas or data decoding error. + /// + /// Note that this function shouldn't be used in transactions and VPs code + /// on unbounded sets to avoid gas usage increasing with the length of the + /// map. + pub fn iter<'a>( + &self, + storage: &'a impl StorageRead, + ) -> Result> + 'a> { + let iter = storage.iter_prefix(&self.get_data_prefix())?; + let iter = itertools::unfold(iter, |iter| { + match storage.iter_next(iter) { + Ok(Some((_key, value))) => { + match KeyVal::::try_from_slice(&value[..]) { + Ok(KeyVal { key, val }) => Some(Ok((key, val))), + Err(err) => Some(Err(storage_api::Error::new(err))), + } + } + Ok(None) => None, + Err(err) => { + // Propagate errors into Iterator's Item + Some(Err(err)) + } + } + }); + Ok(iter) + } + + /// Reads a key-value from storage + fn read_key_val( + storage: &impl StorageRead, + storage_key: &storage::Key, + ) -> Result> { + let res = storage.read(storage_key)?; + Ok(res.map(|KeyVal { key, val }| (key, val))) + } + + /// Write a key-value into storage + fn write_key_val( + storage: &mut impl StorageWrite, + storage_key: &storage::Key, + key: K, + val: V, + ) -> Result<()> { + storage.write(storage_key, KeyVal { key, val }) + } + + /// Get the prefix of set's elements storage + fn get_data_prefix(&self) -> storage::Key { + self.key.push(&DATA_SUBKEY.to_owned()).unwrap() + } + + /// Get the sub-key of a given element + fn get_data_key(&self, key: &K) -> storage::Key { + let hash_str = hash_for_storage_key(key); + self.get_data_prefix().push(&hash_str).unwrap() + } +} diff --git a/shared/src/ledger/storage_api/collections/lazy_map.rs b/shared/src/ledger/storage_api/collections/lazy_map.rs index 25b4cd4d5d..01af3c5a42 100644 --- a/shared/src/ledger/storage_api/collections/lazy_map.rs +++ b/shared/src/ledger/storage_api/collections/lazy_map.rs @@ -1,11 +1,11 @@ -//! Lazy map +//! Lazy hash map +use std::fmt::Display; use std::marker::PhantomData; use borsh::{BorshDeserialize, BorshSerialize}; use super::super::Result; -use super::hasher::hash_for_storage_key; use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; use crate::types::storage; @@ -15,32 +15,29 @@ pub const DATA_SUBKEY: &str = "data"; /// LazyMap ! fill in ! pub struct LazyMap { key: storage::Key, - phantom_h: PhantomData, - phantom_t: PhantomData, -} - -#[derive(Debug, BorshSerialize, BorshDeserialize)] -struct KeyVal { - key: K, - val: V, + phantom_k: PhantomData, + phantom_v: PhantomData, } impl LazyMap where - K: BorshDeserialize + BorshSerialize, + K: BorshDeserialize + BorshSerialize + Display, V: BorshDeserialize + BorshSerialize, { /// Create or use an existing map with the given storage `key`. pub fn new(key: storage::Key) -> Self { Self { key, - phantom_h: PhantomData, - phantom_t: PhantomData, + phantom_k: PhantomData, + phantom_v: PhantomData, } } /// Inserts a key-value pair into the map. /// + /// The full storage key identifies the key in the pair, while the value is + /// held within the storage key. + /// /// If the map did not have this key present, `None` is returned. /// If the map did have this key present, the value is updated, and the old /// value is returned. Unlike in `std::collection::HashMap`, the key is also @@ -58,7 +55,7 @@ where let previous = self.get(storage, &key)?; let data_key = self.get_data_key(&key); - Self::write_key_val(storage, &data_key, key, val)?; + Self::write_key_val(storage, &data_key, val)?; Ok(previous) } @@ -83,31 +80,10 @@ where storage: &impl StorageRead, key: &K, ) -> Result> { - let res = self.get_key_val(storage, key)?; - Ok(res.map(|elem| elem.1)) - } - - /// Returns the key-value corresponding to the key, if any. - pub fn get_key_val( - &self, - storage: &impl StorageRead, - key: &K, - ) -> Result> { let data_key = self.get_data_key(key); Self::read_key_val(storage, &data_key) } - /// Returns the key-value corresponding to the given hash of a key, if any. - pub fn get_key_val_by_hash( - &self, - storage: &impl StorageRead, - key_hash: &str, - ) -> Result> { - let data_key = - self.get_data_prefix().push(&key_hash.to_string()).unwrap(); - Self::read_key_val(storage, &data_key) - } - /// An iterator visiting all key-value elements. The iterator element type /// is `Result<(K, V)>`, because iterator's call to `next` may fail with /// e.g. out of gas or data decoding error. @@ -118,13 +94,13 @@ where pub fn iter<'a>( &self, storage: &'a impl StorageRead, - ) -> Result> + 'a> { + ) -> Result> + 'a> { let iter = storage.iter_prefix(&self.get_data_prefix())?; let iter = itertools::unfold(iter, |iter| { match storage.iter_next(iter) { Ok(Some((_key, value))) => { - match KeyVal::::try_from_slice(&value[..]) { - Ok(KeyVal { key, val }) => Some(Ok((key, val))), + match V::try_from_slice(&value[..]) { + Ok(decoded_value) => Some(Ok(decoded_value)), Err(err) => Some(Err(storage_api::Error::new(err))), } } @@ -138,23 +114,22 @@ where Ok(iter) } - /// Reads a key-value from storage + /// Reads a value from storage fn read_key_val( storage: &impl StorageRead, storage_key: &storage::Key, - ) -> Result> { + ) -> Result> { let res = storage.read(storage_key)?; - Ok(res.map(|KeyVal { key, val }| (key, val))) + Ok(res) } - /// Write a key-value into storage + /// Write a value into storage fn write_key_val( storage: &mut impl StorageWrite, storage_key: &storage::Key, - key: K, val: V, ) -> Result<()> { - storage.write(storage_key, KeyVal { key, val }) + storage.write(storage_key, val) } /// Get the prefix of set's elements storage @@ -164,7 +139,6 @@ where /// Get the sub-key of a given element fn get_data_key(&self, key: &K) -> storage::Key { - let hash_str = hash_for_storage_key(key); - self.get_data_prefix().push(&hash_str).unwrap() + self.get_data_prefix().push(&key.to_string()).unwrap() } } From 50e2f258838e4561d64b8421051950368eba394d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 16 Aug 2022 12:51:19 +0200 Subject: [PATCH 60/81] Switch to use storage::KeySeg and add LazySet Also refactored the iterators implementation to take advantage of changes from #335. Note that this requires `'static` lifetime bound on the types of the collections' elements, which means we cannot use non-static references, but we wouldn't do that anyway. --- .../storage_api/collections/lazy_hashmap.rs | 50 ++++---- .../storage_api/collections/lazy_hashset.rs | 119 ++++++++++++++++++ .../storage_api/collections/lazy_map.rs | 58 +++++---- .../storage_api/collections/lazy_set.rs | 70 ++++++----- .../storage_api/collections/lazy_vec.rs | 35 +++--- .../src/ledger/storage_api/collections/mod.rs | 27 +++- 6 files changed, 252 insertions(+), 107 deletions(-) create mode 100644 shared/src/ledger/storage_api/collections/lazy_hashset.rs diff --git a/shared/src/ledger/storage_api/collections/lazy_hashmap.rs b/shared/src/ledger/storage_api/collections/lazy_hashmap.rs index 405262b33d..fbb76beb8b 100644 --- a/shared/src/ledger/storage_api/collections/lazy_hashmap.rs +++ b/shared/src/ledger/storage_api/collections/lazy_hashmap.rs @@ -1,4 +1,4 @@ -//! Lazy hash map +//! Lazy hash map. use std::marker::PhantomData; @@ -12,7 +12,22 @@ use crate::types::storage; /// Subkey corresponding to the data elements of the LazyMap pub const DATA_SUBKEY: &str = "data"; -/// LazyHashmap ! fill in ! +/// Lazy hash map. +/// +/// This can be used as an alternative to `std::collections::HashMap` and +/// `BTreeMap`. In the lazy map, the elements do not reside in memory but are +/// instead read and written to storage sub-keys of the storage `key` given to +/// construct the map. +/// +/// In the [`LazyHashMap`], the type of key `K` can be anything that +/// [`BorshSerialize`] and [`BorshDeserialize`] and a hex string of sha256 hash +/// over the borsh encoded keys are used as storage key segments. +/// +/// This is different from [`super::LazyMap`], which uses [`storage::KeySeg`] +/// trait. +/// +/// Additionally, [`LazyHashMap`] also writes the unhashed values into the +/// storage together with the values (using an internal `KeyVal` type). pub struct LazyHashMap { key: storage::Key, phantom_k: PhantomData, @@ -26,10 +41,10 @@ struct KeyVal { val: V, } -impl LazyMap +impl LazyHashMap where - K: BorshDeserialize + BorshSerialize, - V: BorshDeserialize + BorshSerialize, + K: BorshDeserialize + BorshSerialize + 'static, + V: BorshDeserialize + BorshSerialize + 'static, { /// Create or use an existing map with the given storage `key`. pub fn new(key: storage::Key) -> Self { @@ -85,7 +100,7 @@ where key: &K, ) -> Result> { let res = self.get_key_val(storage, key)?; - Ok(res.map(|elem| elem.1)) + Ok(res.map(|(_key, val)| val)) } /// Returns the key-value corresponding to the key, if any. @@ -120,23 +135,12 @@ where &self, storage: &'a impl StorageRead, ) -> Result> + 'a> { - let iter = storage.iter_prefix(&self.get_data_prefix())?; - let iter = itertools::unfold(iter, |iter| { - match storage.iter_next(iter) { - Ok(Some((_key, value))) => { - match KeyVal::::try_from_slice(&value[..]) { - Ok(KeyVal { key, val }) => Some(Ok((key, val))), - Err(err) => Some(Err(storage_api::Error::new(err))), - } - } - Ok(None) => None, - Err(err) => { - // Propagate errors into Iterator's Item - Some(Err(err)) - } - } - }); - Ok(iter) + let iter = storage_api::iter_prefix(storage, &self.get_data_prefix())?; + Ok(iter.map(|key_val_res| { + let (_key, val) = key_val_res?; + let KeyVal { key, val } = val; + Ok((key, val)) + })) } /// Reads a key-value from storage diff --git a/shared/src/ledger/storage_api/collections/lazy_hashset.rs b/shared/src/ledger/storage_api/collections/lazy_hashset.rs new file mode 100644 index 0000000000..ae03ff1f0e --- /dev/null +++ b/shared/src/ledger/storage_api/collections/lazy_hashset.rs @@ -0,0 +1,119 @@ +//! Lazy hash set. + +use std::marker::PhantomData; + +use borsh::{BorshDeserialize, BorshSerialize}; + +use super::super::Result; +use super::hasher::hash_for_storage_key; +use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; +use crate::types::storage; + +/// Subkey corresponding to the data elements of the LazySet +pub const DATA_SUBKEY: &str = "data"; + +/// Lazy hash set. +/// +/// This can be used as an alternative to `std::collections::HashSet` and +/// `BTreeSet`. In the lazy set, the elements do not reside in memory but are +/// instead read and written to storage sub-keys of the storage `key` given to +/// construct the set. +/// +/// In the [`LazyHashSet`], the type of value `T` can be anything that +/// [`BorshSerialize`] and [`BorshDeserialize`] and a hex string of sha256 hash +/// over the borsh encoded values are used as storage key segments. +/// +/// This is different from [`super::LazySet`], which uses [`storage::KeySeg`] +/// trait. +/// +/// Additionally, [`LazyHashSet`] also writes the unhashed values into the +/// storage. +pub struct LazyHashSet { + key: storage::Key, + phantom: PhantomData, +} + +impl LazyHashSet +where + T: BorshSerialize + BorshDeserialize + 'static, +{ + /// Create or use an existing set with the given storage `key`. + pub fn new(key: storage::Key) -> Self { + Self { + key, + phantom: PhantomData, + } + } + + /// Adds a value to the set. If the set did not have this value present, + /// `Ok(true)` is returned, `Ok(false)` otherwise. + pub fn insert(&self, storage: &mut S, val: &T) -> Result + where + S: StorageWrite + StorageRead, + { + if self.contains(storage, val)? { + Ok(false) + } else { + let data_key = self.get_data_key(val); + storage.write(&data_key, &val)?; + Ok(true) + } + } + + /// Removes a value from the set. Returns whether the value was present in + /// the set. + pub fn remove(&self, storage: &mut S, val: &T) -> Result + where + S: StorageWrite + StorageRead, + { + let data_key = self.get_data_key(val); + let value: Option = storage.read(&data_key)?; + storage.delete(&data_key)?; + Ok(value.is_some()) + } + + /// Returns whether the set contains a value. + pub fn contains( + &self, + storage: &impl StorageRead, + val: &T, + ) -> Result { + let value: Option = storage.read(&self.get_data_key(val))?; + Ok(value.is_some()) + } + + /// Returns whether the set contains no elements. + pub fn is_empty(&self, storage: &impl StorageRead) -> Result { + let mut iter = storage.iter_prefix(&self.get_data_prefix())?; + Ok(storage.iter_next(&mut iter)?.is_none()) + } + + /// An iterator visiting all elements. The iterator element type is + /// `Result`, because iterator's call to `next` may fail with e.g. out of + /// gas or data decoding error. + /// + /// Note that this function shouldn't be used in transactions and VPs code + /// on unbounded sets to avoid gas usage increasing with the length of the + /// set. + pub fn iter<'a>( + &self, + storage: &'a impl StorageRead, + ) -> Result> + 'a> { + let iter = storage_api::iter_prefix(storage, &self.get_data_prefix())?; + Ok(iter.map(|key_val_res| { + let (_key, val) = key_val_res?; + Ok(val) + })) + } + + /// Get the prefix of set's elements storage + fn get_data_prefix(&self) -> storage::Key { + self.key.push(&DATA_SUBKEY.to_owned()).unwrap() + } + + /// Get the sub-key of a given element + fn get_data_key(&self, val: &T) -> storage::Key { + let hash_str = hash_for_storage_key(val); + self.get_data_prefix().push(&hash_str).unwrap() + } +} diff --git a/shared/src/ledger/storage_api/collections/lazy_map.rs b/shared/src/ledger/storage_api/collections/lazy_map.rs index 01af3c5a42..bf7b45324b 100644 --- a/shared/src/ledger/storage_api/collections/lazy_map.rs +++ b/shared/src/ledger/storage_api/collections/lazy_map.rs @@ -1,18 +1,30 @@ -//! Lazy hash map +//! Lazy map. -use std::fmt::Display; use std::marker::PhantomData; use borsh::{BorshDeserialize, BorshSerialize}; use super::super::Result; -use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; -use crate::types::storage; +use super::ReadError; +use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; +use crate::types::storage::{self, KeySeg}; /// Subkey corresponding to the data elements of the LazyMap pub const DATA_SUBKEY: &str = "data"; -/// LazyMap ! fill in ! +/// Lazy map. +/// +/// This can be used as an alternative to `std::collections::HashMap` and +/// `BTreeMap`. In the lazy map, the elements do not reside in memory but are +/// instead read and written to storage sub-keys of the storage `key` used to +/// construct the map. +/// +/// In the [`LazyMap`], the type of key `K` can be anything that implements +/// [`storage::KeySeg`] and this trait is used to turn the keys into key +/// segments. +/// +/// This is different from [`super::LazyHashMap`], which hashes borsh encoded +/// key. pub struct LazyMap { key: storage::Key, phantom_k: PhantomData, @@ -21,8 +33,8 @@ pub struct LazyMap { impl LazyMap where - K: BorshDeserialize + BorshSerialize + Display, - V: BorshDeserialize + BorshSerialize, + K: storage::KeySeg, + V: BorshDeserialize + BorshSerialize + 'static, { /// Create or use an existing map with the given storage `key`. pub fn new(key: storage::Key) -> Self { @@ -94,24 +106,17 @@ where pub fn iter<'a>( &self, storage: &'a impl StorageRead, - ) -> Result> + 'a> { - let iter = storage.iter_prefix(&self.get_data_prefix())?; - let iter = itertools::unfold(iter, |iter| { - match storage.iter_next(iter) { - Ok(Some((_key, value))) => { - match V::try_from_slice(&value[..]) { - Ok(decoded_value) => Some(Ok(decoded_value)), - Err(err) => Some(Err(storage_api::Error::new(err))), - } - } - Ok(None) => None, - Err(err) => { - // Propagate errors into Iterator's Item - Some(Err(err)) - } - } - }); - Ok(iter) + ) -> Result> + 'a> { + let iter = storage_api::iter_prefix(storage, &self.get_data_prefix())?; + Ok(iter.map(|key_val_res| { + let (key, val) = key_val_res?; + let last_key_seg = key + .last() + .ok_or(ReadError::UnexpectedlyEmptyStorageKey) + .into_storage_result()?; + let key = K::parse(last_key_seg.raw()).into_storage_result()?; + Ok((key, val)) + })) } /// Reads a value from storage @@ -139,6 +144,7 @@ where /// Get the sub-key of a given element fn get_data_key(&self, key: &K) -> storage::Key { - self.get_data_prefix().push(&key.to_string()).unwrap() + let key_str = key.to_db_key(); + self.get_data_prefix().push(&key_str).unwrap() } } diff --git a/shared/src/ledger/storage_api/collections/lazy_set.rs b/shared/src/ledger/storage_api/collections/lazy_set.rs index 862485b687..8c1bbd871f 100644 --- a/shared/src/ledger/storage_api/collections/lazy_set.rs +++ b/shared/src/ledger/storage_api/collections/lazy_set.rs @@ -1,18 +1,28 @@ -//! Lazy hash set +//! Lazy set. use std::marker::PhantomData; -use borsh::{BorshDeserialize, BorshSerialize}; - use super::super::Result; -use super::hasher::hash_for_storage_key; -use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; -use crate::types::storage; +use super::ReadError; +use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; +use crate::types::storage::{self, KeySeg}; /// Subkey corresponding to the data elements of the LazySet pub const DATA_SUBKEY: &str = "data"; -/// lazy hash set +/// Lazy set. +/// +/// This can be used as an alternative to `std::collections::HashSet` and +/// `BTreeSet`. In the lazy set, the elements do not reside in memory but are +/// instead read and written to storage sub-keys of the storage `key` used to +/// construct the set. +/// +/// In the [`LazySet`], the type of value `T` can be anything that implements +/// [`storage::KeySeg`] and this trait is used to turn the values into key +/// segments. +/// +/// This is different from [`super::LazyHashSet`], which hashes borsh encoded +/// values. pub struct LazySet { key: storage::Key, phantom: PhantomData, @@ -20,7 +30,7 @@ pub struct LazySet { impl LazySet where - T: BorshSerialize + BorshDeserialize, + T: storage::KeySeg, { /// Create or use an existing set with the given storage `key`. pub fn new(key: storage::Key) -> Self { @@ -40,7 +50,9 @@ where Ok(false) } else { let data_key = self.get_data_key(val); - storage.write(&data_key, &val)?; + // The actual value is written into the key, so the value written to + // the storage is empty (unit) + storage.write(&data_key, ())?; Ok(true) } } @@ -52,7 +64,7 @@ where S: StorageWrite + StorageRead, { let data_key = self.get_data_key(val); - let value: Option = storage.read(&data_key)?; + let value: Option<()> = storage.read(&data_key)?; storage.delete(&data_key)?; Ok(value.is_some()) } @@ -63,14 +75,15 @@ where storage: &impl StorageRead, val: &T, ) -> Result { - let value: Option = storage.read(&self.get_data_key(val))?; + let value: Option<()> = storage.read(&self.get_data_key(val))?; Ok(value.is_some()) } /// Returns whether the set contains no elements. pub fn is_empty(&self, storage: &impl StorageRead) -> Result { - let mut iter = storage.iter_prefix(&self.get_data_prefix())?; - Ok(storage.iter_next(&mut iter)?.is_none()) + let mut iter = + storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; + Ok(iter.next().is_none()) } /// An iterator visiting all elements. The iterator element type is @@ -84,23 +97,16 @@ where &self, storage: &'a impl StorageRead, ) -> Result> + 'a> { - let iter = storage.iter_prefix(&self.get_data_prefix())?; - let iter = itertools::unfold(iter, |iter| { - match storage.iter_next(iter) { - Ok(Some((_key, value))) => { - match T::try_from_slice(&value[..]) { - Ok(decoded_value) => Some(Ok(decoded_value)), - Err(err) => Some(Err(storage_api::Error::new(err))), - } - } - Ok(None) => None, - Err(err) => { - // Propagate errors into Iterator's Item - Some(Err(err)) - } - } - }); - Ok(iter) + let iter = + storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; + Ok(iter.map(|key_val_res| { + let (key, _val) = key_val_res?; + let last_key_seg = key + .last() + .ok_or(ReadError::UnexpectedlyEmptyStorageKey) + .into_storage_result()?; + T::parse(last_key_seg.raw()).into_storage_result() + })) } /// Get the prefix of set's elements storage @@ -110,7 +116,7 @@ where /// Get the sub-key of a given element fn get_data_key(&self, val: &T) -> storage::Key { - let hash_str = hash_for_storage_key(val); - self.get_data_prefix().push(&hash_str).unwrap() + let key_str = val.to_db_key(); + self.get_data_prefix().push(&key_str).unwrap() } } diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index c55c39e516..f57797f35c 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -1,4 +1,4 @@ -//! Lazy vec +//! Lazy dynamically-sized vector. use std::marker::PhantomData; @@ -13,7 +13,12 @@ pub const LEN_SUBKEY: &str = "len"; /// Subkey corresponding to the data elements of the LazyVec pub const DATA_SUBKEY: &str = "data"; -/// LazyVec ! fill in ! +/// Lazy dynamically-sized vector. +/// +/// This can be used as an alternative to `std::collections::Vec`. In the lazy +/// vector, the elements do not reside in memory but are instead read and +/// written to storage sub-keys of the storage `key` used to construct the +/// vector. pub struct LazyVec { key: storage::Key, phantom: PhantomData, @@ -21,7 +26,7 @@ pub struct LazyVec { impl LazyVec where - T: BorshSerialize + BorshDeserialize, + T: BorshSerialize + BorshDeserialize + 'static, { /// Create or use an existing vector with the given storage `key`. pub fn new(key: storage::Key) -> Self { @@ -44,7 +49,7 @@ where /// Removes the last element from a vector and returns it, or `Ok(None)` if /// it is empty. - + /// /// Note that an empty vector is completely removed from storage. pub fn pop(&self, storage: &mut S) -> Result> where @@ -99,23 +104,11 @@ where &self, storage: &'a impl StorageRead, ) -> Result> + 'a> { - let iter = storage.iter_prefix(&self.get_data_prefix())?; - let iter = itertools::unfold(iter, |iter| { - match storage.iter_next(iter) { - Ok(Some((_key, value))) => { - match T::try_from_slice(&value[..]) { - Ok(decoded_value) => Some(Ok(decoded_value)), - Err(err) => Some(Err(storage_api::Error::new(err))), - } - } - Ok(None) => None, - Err(err) => { - // Propagate errors into Iterator's Item - Some(Err(err)) - } - } - }); - Ok(iter) + let iter = storage_api::iter_prefix(storage, &self.get_data_prefix())?; + Ok(iter.map(|key_val_res| { + let (_key, val) = key_val_res?; + Ok(val) + })) } /// Get the prefix of set's elements storage diff --git a/shared/src/ledger/storage_api/collections/mod.rs b/shared/src/ledger/storage_api/collections/mod.rs index b0dc43779b..156615b9de 100644 --- a/shared/src/ledger/storage_api/collections/mod.rs +++ b/shared/src/ledger/storage_api/collections/mod.rs @@ -1,13 +1,30 @@ //! Lazy data structures for storage access where elements are not all loaded //! into memory. This serves to minimize gas costs, avoid unbounded iteration -//! in some cases, and ease the validation of storage changes in the VP. +//! in some cases, and ease the validation of storage changes in VPs. //! -//! Rather than finding the diff of the state before and after, the VP will -//! just receive the storage sub-keys that have experienced changes. -//! -//! CONTINUE TO UPDATE THE ABOVE +//! Rather than finding the diff of the state before and after (which requires +//! iteration over both of the states that also have to be decoded), VPs will +//! just receive the storage sub-keys that have experienced changes without +//! having to check any of the unchanged elements. + +use thiserror::Error; mod hasher; +pub mod lazy_hashmap; +pub mod lazy_hashset; pub mod lazy_map; pub mod lazy_set; pub mod lazy_vec; + +pub use lazy_hashmap::LazyHashMap; +pub use lazy_hashset::LazyHashSet; +pub use lazy_map::LazyMap; +pub use lazy_set::LazySet; +pub use lazy_vec::LazyVec; + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum ReadError { + #[error("A storage key was unexpectedly empty")] + UnexpectedlyEmptyStorageKey, +} From 12ce117920cc0364b2aa602ba0682ad93ccfb303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 16 Aug 2022 12:54:54 +0200 Subject: [PATCH 61/81] storage: add `Key::last` method, impl KeySeg for all ints --- shared/src/types/storage.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/shared/src/types/storage.rs b/shared/src/types/storage.rs index 1892334031..289f115c8f 100644 --- a/shared/src/types/storage.rs +++ b/shared/src/types/storage.rs @@ -278,6 +278,11 @@ impl Key { self.len() == 0 } + /// Returns the last segment of the key, or `None` if it is empty. + pub fn last(&self) -> Option<&DbKeySeg> { + self.segments.last() + } + /// Returns a key of the validity predicate of the given address /// Only this function can push "?" segment for validity predicate pub fn validity_predicate(addr: &Address) -> Self { @@ -321,8 +326,11 @@ impl Key { .split_off(2) .join(&KEY_SEGMENT_SEPARATOR.to_string()), ) - .map_err(|e| Error::Temporary { - error: format!("Cannot parse key segments {}: {}", db_key, e), + .map_err(|e| { + Error::ParseKeySeg(format!( + "Cannot parse key segments {}: {}", + db_key, e + )) })?, }; Ok(key) @@ -450,7 +458,12 @@ impl KeySeg for String { impl KeySeg for BlockHeight { fn parse(string: String) -> Result { - let h: u64 = KeySeg::parse(string)?; + let h = string.parse::().map_err(|e| { + Error::ParseKeySeg(format!( + "Unexpected height value {}, {}", + string, e + )) + })?; Ok(BlockHeight(h)) } From bb18f2b79562c2ec3e0fddf655c196e6a4e633a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 16 Aug 2022 16:41:27 +0200 Subject: [PATCH 62/81] update lazy for explicit lifetime in StorageRead trait --- .../storage_api/collections/lazy_hashmap.rs | 46 ++++++++++--------- .../storage_api/collections/lazy_hashset.rs | 24 +++++----- .../storage_api/collections/lazy_map.rs | 28 +++++------ .../storage_api/collections/lazy_set.rs | 24 +++++----- .../storage_api/collections/lazy_vec.rs | 29 +++++++----- 5 files changed, 83 insertions(+), 68 deletions(-) diff --git a/shared/src/ledger/storage_api/collections/lazy_hashmap.rs b/shared/src/ledger/storage_api/collections/lazy_hashmap.rs index fbb76beb8b..d626b75451 100644 --- a/shared/src/ledger/storage_api/collections/lazy_hashmap.rs +++ b/shared/src/ledger/storage_api/collections/lazy_hashmap.rs @@ -69,7 +69,7 @@ where val: V, ) -> Result> where - S: StorageWrite + StorageRead, + S: StorageWrite + for<'iter> StorageRead<'iter>, { let previous = self.get(storage, &key)?; @@ -83,7 +83,7 @@ where /// was previously in the map. pub fn remove(&self, storage: &mut S, key: &K) -> Result> where - S: StorageWrite + StorageRead, + S: StorageWrite + for<'iter> StorageRead<'iter>, { let value = self.get(storage, key)?; @@ -94,31 +94,32 @@ where } /// Returns the value corresponding to the key, if any. - pub fn get( - &self, - storage: &impl StorageRead, - key: &K, - ) -> Result> { + pub fn get(&self, storage: &S, key: &K) -> Result> + where + S: for<'iter> StorageRead<'iter>, + { let res = self.get_key_val(storage, key)?; Ok(res.map(|(_key, val)| val)) } /// Returns the key-value corresponding to the key, if any. - pub fn get_key_val( - &self, - storage: &impl StorageRead, - key: &K, - ) -> Result> { + pub fn get_key_val(&self, storage: &S, key: &K) -> Result> + where + S: for<'iter> StorageRead<'iter>, + { let data_key = self.get_data_key(key); Self::read_key_val(storage, &data_key) } /// Returns the key-value corresponding to the given hash of a key, if any. - pub fn get_key_val_by_hash( + pub fn get_key_val_by_hash( &self, - storage: &impl StorageRead, + storage: &S, key_hash: &str, - ) -> Result> { + ) -> Result> + where + S: for<'iter> StorageRead<'iter>, + { let data_key = self.get_data_prefix().push(&key_hash.to_string()).unwrap(); Self::read_key_val(storage, &data_key) @@ -131,10 +132,10 @@ where /// Note that this function shouldn't be used in transactions and VPs code /// on unbounded sets to avoid gas usage increasing with the length of the /// map. - pub fn iter<'a>( + pub fn iter<'iter, S>( &self, - storage: &'a impl StorageRead, - ) -> Result> + 'a> { + storage: &'iter impl StorageRead<'iter>, + ) -> Result> + 'iter> { let iter = storage_api::iter_prefix(storage, &self.get_data_prefix())?; Ok(iter.map(|key_val_res| { let (_key, val) = key_val_res?; @@ -144,10 +145,13 @@ where } /// Reads a key-value from storage - fn read_key_val( - storage: &impl StorageRead, + fn read_key_val( + storage: &S, storage_key: &storage::Key, - ) -> Result> { + ) -> Result> + where + S: for<'iter> StorageRead<'iter>, + { let res = storage.read(storage_key)?; Ok(res.map(|KeyVal { key, val }| (key, val))) } diff --git a/shared/src/ledger/storage_api/collections/lazy_hashset.rs b/shared/src/ledger/storage_api/collections/lazy_hashset.rs index ae03ff1f0e..96a31ecef7 100644 --- a/shared/src/ledger/storage_api/collections/lazy_hashset.rs +++ b/shared/src/ledger/storage_api/collections/lazy_hashset.rs @@ -49,7 +49,7 @@ where /// `Ok(true)` is returned, `Ok(false)` otherwise. pub fn insert(&self, storage: &mut S, val: &T) -> Result where - S: StorageWrite + StorageRead, + S: StorageWrite + for<'iter> StorageRead<'iter>, { if self.contains(storage, val)? { Ok(false) @@ -64,7 +64,7 @@ where /// the set. pub fn remove(&self, storage: &mut S, val: &T) -> Result where - S: StorageWrite + StorageRead, + S: StorageWrite + for<'iter> StorageRead<'iter>, { let data_key = self.get_data_key(val); let value: Option = storage.read(&data_key)?; @@ -73,17 +73,19 @@ where } /// Returns whether the set contains a value. - pub fn contains( - &self, - storage: &impl StorageRead, - val: &T, - ) -> Result { + pub fn contains(&self, storage: &S, val: &T) -> Result + where + S: for<'iter> StorageRead<'iter>, + { let value: Option = storage.read(&self.get_data_key(val))?; Ok(value.is_some()) } /// Returns whether the set contains no elements. - pub fn is_empty(&self, storage: &impl StorageRead) -> Result { + pub fn is_empty(&self, storage: &S) -> Result + where + S: for<'iter> StorageRead<'iter>, + { let mut iter = storage.iter_prefix(&self.get_data_prefix())?; Ok(storage.iter_next(&mut iter)?.is_none()) } @@ -95,10 +97,10 @@ where /// Note that this function shouldn't be used in transactions and VPs code /// on unbounded sets to avoid gas usage increasing with the length of the /// set. - pub fn iter<'a>( + pub fn iter<'iter>( &self, - storage: &'a impl StorageRead, - ) -> Result> + 'a> { + storage: &'iter impl StorageRead<'iter>, + ) -> Result> + 'iter> { let iter = storage_api::iter_prefix(storage, &self.get_data_prefix())?; Ok(iter.map(|key_val_res| { let (_key, val) = key_val_res?; diff --git a/shared/src/ledger/storage_api/collections/lazy_map.rs b/shared/src/ledger/storage_api/collections/lazy_map.rs index bf7b45324b..6da5c97031 100644 --- a/shared/src/ledger/storage_api/collections/lazy_map.rs +++ b/shared/src/ledger/storage_api/collections/lazy_map.rs @@ -62,7 +62,7 @@ where val: V, ) -> Result> where - S: StorageWrite + StorageRead, + S: StorageWrite + for<'iter> StorageRead<'iter>, { let previous = self.get(storage, &key)?; @@ -76,7 +76,7 @@ where /// was previously in the map. pub fn remove(&self, storage: &mut S, key: &K) -> Result> where - S: StorageWrite + StorageRead, + S: StorageWrite + for<'iter> StorageRead<'iter>, { let value = self.get(storage, key)?; @@ -87,11 +87,10 @@ where } /// Returns the value corresponding to the key, if any. - pub fn get( - &self, - storage: &impl StorageRead, - key: &K, - ) -> Result> { + pub fn get(&self, storage: &S, key: &K) -> Result> + where + S: for<'iter> StorageRead<'iter>, + { let data_key = self.get_data_key(key); Self::read_key_val(storage, &data_key) } @@ -103,10 +102,10 @@ where /// Note that this function shouldn't be used in transactions and VPs code /// on unbounded sets to avoid gas usage increasing with the length of the /// map. - pub fn iter<'a>( + pub fn iter<'iter>( &self, - storage: &'a impl StorageRead, - ) -> Result> + 'a> { + storage: &'iter impl StorageRead<'iter>, + ) -> Result> + 'iter> { let iter = storage_api::iter_prefix(storage, &self.get_data_prefix())?; Ok(iter.map(|key_val_res| { let (key, val) = key_val_res?; @@ -120,10 +119,13 @@ where } /// Reads a value from storage - fn read_key_val( - storage: &impl StorageRead, + fn read_key_val( + storage: &S, storage_key: &storage::Key, - ) -> Result> { + ) -> Result> + where + S: for<'iter> StorageRead<'iter>, + { let res = storage.read(storage_key)?; Ok(res) } diff --git a/shared/src/ledger/storage_api/collections/lazy_set.rs b/shared/src/ledger/storage_api/collections/lazy_set.rs index 8c1bbd871f..1e1f259c55 100644 --- a/shared/src/ledger/storage_api/collections/lazy_set.rs +++ b/shared/src/ledger/storage_api/collections/lazy_set.rs @@ -44,7 +44,7 @@ where /// `Ok(true)` is returned, `Ok(false)` otherwise. pub fn insert(&self, storage: &mut S, val: &T) -> Result where - S: StorageWrite + StorageRead, + S: StorageWrite + for<'iter> StorageRead<'iter>, { if self.contains(storage, val)? { Ok(false) @@ -61,7 +61,7 @@ where /// the set. pub fn remove(&self, storage: &mut S, val: &T) -> Result where - S: StorageWrite + StorageRead, + S: StorageWrite + for<'iter> StorageRead<'iter>, { let data_key = self.get_data_key(val); let value: Option<()> = storage.read(&data_key)?; @@ -70,17 +70,19 @@ where } /// Returns whether the set contains a value. - pub fn contains( - &self, - storage: &impl StorageRead, - val: &T, - ) -> Result { + pub fn contains(&self, storage: &S, val: &T) -> Result + where + S: for<'iter> StorageRead<'iter>, + { let value: Option<()> = storage.read(&self.get_data_key(val))?; Ok(value.is_some()) } /// Returns whether the set contains no elements. - pub fn is_empty(&self, storage: &impl StorageRead) -> Result { + pub fn is_empty(&self, storage: &S) -> Result + where + S: for<'iter> StorageRead<'iter>, + { let mut iter = storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; Ok(iter.next().is_none()) @@ -93,10 +95,10 @@ where /// Note that this function shouldn't be used in transactions and VPs code /// on unbounded sets to avoid gas usage increasing with the length of the /// set. - pub fn iter<'a>( + pub fn iter<'iter>( &self, - storage: &'a impl StorageRead, - ) -> Result> + 'a> { + storage: &'iter impl StorageRead<'iter>, + ) -> Result> + 'iter> { let iter = storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; Ok(iter.map(|key_val_res| { diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index f57797f35c..e6a186c408 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -39,7 +39,7 @@ where /// Appends an element to the back of a collection. pub fn push(&self, storage: &mut S, val: T) -> Result<()> where - S: StorageWrite + StorageRead, + S: StorageWrite + for<'iter> StorageRead<'iter>, { let len = self.len(storage)?; let data_key = self.get_data_key(len); @@ -53,7 +53,7 @@ where /// Note that an empty vector is completely removed from storage. pub fn pop(&self, storage: &mut S) -> Result> where - S: StorageWrite + StorageRead, + S: StorageWrite + for<'iter> StorageRead<'iter>, { let len = self.len(storage)?; if len == 0 { @@ -73,23 +73,28 @@ where } /// Read an element at the index or `Ok(None)` if out of bounds. - pub fn get( - &self, - storage: &impl StorageRead, - index: u64, - ) -> Result> { + pub fn get(&self, storage: &S, index: u64) -> Result> + where + S: for<'iter> StorageRead<'iter>, + { storage.read(&self.get_data_key(index)) } /// Reads the number of elements in the vector. #[allow(clippy::len_without_is_empty)] - pub fn len(&self, storage: &impl StorageRead) -> Result { + pub fn len(&self, storage: &S) -> Result + where + S: for<'iter> StorageRead<'iter>, + { let len = storage.read(&self.get_len_key())?; Ok(len.unwrap_or_default()) } /// Returns `true` if the vector contains no elements. - pub fn is_empty(&self, storage: &impl StorageRead) -> Result { + pub fn is_empty(&self, storage: &S) -> Result + where + S: for<'iter> StorageRead<'iter>, + { Ok(self.len(storage)? == 0) } @@ -100,10 +105,10 @@ where /// Note that this function shouldn't be used in transactions and VPs code /// on unbounded sets to avoid gas usage increasing with the length of the /// set. - pub fn iter<'a>( + pub fn iter<'iter>( &self, - storage: &'a impl StorageRead, - ) -> Result> + 'a> { + storage: &'iter impl StorageRead<'iter>, + ) -> Result> + 'iter> { let iter = storage_api::iter_prefix(storage, &self.get_data_prefix())?; Ok(iter.map(|key_val_res| { let (_key, val) = key_val_res?; From 22c2ea54591761e92a79b3f2b2c4de41d66d5eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 16 Aug 2022 15:01:16 +0200 Subject: [PATCH 63/81] cargo test test_lazy_vec_basics --- shared/src/ledger/storage/mod.rs | 40 +++++++++++++++++- .../storage_api/collections/lazy_vec.rs | 42 +++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/shared/src/ledger/storage/mod.rs b/shared/src/ledger/storage/mod.rs index 1c4e4efd94..3a1bfa697e 100644 --- a/shared/src/ledger/storage/mod.rs +++ b/shared/src/ledger/storage/mod.rs @@ -459,7 +459,7 @@ where // Note that this method is the same as `StorageWrite::delete`, // but with gas and storage bytes len diff accounting let mut deleted_bytes_len = 0; - if self.has_key(key)?.0 { + if Self::has_key(&self, key)?.0 { self.block.tree.delete(key)?; deleted_bytes_len = self.db.delete_subspace_val(self.last_height, key)?; @@ -808,6 +808,44 @@ where } } +impl StorageWrite for &mut Storage +where + D: DB + for<'iter> DBIter<'iter>, + H: StorageHasher, +{ + fn write( + &mut self, + key: &crate::types::storage::Key, + val: T, + ) -> storage_api::Result<()> { + let val = val.try_to_vec().unwrap(); + self.write_bytes(key, val) + } + + fn write_bytes( + &mut self, + key: &crate::types::storage::Key, + val: impl AsRef<[u8]>, + ) -> storage_api::Result<()> { + let _ = self + .db + .write_subspace_val(self.block.height, key, val) + .into_storage_result()?; + Ok(()) + } + + fn delete( + &mut self, + key: &crate::types::storage::Key, + ) -> storage_api::Result<()> { + let _ = self + .db + .delete_subspace_val(self.block.height, key) + .into_storage_result()?; + Ok(()) + } +} + impl From for Error { fn from(error: MerkleTreeError) -> Self { Self::MerkleTreeError(error) diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index e6a186c408..73989bf942 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -131,3 +131,45 @@ where self.key.push(&LEN_SUBKEY.to_owned()).unwrap() } } + +#[cfg(test)] +mod test { + use super::*; + use crate::ledger::storage::testing::TestStorage; + + #[test] + fn test_lazy_vec_basics() -> storage_api::Result<()> { + let mut storage = TestStorage::default(); + + let key = storage::Key::parse("test").unwrap(); + let lazy_vec = LazyVec::::new(key); + + // The vec should be empty at first + assert!(lazy_vec.is_empty(&storage)?); + assert!(lazy_vec.len(&storage)? == 0); + assert!(lazy_vec.iter(&storage)?.next().is_none()); + assert!(lazy_vec.pop(&mut storage)?.is_none()); + assert!(lazy_vec.get(&storage, 0)?.is_none()); + assert!(lazy_vec.get(&storage, 1)?.is_none()); + + // Push a new value and check that it's added + lazy_vec.push(&mut storage, 15_u32)?; + assert!(!lazy_vec.is_empty(&storage)?); + assert!(lazy_vec.len(&storage)? == 1); + assert_eq!(lazy_vec.iter(&storage)?.next().unwrap()?, 15_u32); + assert_eq!(lazy_vec.get(&storage, 0)?.unwrap(), 15_u32); + assert!(lazy_vec.get(&storage, 1)?.is_none()); + + // Pop the last value and check that the vec is empty again + let popped = lazy_vec.pop(&mut storage)?.unwrap(); + assert_eq!(popped, 15_u32); + assert!(lazy_vec.is_empty(&storage)?); + assert!(lazy_vec.len(&storage)? == 0); + assert!(lazy_vec.iter(&storage)?.next().is_none()); + assert!(lazy_vec.pop(&mut storage)?.is_none()); + assert!(lazy_vec.get(&storage, 0)?.is_none()); + assert!(lazy_vec.get(&storage, 1)?.is_none()); + + Ok(()) + } +} From fff77348a1d18879786e44fdef1bb3d026f2d472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 16 Aug 2022 17:47:36 +0200 Subject: [PATCH 64/81] storage_api/collections/lazy: add basic tests and missing methods --- .../storage_api/collections/lazy_hashmap.rs | 96 ++++++++++++++++++- .../storage_api/collections/lazy_hashset.rs | 67 ++++++++++++- .../storage_api/collections/lazy_map.rs | 92 +++++++++++++++++- .../storage_api/collections/lazy_set.rs | 72 +++++++++++++- 4 files changed, 314 insertions(+), 13 deletions(-) diff --git a/shared/src/ledger/storage_api/collections/lazy_hashmap.rs b/shared/src/ledger/storage_api/collections/lazy_hashmap.rs index d626b75451..f3330df8e3 100644 --- a/shared/src/ledger/storage_api/collections/lazy_hashmap.rs +++ b/shared/src/ledger/storage_api/collections/lazy_hashmap.rs @@ -6,7 +6,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use super::super::Result; use super::hasher::hash_for_storage_key; -use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; +use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; use crate::types::storage; /// Subkey corresponding to the data elements of the LazyMap @@ -125,14 +125,51 @@ where Self::read_key_val(storage, &data_key) } + /// Returns whether the set contains a value. + pub fn contains(&self, storage: &S, key: &K) -> Result + where + S: for<'iter> StorageRead<'iter>, + { + storage.has_key(&self.get_data_key(key)) + } + + /// Reads the number of elements in the map. + /// + /// Note that this function shouldn't be used in transactions and VPs code + /// on unbounded maps to avoid gas usage increasing with the length of the + /// set. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self, storage: &S) -> Result + where + S: for<'iter> StorageRead<'iter>, + { + let iter = + storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; + iter.count().try_into().into_storage_result() + } + + /// Returns whether the map contains no elements. + /// + /// Note that this function shouldn't be used in transactions and VPs code + /// on unbounded maps to avoid gas usage increasing with the length of the + /// set. + pub fn is_empty(&self, storage: &S) -> Result + where + S: for<'iter> StorageRead<'iter>, + { + let mut iter = + storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; + Ok(iter.next().is_none()) + } + /// An iterator visiting all key-value elements. The iterator element type /// is `Result<(K, V)>`, because iterator's call to `next` may fail with /// e.g. out of gas or data decoding error. /// /// Note that this function shouldn't be used in transactions and VPs code - /// on unbounded sets to avoid gas usage increasing with the length of the + /// on unbounded maps to avoid gas usage increasing with the length of the /// map. - pub fn iter<'iter, S>( + pub fn iter<'iter>( &self, storage: &'iter impl StorageRead<'iter>, ) -> Result> + 'iter> { @@ -177,3 +214,56 @@ where self.get_data_prefix().push(&hash_str).unwrap() } } + +#[cfg(test)] +mod test { + use super::*; + use crate::ledger::storage::testing::TestStorage; + + #[test] + fn test_lazy_hash_map_basics() -> storage_api::Result<()> { + let mut storage = TestStorage::default(); + + let key = storage::Key::parse("test").unwrap(); + let lazy_map = LazyHashMap::::new(key); + + // The map should be empty at first + assert!(lazy_map.is_empty(&storage)?); + assert!(lazy_map.len(&storage)? == 0); + assert!(!lazy_map.contains(&storage, &0)?); + assert!(!lazy_map.contains(&storage, &1)?); + assert!(lazy_map.iter(&storage)?.next().is_none()); + assert!(lazy_map.get(&storage, &0)?.is_none()); + assert!(lazy_map.get(&storage, &1)?.is_none()); + assert!(lazy_map.remove(&mut storage, &0)?.is_none()); + assert!(lazy_map.remove(&mut storage, &1)?.is_none()); + + // Insert a new value and check that it's added + let (key, val) = (123, "Test".to_string()); + lazy_map.insert(&mut storage, key, val.clone())?; + assert!(!lazy_map.contains(&storage, &0)?); + assert!(lazy_map.contains(&storage, &key)?); + assert!(!lazy_map.is_empty(&storage)?); + assert!(lazy_map.len(&storage)? == 1); + assert_eq!( + lazy_map.iter(&storage)?.next().unwrap()?, + (key, val.clone()) + ); + assert!(lazy_map.get(&storage, &0)?.is_none()); + assert_eq!(lazy_map.get(&storage, &key)?.unwrap(), val); + + // Remove the last value and check that the map is empty again + let removed = lazy_map.remove(&mut storage, &key)?.unwrap(); + assert_eq!(removed, val); + assert!(lazy_map.is_empty(&storage)?); + assert!(lazy_map.len(&storage)? == 0); + assert!(!lazy_map.contains(&storage, &0)?); + assert!(!lazy_map.contains(&storage, &1)?); + assert!(lazy_map.get(&storage, &0)?.is_none()); + assert!(lazy_map.get(&storage, &key)?.is_none()); + assert!(lazy_map.iter(&storage)?.next().is_none()); + assert!(lazy_map.remove(&mut storage, &key)?.is_none()); + + Ok(()) + } +} diff --git a/shared/src/ledger/storage_api/collections/lazy_hashset.rs b/shared/src/ledger/storage_api/collections/lazy_hashset.rs index 96a31ecef7..c9bd01a34c 100644 --- a/shared/src/ledger/storage_api/collections/lazy_hashset.rs +++ b/shared/src/ledger/storage_api/collections/lazy_hashset.rs @@ -6,7 +6,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use super::super::Result; use super::hasher::hash_for_storage_key; -use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; +use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; use crate::types::storage; /// Subkey corresponding to the data elements of the LazySet @@ -47,14 +47,14 @@ where /// Adds a value to the set. If the set did not have this value present, /// `Ok(true)` is returned, `Ok(false)` otherwise. - pub fn insert(&self, storage: &mut S, val: &T) -> Result + pub fn insert(&self, storage: &mut S, val: T) -> Result where S: StorageWrite + for<'iter> StorageRead<'iter>, { - if self.contains(storage, val)? { + if self.contains(storage, &val)? { Ok(false) } else { - let data_key = self.get_data_key(val); + let data_key = self.get_data_key(&val); storage.write(&data_key, &val)?; Ok(true) } @@ -81,6 +81,21 @@ where Ok(value.is_some()) } + /// Reads the number of elements in the set. + /// + /// Note that this function shouldn't be used in transactions and VPs code + /// on unbounded sets to avoid gas usage increasing with the length of the + /// set. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self, storage: &S) -> Result + where + S: for<'iter> StorageRead<'iter>, + { + let iter = + storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; + iter.count().try_into().into_storage_result() + } + /// Returns whether the set contains no elements. pub fn is_empty(&self, storage: &S) -> Result where @@ -119,3 +134,47 @@ where self.get_data_prefix().push(&hash_str).unwrap() } } + +#[cfg(test)] +mod test { + use super::*; + use crate::ledger::storage::testing::TestStorage; + + #[test] + fn test_lazy_set_basics() -> storage_api::Result<()> { + let mut storage = TestStorage::default(); + + let key = storage::Key::parse("test").unwrap(); + let lazy_set = LazyHashSet::::new(key); + + // The set should be empty at first + assert!(lazy_set.is_empty(&storage)?); + assert!(lazy_set.len(&storage)? == 0); + assert!(!lazy_set.contains(&storage, &0)?); + assert!(lazy_set.is_empty(&storage)?); + assert!(lazy_set.iter(&storage)?.next().is_none()); + assert!(!lazy_set.remove(&mut storage, &0)?); + assert!(!lazy_set.remove(&mut storage, &1)?); + + // Insert a new value and check that it's added + let val = 1337; + lazy_set.insert(&mut storage, val)?; + assert!(!lazy_set.is_empty(&storage)?); + assert!(lazy_set.len(&storage)? == 1); + assert_eq!(lazy_set.iter(&storage)?.next().unwrap()?, val.clone()); + assert!(!lazy_set.contains(&storage, &0)?); + assert!(lazy_set.contains(&storage, &val)?); + + // Remove the last value and check that the set is empty again + let is_removed = lazy_set.remove(&mut storage, &val)?; + assert!(is_removed); + assert!(lazy_set.is_empty(&storage)?); + assert!(lazy_set.len(&storage)? == 0); + assert!(!lazy_set.contains(&storage, &0)?); + assert!(lazy_set.is_empty(&storage)?); + assert!(!lazy_set.remove(&mut storage, &0)?); + assert!(!lazy_set.remove(&mut storage, &1)?); + + Ok(()) + } +} diff --git a/shared/src/ledger/storage_api/collections/lazy_map.rs b/shared/src/ledger/storage_api/collections/lazy_map.rs index 6da5c97031..a89cb02469 100644 --- a/shared/src/ledger/storage_api/collections/lazy_map.rs +++ b/shared/src/ledger/storage_api/collections/lazy_map.rs @@ -95,12 +95,49 @@ where Self::read_key_val(storage, &data_key) } + /// Returns whether the set contains a value. + pub fn contains(&self, storage: &S, key: &K) -> Result + where + S: for<'iter> StorageRead<'iter>, + { + storage.has_key(&self.get_data_key(key)) + } + + /// Reads the number of elements in the map. + /// + /// Note that this function shouldn't be used in transactions and VPs code + /// on unbounded maps to avoid gas usage increasing with the length of the + /// set. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self, storage: &S) -> Result + where + S: for<'iter> StorageRead<'iter>, + { + let iter = + storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; + iter.count().try_into().into_storage_result() + } + + /// Returns whether the map contains no elements. + /// + /// Note that this function shouldn't be used in transactions and VPs code + /// on unbounded maps to avoid gas usage increasing with the length of the + /// set. + pub fn is_empty(&self, storage: &S) -> Result + where + S: for<'iter> StorageRead<'iter>, + { + let mut iter = + storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; + Ok(iter.next().is_none()) + } + /// An iterator visiting all key-value elements. The iterator element type /// is `Result<(K, V)>`, because iterator's call to `next` may fail with /// e.g. out of gas or data decoding error. /// /// Note that this function shouldn't be used in transactions and VPs code - /// on unbounded sets to avoid gas usage increasing with the length of the + /// on unbounded maps to avoid gas usage increasing with the length of the /// map. pub fn iter<'iter>( &self, @@ -150,3 +187,56 @@ where self.get_data_prefix().push(&key_str).unwrap() } } + +#[cfg(test)] +mod test { + use super::*; + use crate::ledger::storage::testing::TestStorage; + + #[test] + fn test_lazy_map_basics() -> storage_api::Result<()> { + let mut storage = TestStorage::default(); + + let key = storage::Key::parse("test").unwrap(); + let lazy_map = LazyMap::::new(key); + + // The map should be empty at first + assert!(lazy_map.is_empty(&storage)?); + assert!(lazy_map.len(&storage)? == 0); + assert!(!lazy_map.contains(&storage, &0)?); + assert!(!lazy_map.contains(&storage, &1)?); + assert!(lazy_map.iter(&storage)?.next().is_none()); + assert!(lazy_map.get(&storage, &0)?.is_none()); + assert!(lazy_map.get(&storage, &1)?.is_none()); + assert!(lazy_map.remove(&mut storage, &0)?.is_none()); + assert!(lazy_map.remove(&mut storage, &1)?.is_none()); + + // Insert a new value and check that it's added + let (key, val) = (123, "Test".to_string()); + lazy_map.insert(&mut storage, key, val.clone())?; + assert!(!lazy_map.contains(&storage, &0)?); + assert!(lazy_map.contains(&storage, &key)?); + assert!(!lazy_map.is_empty(&storage)?); + assert!(lazy_map.len(&storage)? == 1); + assert_eq!( + lazy_map.iter(&storage)?.next().unwrap()?, + (key, val.clone()) + ); + assert!(lazy_map.get(&storage, &0)?.is_none()); + assert_eq!(lazy_map.get(&storage, &key)?.unwrap(), val); + + // Remove the last value and check that the map is empty again + let removed = lazy_map.remove(&mut storage, &key)?.unwrap(); + assert_eq!(removed, val); + assert!(lazy_map.is_empty(&storage)?); + assert!(lazy_map.len(&storage)? == 0); + assert!(!lazy_map.contains(&storage, &0)?); + assert!(!lazy_map.contains(&storage, &1)?); + assert!(lazy_map.get(&storage, &0)?.is_none()); + assert!(lazy_map.get(&storage, &key)?.is_none()); + assert!(lazy_map.iter(&storage)?.next().is_none()); + assert!(lazy_map.remove(&mut storage, &key)?.is_none()); + + Ok(()) + } +} diff --git a/shared/src/ledger/storage_api/collections/lazy_set.rs b/shared/src/ledger/storage_api/collections/lazy_set.rs index 1e1f259c55..0bf533dea9 100644 --- a/shared/src/ledger/storage_api/collections/lazy_set.rs +++ b/shared/src/ledger/storage_api/collections/lazy_set.rs @@ -42,14 +42,14 @@ where /// Adds a value to the set. If the set did not have this value present, /// `Ok(true)` is returned, `Ok(false)` otherwise. - pub fn insert(&self, storage: &mut S, val: &T) -> Result + pub fn insert(&self, storage: &mut S, val: T) -> Result where S: StorageWrite + for<'iter> StorageRead<'iter>, { - if self.contains(storage, val)? { + if self.contains(storage, &val)? { Ok(false) } else { - let data_key = self.get_data_key(val); + let data_key = self.get_data_key(&val); // The actual value is written into the key, so the value written to // the storage is empty (unit) storage.write(&data_key, ())?; @@ -74,11 +74,29 @@ where where S: for<'iter> StorageRead<'iter>, { - let value: Option<()> = storage.read(&self.get_data_key(val))?; - Ok(value.is_some()) + storage.has_key(&self.get_data_key(val)) + } + + /// Reads the number of elements in the set. + /// + /// Note that this function shouldn't be used in transactions and VPs code + /// on unbounded sets to avoid gas usage increasing with the length of the + /// set. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self, storage: &S) -> Result + where + S: for<'iter> StorageRead<'iter>, + { + let iter = + storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; + iter.count().try_into().into_storage_result() } /// Returns whether the set contains no elements. + /// + /// Note that this function shouldn't be used in transactions and VPs code + /// on unbounded sets to avoid gas usage increasing with the length of the + /// set. pub fn is_empty(&self, storage: &S) -> Result where S: for<'iter> StorageRead<'iter>, @@ -122,3 +140,47 @@ where self.get_data_prefix().push(&key_str).unwrap() } } + +#[cfg(test)] +mod test { + use super::*; + use crate::ledger::storage::testing::TestStorage; + + #[test] + fn test_lazy_set_basics() -> storage_api::Result<()> { + let mut storage = TestStorage::default(); + + let key = storage::Key::parse("test").unwrap(); + let lazy_set = LazySet::::new(key); + + // The set should be empty at first + assert!(lazy_set.is_empty(&storage)?); + assert!(lazy_set.len(&storage)? == 0); + assert!(!lazy_set.contains(&storage, &0)?); + assert!(lazy_set.is_empty(&storage)?); + assert!(lazy_set.iter(&storage)?.next().is_none()); + assert!(!lazy_set.remove(&mut storage, &0)?); + assert!(!lazy_set.remove(&mut storage, &1)?); + + // Insert a new value and check that it's added + let val = 1337; + lazy_set.insert(&mut storage, val)?; + assert!(!lazy_set.is_empty(&storage)?); + assert!(lazy_set.len(&storage)? == 1); + assert_eq!(lazy_set.iter(&storage)?.next().unwrap()?, val.clone()); + assert!(!lazy_set.contains(&storage, &0)?); + assert!(lazy_set.contains(&storage, &val)?); + + // Remove the last value and check that the set is empty again + let is_removed = lazy_set.remove(&mut storage, &val)?; + assert!(is_removed); + assert!(lazy_set.is_empty(&storage)?); + assert!(lazy_set.len(&storage)? == 0); + assert!(!lazy_set.contains(&storage, &0)?); + assert!(lazy_set.is_empty(&storage)?); + assert!(!lazy_set.remove(&mut storage, &0)?); + assert!(!lazy_set.remove(&mut storage, &1)?); + + Ok(()) + } +} From ca50fb950cefd968f8e7b77bd8dc617388b5def1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 16 Aug 2022 17:52:44 +0200 Subject: [PATCH 65/81] fix clippy --- shared/src/ledger/storage/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/ledger/storage/mod.rs b/shared/src/ledger/storage/mod.rs index 3a1bfa697e..bd969fa464 100644 --- a/shared/src/ledger/storage/mod.rs +++ b/shared/src/ledger/storage/mod.rs @@ -459,7 +459,7 @@ where // Note that this method is the same as `StorageWrite::delete`, // but with gas and storage bytes len diff accounting let mut deleted_bytes_len = 0; - if Self::has_key(&self, key)?.0 { + if self.has_key(key)?.0 { self.block.tree.delete(key)?; deleted_bytes_len = self.db.delete_subspace_val(self.last_height, key)?; From 5819b23ce380ebe1a4980c306d738b41d72c5576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 23 Aug 2022 17:09:18 +0200 Subject: [PATCH 66/81] add lazy_vec validation --- .../storage_api/collections/lazy_vec.rs | 308 +++++++++++++++++- shared/src/ledger/storage_api/mod.rs | 1 + .../src/ledger/storage_api/validation/mod.rs | 53 +++ shared/src/types/storage.rs | 28 ++ 4 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 shared/src/ledger/storage_api/validation/mod.rs diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index 73989bf942..c9b836f774 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -1,11 +1,17 @@ //! Lazy dynamically-sized vector. +use std::collections::BTreeSet; use std::marker::PhantomData; +use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; +use derivative::Derivative; +use thiserror::Error; use super::super::Result; +use crate::ledger::storage_api::validation::{self, Data}; use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; +use crate::ledger::vp_env::VpEnv; use crate::types::storage; /// Subkey pointing to the length of the LazyVec @@ -13,6 +19,9 @@ pub const LEN_SUBKEY: &str = "len"; /// Subkey corresponding to the data elements of the LazyVec pub const DATA_SUBKEY: &str = "data"; +/// Using `u64` for vector's indices +pub type Index = u64; + /// Lazy dynamically-sized vector. /// /// This can be used as an alternative to `std::collections::Vec`. In the lazy @@ -24,6 +33,69 @@ pub struct LazyVec { phantom: PhantomData, } +/// Possible sub-keys of a [`LazyVec`] +pub enum SubKey { + /// Length sub-key + Len, + /// Data sub-key, further sub-keyed by its index + Data(Index), +} + +/// Possible sub-keys of a [`LazyVec`], together with their [`validation::Data`] +/// that contains prior and posterior state. +#[derive(Debug)] +pub enum SubKeyWithData { + /// Length sub-key + Len(Data), + /// Data sub-key, further sub-keyed by its index + Data(Index, Data), +} + +/// Possible actions that can modify a [`LazyVec`]. This roughly corresponds to +/// the methods that have `StorageWrite` access. +pub enum Action { + /// Push a value `T` into a [`LazyVec`] + Push(T), + /// Pop a value `T` from a [`LazyVec`] + Pop(T), +} + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum ValidationError { + #[error("Incorrect difference in LazyVec's length")] + InvalidLenDiff, + #[error("An empty LazyVec must be deleted from storage")] + EmptyVecShouldBeDeleted, + #[error("Push at a wrong index. Got {got}, expected {expected}.")] + UnexpectedPushIndex { got: Index, expected: Index }, + #[error("Pop at a wrong index. Got {got}, expected {expected}.")] + UnexpectedPopIndex { got: Index, expected: Index }, + #[error( + "Update (combination of pop and push) at a wrong index. Got {got}, \ + expected {expected}." + )] + UnexpectedUpdateIndex { got: Index, expected: Index }, + #[error("An index has overflown its representation: {0}")] + IndexOverflow(>::Error), + #[error("Unexpected underflow in `{0} - {0}`")] + UnexpectedUnderflow(Index, Index), +} + +/// [`LazyVec`] validation result +pub type ValidationResult = std::result::Result; + +/// [`LazyVec`] validation builder from storage changes. The changes can be +/// accumulated with `LazyVec::validate()` and then turned into a list +/// of valid actions on the vector with `ValidationBuilder::build()`. +#[derive(Debug, Derivative)] +// https://mcarton.github.io/rust-derivative/latest/Default.html#custom-bound +#[derivative(Default(bound = ""))] +pub struct ValidationBuilder { + /// The accumulator of found changes under the vector + pub changes: Vec>, +} + impl LazyVec where T: BorshSerialize + BorshDeserialize + 'static, @@ -73,7 +145,7 @@ where } /// Read an element at the index or `Ok(None)` if out of bounds. - pub fn get(&self, storage: &S, index: u64) -> Result> + pub fn get(&self, storage: &S, index: Index) -> Result> where S: for<'iter> StorageRead<'iter>, { @@ -116,13 +188,64 @@ where })) } + /// Check if the given storage key is a LazyVec sub-key and if so return + /// which one + pub fn is_sub_key(&self, key: &storage::Key) -> Option { + if let Some((prefix, storage::DbKeySeg::StringSeg(last))) = + key.split_last() + { + if let Ok(index) = Index::from_str(last) { + if let Some((prefix, storage::DbKeySeg::StringSeg(snd_last))) = + prefix.split_last() + { + if snd_last == DATA_SUBKEY && prefix.eq_owned(&self.key) { + return Some(SubKey::Data(index)); + } + } + } else if last == LEN_SUBKEY && prefix.eq_owned(&self.key) { + return Some(SubKey::Len); + } + } + None + } + + /// Accumulate storage changes inside a [`ValidationBuilder`] + pub fn validate( + &self, + builder: &mut Option>, + env: &ENV, + key_changed: storage::Key, + ) -> std::result::Result<(), ENV::Error> + where + ENV: VpEnv, + { + if let Some(sub) = self.is_sub_key(&key_changed) { + let change = match sub { + SubKey::Len => { + let data = validation::read_data(env, &key_changed)?; + data.map(SubKeyWithData::Len) + } + SubKey::Data(index) => { + let data = validation::read_data(env, &key_changed)?; + data.map(|data| SubKeyWithData::Data(index, data)) + } + }; + if let Some(change) = change { + let builder = + builder.get_or_insert(ValidationBuilder::default()); + builder.changes.push(change) + } + } + Ok(()) + } + /// Get the prefix of set's elements storage fn get_data_prefix(&self) -> storage::Key { self.key.push(&DATA_SUBKEY.to_owned()).unwrap() } /// Get the sub-key of vector's elements storage - fn get_data_key(&self, index: u64) -> storage::Key { + fn get_data_key(&self, index: Index) -> storage::Key { self.get_data_prefix().push(&index.to_string()).unwrap() } @@ -132,6 +255,187 @@ where } } +impl ValidationBuilder { + /// Validate storage changes and if valid, build from them a list of + /// actions. + /// + /// The validation rules for a [`LazyVec`] are: + /// - A difference in the vector's length must correspond to the + /// difference in how many elements where pushed versus how many + /// elements were popped. + /// - An empty vector must be deleted from storage + /// - In addition, we check that indices of any changes are within an + /// expected range (i.e. the vectors indices should always be + /// monotonically increasing from zero) + pub fn build(self) -> ValidationResult>> { + let mut actions = vec![]; + + // We need to accumlate some values for what's changed + let mut post_gt_pre = false; + let mut len_diff: u64 = 0; + let mut len_pre: u64 = 0; + let mut added = BTreeSet::::default(); + let mut updated = BTreeSet::::default(); + let mut deleted = BTreeSet::::default(); + + for change in self.changes { + match change { + SubKeyWithData::Len(data) => match data { + Data::Add { post } => { + if post == 0 { + return Err( + ValidationError::EmptyVecShouldBeDeleted, + ); + } + post_gt_pre = true; + len_diff = post; + } + Data::Update { pre, post } => { + if post == 0 { + return Err( + ValidationError::EmptyVecShouldBeDeleted, + ); + } + if post > pre { + post_gt_pre = true; + len_diff = post - pre; + } else { + len_diff = pre - post; + } + len_pre = pre; + } + Data::Delete { pre } => { + len_diff = pre; + len_pre = pre; + } + }, + SubKeyWithData::Data(index, data) => match data { + Data::Add { post } => { + actions.push(Action::Push(post)); + added.insert(index); + } + Data::Update { pre, post } => { + actions.push(Action::Pop(pre)); + actions.push(Action::Push(post)); + updated.insert(index); + } + Data::Delete { pre } => { + actions.push(Action::Pop(pre)); + deleted.insert(index); + } + }, + } + } + let added_len: u64 = deleted + .len() + .try_into() + .map_err(ValidationError::IndexOverflow)?; + let deleted_len: u64 = deleted + .len() + .try_into() + .map_err(ValidationError::IndexOverflow)?; + + if len_diff != 0 + && !(if post_gt_pre { + deleted_len + len_diff == added_len + } else { + added_len + len_diff == deleted_len + }) + { + return Err(ValidationError::InvalidLenDiff); + } + + let mut last_added = Option::None; + // Iterate additions in increasing order of indices + for index in added { + if let Some(last_added) = last_added { + // Following additions should be at monotonically increasing + // indices + let expected = last_added + 1; + if expected != index { + return Err(ValidationError::UnexpectedPushIndex { + got: index, + expected, + }); + } + } else if index != len_pre { + // The first addition must be at the pre length value. + // If something is deleted and a new value is added + // in its place, it will go through `Data::Update` + // instead. + return Err(ValidationError::UnexpectedPushIndex { + got: index, + expected: len_pre, + }); + } + last_added = Some(index); + } + + let mut last_deleted = Option::None; + // Also iterate deletions in increasing order of indices + for index in deleted { + if let Some(last_added) = last_deleted { + // Following deletions should be at monotonically increasing + // indices + let expected = last_added + 1; + if expected != index { + return Err(ValidationError::UnexpectedPopIndex { + got: index, + expected, + }); + } + } + last_deleted = Some(index); + } + if let Some(index) = last_deleted { + if len_pre > 0 { + let expected = len_pre - 1; + if index != expected { + // The last deletion must be at the pre length value minus 1 + return Err(ValidationError::UnexpectedPopIndex { + got: index, + expected: len_pre, + }); + } + } + } + + // And finally iterate updates in increasing order of indices + let mut last_updated = Option::None; + for index in updated { + if let Some(last_updated) = last_updated { + // Following additions should be at monotonically increasing + // indices + let expected = last_updated + 1; + if expected != index { + return Err(ValidationError::UnexpectedUpdateIndex { + got: index, + expected, + }); + } + } + last_updated = Some(index); + } + if let Some(index) = last_updated { + let expected = len_pre.checked_sub(deleted_len).ok_or( + ValidationError::UnexpectedUnderflow(len_pre, deleted_len), + )?; + if index != expected { + // The last update must be at the pre length value minus + // deleted_len. + // If something is added and then deleted in a + // single tx, it will never be visible here. + return Err(ValidationError::UnexpectedUpdateIndex { + got: index, + expected: len_pre, + }); + } + } + + Ok(actions) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/shared/src/ledger/storage_api/mod.rs b/shared/src/ledger/storage_api/mod.rs index 8cb04434e2..b806f35801 100644 --- a/shared/src/ledger/storage_api/mod.rs +++ b/shared/src/ledger/storage_api/mod.rs @@ -3,6 +3,7 @@ pub mod collections; mod error; +pub mod validation; use borsh::{BorshDeserialize, BorshSerialize}; pub use error::{CustomError, Error, OptionExt, Result, ResultExt}; diff --git a/shared/src/ledger/storage_api/validation/mod.rs b/shared/src/ledger/storage_api/validation/mod.rs new file mode 100644 index 0000000000..30c1e2566b --- /dev/null +++ b/shared/src/ledger/storage_api/validation/mod.rs @@ -0,0 +1,53 @@ +//! Storage change validation helpers + +use std::fmt::Debug; + +use borsh::BorshDeserialize; + +use crate::ledger::vp_env::VpEnv; +use crate::types::storage; + +/// Data update with prior and posterior state. +#[derive(Clone, Debug)] +pub enum Data { + /// Newly added value + Add { + /// Posterior state + post: T, + }, + /// Updated value prior and posterior state + Update { + /// Prior state + pre: T, + /// Posterior state + post: T, + }, + /// Deleted value + Delete { + /// Prior state + pre: T, + }, +} + +/// Read the prior and posterior state for the given key. +pub fn read_data( + env: &ENV, + key: &storage::Key, +) -> Result>, ENV::Error> +where + T: BorshDeserialize, + ENV: VpEnv, +{ + let pre = env.read_pre(key)?; + let post = env.read_post(key)?; + Ok(match (pre, post) { + (None, None) => { + // If the key was inserted and then deleted in the same tx, we don't + // need to validate it as it's not visible to any VPs + None + } + (None, Some(post)) => Some(Data::Add { post }), + (Some(pre), None) => Some(Data::Delete { pre }), + (Some(pre), Some(post)) => Some(Data::Update { pre, post }), + }) +} diff --git a/shared/src/types/storage.rs b/shared/src/types/storage.rs index 289f115c8f..49abe7197d 100644 --- a/shared/src/types/storage.rs +++ b/shared/src/types/storage.rs @@ -210,6 +210,13 @@ pub struct Key { pub segments: Vec, } +/// A [`Key`] made of borrowed key segments [`DbKeySeg`]. +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct KeyRef<'a> { + /// Reference of key segments + pub segments: &'a [DbKeySeg], +} + impl From for Key { fn from(seg: DbKeySeg) -> Self { Self { @@ -283,6 +290,13 @@ impl Key { self.segments.last() } + /// Returns the prefix before the last segment and last segment of the key, + /// or `None` if it is empty. + pub fn split_last(&self) -> Option<(KeyRef<'_>, &DbKeySeg)> { + let (last, prefix) = self.segments.split_last()?; + Some((KeyRef { segments: prefix }, last)) + } + /// Returns a key of the validity predicate of the given address /// Only this function can push "?" segment for validity predicate pub fn validity_predicate(addr: &Address) -> Self { @@ -372,6 +386,20 @@ impl Display for Key { } } +impl KeyRef<'_> { + /// Check if [`KeyRef`] is equal to a [`Key`]. + pub fn eq_owned(&self, other: &Key) -> bool { + self.segments == other.segments + } + + /// Returns the prefix before the last segment and last segment of the key, + /// or `None` if it is empty. + pub fn split_last(&self) -> Option<(KeyRef<'_>, &DbKeySeg)> { + let (last, prefix) = self.segments.split_last()?; + Some((KeyRef { segments: prefix }, last)) + } +} + // TODO use std::convert::{TryFrom, Into}? /// Represents a segment in a path that may be used as a database key pub trait KeySeg { From a474c802b755ff7f133a2ca0829ba836ac70ef52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 29 Aug 2022 14:16:59 +0200 Subject: [PATCH 67/81] storage_api/collections/lazy: allow nested lazy collections in LazyMap --- .../storage_api/collections/lazy_hashmap.rs | 15 ++-- .../storage_api/collections/lazy_hashset.rs | 13 +-- .../storage_api/collections/lazy_map.rs | 72 +++++++++++----- .../storage_api/collections/lazy_set.rs | 14 ++-- .../storage_api/collections/lazy_vec.rs | 83 ++++++++++--------- .../src/ledger/storage_api/collections/mod.rs | 15 ++++ 6 files changed, 134 insertions(+), 78 deletions(-) diff --git a/shared/src/ledger/storage_api/collections/lazy_hashmap.rs b/shared/src/ledger/storage_api/collections/lazy_hashmap.rs index f3330df8e3..4098e6d8c9 100644 --- a/shared/src/ledger/storage_api/collections/lazy_hashmap.rs +++ b/shared/src/ledger/storage_api/collections/lazy_hashmap.rs @@ -6,6 +6,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use super::super::Result; use super::hasher::hash_for_storage_key; +use super::LazyCollection; use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; use crate::types::storage; @@ -41,20 +42,22 @@ struct KeyVal { val: V, } -impl LazyHashMap -where - K: BorshDeserialize + BorshSerialize + 'static, - V: BorshDeserialize + BorshSerialize + 'static, -{ +impl LazyCollection for LazyHashMap { /// Create or use an existing map with the given storage `key`. - pub fn new(key: storage::Key) -> Self { + fn new(key: storage::Key) -> Self { Self { key, phantom_k: PhantomData, phantom_v: PhantomData, } } +} +impl LazyHashMap +where + K: BorshDeserialize + BorshSerialize + 'static, + V: BorshDeserialize + BorshSerialize + 'static, +{ /// Inserts a key-value pair into the map. /// /// If the map did not have this key present, `None` is returned. diff --git a/shared/src/ledger/storage_api/collections/lazy_hashset.rs b/shared/src/ledger/storage_api/collections/lazy_hashset.rs index c9bd01a34c..a110072371 100644 --- a/shared/src/ledger/storage_api/collections/lazy_hashset.rs +++ b/shared/src/ledger/storage_api/collections/lazy_hashset.rs @@ -6,6 +6,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use super::super::Result; use super::hasher::hash_for_storage_key; +use super::LazyCollection; use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; use crate::types::storage; @@ -33,18 +34,20 @@ pub struct LazyHashSet { phantom: PhantomData, } -impl LazyHashSet -where - T: BorshSerialize + BorshDeserialize + 'static, -{ +impl LazyCollection for LazyHashSet { /// Create or use an existing set with the given storage `key`. - pub fn new(key: storage::Key) -> Self { + fn new(key: storage::Key) -> Self { Self { key, phantom: PhantomData, } } +} +impl LazyHashSet +where + T: BorshSerialize + BorshDeserialize + 'static, +{ /// Adds a value to the set. If the set did not have this value present, /// `Ok(true)` is returned, `Ok(false)` otherwise. pub fn insert(&self, storage: &mut S, val: T) -> Result diff --git a/shared/src/ledger/storage_api/collections/lazy_map.rs b/shared/src/ledger/storage_api/collections/lazy_map.rs index a89cb02469..ddec03363e 100644 --- a/shared/src/ledger/storage_api/collections/lazy_map.rs +++ b/shared/src/ledger/storage_api/collections/lazy_map.rs @@ -5,7 +5,7 @@ use std::marker::PhantomData; use borsh::{BorshDeserialize, BorshSerialize}; use super::super::Result; -use super::ReadError; +use super::{LazyCollection, ReadError}; use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; use crate::types::storage::{self, KeySeg}; @@ -31,20 +31,65 @@ pub struct LazyMap { phantom_v: PhantomData, } -impl LazyMap +impl LazyCollection for LazyMap where K: storage::KeySeg, - V: BorshDeserialize + BorshSerialize + 'static, { /// Create or use an existing map with the given storage `key`. - pub fn new(key: storage::Key) -> Self { + fn new(key: storage::Key) -> Self { Self { key, phantom_k: PhantomData, phantom_v: PhantomData, } } +} + +// Generic `LazyMap` methods that require no bounds on values `V` +impl LazyMap +where + K: storage::KeySeg, +{ + /// Returns whether the set contains a value. + pub fn contains(&self, storage: &S, key: &K) -> Result + where + S: for<'iter> StorageRead<'iter>, + { + storage.has_key(&self.get_data_key(key)) + } + + /// Get the prefix of set's elements storage + fn get_data_prefix(&self) -> storage::Key { + self.key.push(&DATA_SUBKEY.to_owned()).unwrap() + } + + /// Get the sub-key of a given element + fn get_data_key(&self, key: &K) -> storage::Key { + let key_str = key.to_db_key(); + self.get_data_prefix().push(&key_str).unwrap() + } +} + +// `LazyMap` methods with nested `LazyCollection`s `V` +impl LazyMap +where + K: storage::KeySeg, + V: LazyCollection, +{ + /// Get a nested collection at given key `key`. If there is no nested + /// collection at the given key, a new empty one will be provided. The + /// nested collection may be manipulated through its methods. + pub fn at(&self, key: &K) -> V { + V::new(self.get_data_key(key)) + } +} +// `LazyMap` methods with borsh encoded values `V` +impl LazyMap +where + K: storage::KeySeg, + V: BorshDeserialize + BorshSerialize + 'static, +{ /// Inserts a key-value pair into the map. /// /// The full storage key identifies the key in the pair, while the value is @@ -95,14 +140,6 @@ where Self::read_key_val(storage, &data_key) } - /// Returns whether the set contains a value. - pub fn contains(&self, storage: &S, key: &K) -> Result - where - S: for<'iter> StorageRead<'iter>, - { - storage.has_key(&self.get_data_key(key)) - } - /// Reads the number of elements in the map. /// /// Note that this function shouldn't be used in transactions and VPs code @@ -175,17 +212,6 @@ where ) -> Result<()> { storage.write(storage_key, val) } - - /// Get the prefix of set's elements storage - fn get_data_prefix(&self) -> storage::Key { - self.key.push(&DATA_SUBKEY.to_owned()).unwrap() - } - - /// Get the sub-key of a given element - fn get_data_key(&self, key: &K) -> storage::Key { - let key_str = key.to_db_key(); - self.get_data_prefix().push(&key_str).unwrap() - } } #[cfg(test)] diff --git a/shared/src/ledger/storage_api/collections/lazy_set.rs b/shared/src/ledger/storage_api/collections/lazy_set.rs index 0bf533dea9..8918b55efd 100644 --- a/shared/src/ledger/storage_api/collections/lazy_set.rs +++ b/shared/src/ledger/storage_api/collections/lazy_set.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use super::super::Result; -use super::ReadError; +use super::{LazyCollection, ReadError}; use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; use crate::types::storage::{self, KeySeg}; @@ -28,18 +28,20 @@ pub struct LazySet { phantom: PhantomData, } -impl LazySet -where - T: storage::KeySeg, -{ +impl LazyCollection for LazySet { /// Create or use an existing set with the given storage `key`. - pub fn new(key: storage::Key) -> Self { + fn new(key: storage::Key) -> Self { Self { key, phantom: PhantomData, } } +} +impl LazySet +where + T: storage::KeySeg, +{ /// Adds a value to the set. If the set did not have this value present, /// `Ok(true)` is returned, `Ok(false)` otherwise. pub fn insert(&self, storage: &mut S, val: T) -> Result diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index c9b836f774..f87d4c126f 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -9,6 +9,7 @@ use derivative::Derivative; use thiserror::Error; use super::super::Result; +use super::LazyCollection; use crate::ledger::storage_api::validation::{self, Data}; use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; use crate::ledger::vp_env::VpEnv; @@ -96,18 +97,57 @@ pub struct ValidationBuilder { pub changes: Vec>, } -impl LazyVec -where - T: BorshSerialize + BorshDeserialize + 'static, -{ +impl LazyCollection for LazyVec { /// Create or use an existing vector with the given storage `key`. - pub fn new(key: storage::Key) -> Self { + fn new(key: storage::Key) -> Self { Self { key, phantom: PhantomData, } } +} + +// Generic `LazyVec` methods that require no bounds on values `T` +impl LazyVec { + /// Reads the number of elements in the vector. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self, storage: &S) -> Result + where + S: for<'iter> StorageRead<'iter>, + { + let len = storage.read(&self.get_len_key())?; + Ok(len.unwrap_or_default()) + } + + /// Returns `true` if the vector contains no elements. + pub fn is_empty(&self, storage: &S) -> Result + where + S: for<'iter> StorageRead<'iter>, + { + Ok(self.len(storage)? == 0) + } + + /// Get the prefix of set's elements storage + fn get_data_prefix(&self) -> storage::Key { + self.key.push(&DATA_SUBKEY.to_owned()).unwrap() + } + + /// Get the sub-key of vector's elements storage + fn get_data_key(&self, index: Index) -> storage::Key { + self.get_data_prefix().push(&index.to_string()).unwrap() + } + + /// Get the sub-key of vector's length storage + fn get_len_key(&self) -> storage::Key { + self.key.push(&LEN_SUBKEY.to_owned()).unwrap() + } +} +// `LazyVec` methods with borsh encoded values `T` +impl LazyVec +where + T: BorshSerialize + BorshDeserialize + 'static, +{ /// Appends an element to the back of a collection. pub fn push(&self, storage: &mut S, val: T) -> Result<()> where @@ -152,24 +192,6 @@ where storage.read(&self.get_data_key(index)) } - /// Reads the number of elements in the vector. - #[allow(clippy::len_without_is_empty)] - pub fn len(&self, storage: &S) -> Result - where - S: for<'iter> StorageRead<'iter>, - { - let len = storage.read(&self.get_len_key())?; - Ok(len.unwrap_or_default()) - } - - /// Returns `true` if the vector contains no elements. - pub fn is_empty(&self, storage: &S) -> Result - where - S: for<'iter> StorageRead<'iter>, - { - Ok(self.len(storage)? == 0) - } - /// An iterator visiting all elements. The iterator element type is /// `Result`, because iterator's call to `next` may fail with e.g. out of /// gas or data decoding error. @@ -238,21 +260,6 @@ where } Ok(()) } - - /// Get the prefix of set's elements storage - fn get_data_prefix(&self) -> storage::Key { - self.key.push(&DATA_SUBKEY.to_owned()).unwrap() - } - - /// Get the sub-key of vector's elements storage - fn get_data_key(&self, index: Index) -> storage::Key { - self.get_data_prefix().push(&index.to_string()).unwrap() - } - - /// Get the sub-key of vector's length storage - fn get_len_key(&self) -> storage::Key { - self.key.push(&LEN_SUBKEY.to_owned()).unwrap() - } } impl ValidationBuilder { diff --git a/shared/src/ledger/storage_api/collections/mod.rs b/shared/src/ledger/storage_api/collections/mod.rs index 156615b9de..01bcae439e 100644 --- a/shared/src/ledger/storage_api/collections/mod.rs +++ b/shared/src/ledger/storage_api/collections/mod.rs @@ -22,9 +22,24 @@ pub use lazy_map::LazyMap; pub use lazy_set::LazySet; pub use lazy_vec::LazyVec; +use crate::types::storage; + #[allow(missing_docs)] #[derive(Error, Debug)] pub enum ReadError { #[error("A storage key was unexpectedly empty")] UnexpectedlyEmptyStorageKey, } + +/// A lazy collection of storage values is a handler with some storage prefix +/// that is given to its `fn new()`. The values are typically nested under this +/// prefix and they can be changed individually (e.g. without reading in the +/// whole collection) and their changes directly indicated to the validity +/// predicates, which do not need to iterate the whole collection pre/post to +/// find diffs. +/// +/// An empty collection must be deleted from storage. +pub trait LazyCollection { + /// Create or use an existing vector with the given storage `key`. + fn new(key: storage::Key) -> Self; +} From 51036681d046e5b8100605d68cd8376b048dbbb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 1 Sep 2022 10:43:32 +0200 Subject: [PATCH 68/81] impl LazyCollection trait for all the collections + refactor --- .../storage_api/collections/lazy_hashmap.rs | 23 +++---- .../storage_api/collections/lazy_hashset.rs | 1 + .../storage_api/collections/lazy_map.rs | 61 ++++++++++++++---- .../storage_api/collections/lazy_set.rs | 23 +++---- .../storage_api/collections/lazy_vec.rs | 62 +++++++++++++++---- 5 files changed, 120 insertions(+), 50 deletions(-) diff --git a/shared/src/ledger/storage_api/collections/lazy_hashmap.rs b/shared/src/ledger/storage_api/collections/lazy_hashmap.rs index 4098e6d8c9..46bc55fa6e 100644 --- a/shared/src/ledger/storage_api/collections/lazy_hashmap.rs +++ b/shared/src/ledger/storage_api/collections/lazy_hashmap.rs @@ -29,6 +29,7 @@ pub const DATA_SUBKEY: &str = "data"; /// /// Additionally, [`LazyHashMap`] also writes the unhashed values into the /// storage together with the values (using an internal `KeyVal` type). +#[derive(Debug)] pub struct LazyHashMap { key: storage::Key, phantom_k: PhantomData, @@ -136,33 +137,29 @@ where storage.has_key(&self.get_data_key(key)) } - /// Reads the number of elements in the map. - /// - /// Note that this function shouldn't be used in transactions and VPs code - /// on unbounded maps to avoid gas usage increasing with the length of the - /// set. - #[allow(clippy::len_without_is_empty)] - pub fn len(&self, storage: &S) -> Result + /// Returns whether the map contains no elements. + pub fn is_empty(&self, storage: &S) -> Result where S: for<'iter> StorageRead<'iter>, { - let iter = + let mut iter = storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; - iter.count().try_into().into_storage_result() + Ok(iter.next().is_none()) } - /// Returns whether the map contains no elements. + /// Reads the number of elements in the map. /// /// Note that this function shouldn't be used in transactions and VPs code /// on unbounded maps to avoid gas usage increasing with the length of the /// set. - pub fn is_empty(&self, storage: &S) -> Result + #[allow(clippy::len_without_is_empty)] + pub fn len(&self, storage: &S) -> Result where S: for<'iter> StorageRead<'iter>, { - let mut iter = + let iter = storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; - Ok(iter.next().is_none()) + iter.count().try_into().into_storage_result() } /// An iterator visiting all key-value elements. The iterator element type diff --git a/shared/src/ledger/storage_api/collections/lazy_hashset.rs b/shared/src/ledger/storage_api/collections/lazy_hashset.rs index a110072371..06a6ef2295 100644 --- a/shared/src/ledger/storage_api/collections/lazy_hashset.rs +++ b/shared/src/ledger/storage_api/collections/lazy_hashset.rs @@ -29,6 +29,7 @@ pub const DATA_SUBKEY: &str = "data"; /// /// Additionally, [`LazyHashSet`] also writes the unhashed values into the /// storage. +#[derive(Debug)] pub struct LazyHashSet { key: storage::Key, phantom: PhantomData, diff --git a/shared/src/ledger/storage_api/collections/lazy_map.rs b/shared/src/ledger/storage_api/collections/lazy_map.rs index ddec03363e..103508404e 100644 --- a/shared/src/ledger/storage_api/collections/lazy_map.rs +++ b/shared/src/ledger/storage_api/collections/lazy_map.rs @@ -6,6 +6,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use super::super::Result; use super::{LazyCollection, ReadError}; +use crate::ledger::storage_api::validation::Data; use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; use crate::types::storage::{self, KeySeg}; @@ -25,12 +26,50 @@ pub const DATA_SUBKEY: &str = "data"; /// /// This is different from [`super::LazyHashMap`], which hashes borsh encoded /// key. +#[derive(Debug)] pub struct LazyMap { key: storage::Key, phantom_k: PhantomData, phantom_v: PhantomData, } +/// Possible sub-keys of a [`LazyMap`] +#[derive(Debug)] +pub enum SubKey { + /// Data sub-key, further sub-keyed by its literal map key + Data(K), +} + +/// Possible sub-keys of a [`LazyMap`], together with their [`validation::Data`] +/// that contains prior and posterior state. +#[derive(Debug)] +pub enum SubKeyWithData { + /// Data sub-key, further sub-keyed by its literal map key + Data(K, Data), +} + +/// Possible actions that can modify a [`LazyMap`]. This roughly corresponds to +/// the methods that have `StorageWrite` access. +/// TODO: In a nested collection, `V` may be an action inside the nested +/// collection. +#[derive(Debug)] +pub enum Action { + /// Insert or update a value `V` at key `K` in a [`LazyMap`]. + Insert(K, V), + /// Remove a value `V` at key `K` from a [`LazyMap`]. + Remove(K, V), +} + +/// TODO: In a nested collection, `V` may be an action inside the nested +/// collection. +#[derive(Debug)] +pub enum Nested { + /// Insert or update a value `V` at key `K` in a [`LazyMap`]. + Insert(K, V), + /// Remove a value `V` at key `K` from a [`LazyMap`]. + Remove(K, V), +} + impl LazyCollection for LazyMap where K: storage::KeySeg, @@ -140,33 +179,29 @@ where Self::read_key_val(storage, &data_key) } - /// Reads the number of elements in the map. - /// - /// Note that this function shouldn't be used in transactions and VPs code - /// on unbounded maps to avoid gas usage increasing with the length of the - /// set. - #[allow(clippy::len_without_is_empty)] - pub fn len(&self, storage: &S) -> Result + /// Returns whether the map contains no elements. + pub fn is_empty(&self, storage: &S) -> Result where S: for<'iter> StorageRead<'iter>, { - let iter = + let mut iter = storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; - iter.count().try_into().into_storage_result() + Ok(iter.next().is_none()) } - /// Returns whether the map contains no elements. + /// Reads the number of elements in the map. /// /// Note that this function shouldn't be used in transactions and VPs code /// on unbounded maps to avoid gas usage increasing with the length of the /// set. - pub fn is_empty(&self, storage: &S) -> Result + #[allow(clippy::len_without_is_empty)] + pub fn len(&self, storage: &S) -> Result where S: for<'iter> StorageRead<'iter>, { - let mut iter = + let iter = storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; - Ok(iter.next().is_none()) + iter.count().try_into().into_storage_result() } /// An iterator visiting all key-value elements. The iterator element type diff --git a/shared/src/ledger/storage_api/collections/lazy_set.rs b/shared/src/ledger/storage_api/collections/lazy_set.rs index 8918b55efd..fc301b09f1 100644 --- a/shared/src/ledger/storage_api/collections/lazy_set.rs +++ b/shared/src/ledger/storage_api/collections/lazy_set.rs @@ -23,6 +23,7 @@ pub const DATA_SUBKEY: &str = "data"; /// /// This is different from [`super::LazyHashSet`], which hashes borsh encoded /// values. +#[derive(Debug)] pub struct LazySet { key: storage::Key, phantom: PhantomData, @@ -79,33 +80,29 @@ where storage.has_key(&self.get_data_key(val)) } - /// Reads the number of elements in the set. - /// - /// Note that this function shouldn't be used in transactions and VPs code - /// on unbounded sets to avoid gas usage increasing with the length of the - /// set. - #[allow(clippy::len_without_is_empty)] - pub fn len(&self, storage: &S) -> Result + /// Returns whether the set contains no elements. + pub fn is_empty(&self, storage: &S) -> Result where S: for<'iter> StorageRead<'iter>, { - let iter = + let mut iter = storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; - iter.count().try_into().into_storage_result() + Ok(iter.next().is_none()) } - /// Returns whether the set contains no elements. + /// Reads the number of elements in the set. /// /// Note that this function shouldn't be used in transactions and VPs code /// on unbounded sets to avoid gas usage increasing with the length of the /// set. - pub fn is_empty(&self, storage: &S) -> Result + #[allow(clippy::len_without_is_empty)] + pub fn len(&self, storage: &S) -> Result where S: for<'iter> StorageRead<'iter>, { - let mut iter = + let iter = storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; - Ok(iter.next().is_none()) + iter.count().try_into().into_storage_result() } /// An iterator visiting all elements. The iterator element type is diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index f87d4c126f..6dd57ca556 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -11,7 +11,7 @@ use thiserror::Error; use super::super::Result; use super::LazyCollection; use crate::ledger::storage_api::validation::{self, Data}; -use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; +use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; use crate::ledger::vp_env::VpEnv; use crate::types::storage; @@ -29,12 +29,14 @@ pub type Index = u64; /// vector, the elements do not reside in memory but are instead read and /// written to storage sub-keys of the storage `key` used to construct the /// vector. +#[derive(Debug)] pub struct LazyVec { key: storage::Key, phantom: PhantomData, } /// Possible sub-keys of a [`LazyVec`] +#[derive(Debug)] pub enum SubKey { /// Length sub-key Len, @@ -54,11 +56,21 @@ pub enum SubKeyWithData { /// Possible actions that can modify a [`LazyVec`]. This roughly corresponds to /// the methods that have `StorageWrite` access. +#[derive(Debug)] pub enum Action { /// Push a value `T` into a [`LazyVec`] Push(T), /// Pop a value `T` from a [`LazyVec`] Pop(T), + /// Update a value `T` at index from pre to post state in a [`LazyVec`] + Update { + /// index at which the value is updated + index: Index, + /// value before the update + pre: T, + /// value after the update + post: T, + }, } #[allow(missing_docs)] @@ -83,6 +95,15 @@ pub enum ValidationError { UnexpectedUnderflow(Index, Index), } +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum UpdateError { + #[error( + "Invalid index into a LazyVec. Got {index}, but the length is {len}" + )] + InvalidIndex { index: Index, len: u64 }, +} + /// [`LazyVec`] validation result pub type ValidationResult = std::result::Result; @@ -184,6 +205,23 @@ where } } + /// Update an element at the given index. + /// + /// The index must be smaller than the length of the vector, otherwise this + /// will fail with `UpdateError::InvalidIndex`. + pub fn update(&self, storage: &mut S, index: Index, val: T) -> Result<()> + where + S: StorageWrite + for<'iter> StorageRead<'iter>, + { + let len = self.len(storage)?; + if index >= len { + return Err(UpdateError::InvalidIndex { index, len }) + .into_storage_result(); + } + let data_key = self.get_data_key(index); + storage.write(&data_key, val) + } + /// Read an element at the index or `Ok(None)` if out of bounds. pub fn get(&self, storage: &S, index: Index) -> Result> where @@ -231,24 +269,27 @@ where None } - /// Accumulate storage changes inside a [`ValidationBuilder`] - pub fn validate( + /// Accumulate storage changes inside a [`ValidationBuilder`]. This is + /// typically done by the validity predicate while looping through the + /// changed keys. If the resulting `builder` is not `None`, one must + /// call `fn build()` on it to get the validation result. + pub fn accumulate( &self, - builder: &mut Option>, env: &ENV, - key_changed: storage::Key, + builder: &mut Option>, + key_changed: &storage::Key, ) -> std::result::Result<(), ENV::Error> where ENV: VpEnv, { - if let Some(sub) = self.is_sub_key(&key_changed) { + if let Some(sub) = self.is_sub_key(key_changed) { let change = match sub { SubKey::Len => { - let data = validation::read_data(env, &key_changed)?; + let data = validation::read_data(env, key_changed)?; data.map(SubKeyWithData::Len) } SubKey::Data(index) => { - let data = validation::read_data(env, &key_changed)?; + let data = validation::read_data(env, key_changed)?; data.map(|data| SubKeyWithData::Data(index, data)) } }; @@ -277,7 +318,7 @@ impl ValidationBuilder { pub fn build(self) -> ValidationResult>> { let mut actions = vec![]; - // We need to accumlate some values for what's changed + // We need to accumulate some values for what's changed let mut post_gt_pre = false; let mut len_diff: u64 = 0; let mut len_pre: u64 = 0; @@ -322,8 +363,7 @@ impl ValidationBuilder { added.insert(index); } Data::Update { pre, post } => { - actions.push(Action::Pop(pre)); - actions.push(Action::Push(post)); + actions.push(Action::Update { index, pre, post }); updated.insert(index); } Data::Delete { pre } => { From 255b43be1e337e6838855187864bce59a611c313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 12 Sep 2022 18:18:26 +0200 Subject: [PATCH 69/81] storage/lazy_vec/validation: disallow unrecognized keys matching prefix --- .../storage_api/collections/lazy_vec.rs | 84 ++++++++++++++----- shared/src/types/storage.rs | 19 +++++ 2 files changed, 80 insertions(+), 23 deletions(-) diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index 6dd57ca556..c13d40855d 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -13,7 +13,7 @@ use super::LazyCollection; use crate::ledger::storage_api::validation::{self, Data}; use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; use crate::ledger::vp_env::VpEnv; -use crate::types::storage; +use crate::types::storage::{self, DbKeySeg}; /// Subkey pointing to the length of the LazyVec pub const LEN_SUBKEY: &str = "len"; @@ -76,6 +76,8 @@ pub enum Action { #[allow(missing_docs)] #[derive(Error, Debug)] pub enum ValidationError { + #[error("Storage error in reading key {0}")] + StorageError(storage::Key), #[error("Incorrect difference in LazyVec's length")] InvalidLenDiff, #[error("An empty LazyVec must be deleted from storage")] @@ -93,6 +95,8 @@ pub enum ValidationError { IndexOverflow(>::Error), #[error("Unexpected underflow in `{0} - {0}`")] UnexpectedUnderflow(Index, Index), + #[error("Invalid storage key {0}")] + InvalidSubKey(storage::Key), } #[allow(missing_docs)] @@ -155,6 +159,7 @@ impl LazyVec { /// Get the sub-key of vector's elements storage fn get_data_key(&self, index: Index) -> storage::Key { + // TODO rebase on ordered prefix iter (https://github.com/anoma/namada/pull/458) and remove `.to_string()` self.get_data_prefix().push(&index.to_string()).unwrap() } @@ -248,58 +253,91 @@ where })) } - /// Check if the given storage key is a LazyVec sub-key and if so return - /// which one - pub fn is_sub_key(&self, key: &storage::Key) -> Option { - if let Some((prefix, storage::DbKeySeg::StringSeg(last))) = - key.split_last() - { - if let Ok(index) = Index::from_str(last) { - if let Some((prefix, storage::DbKeySeg::StringSeg(snd_last))) = - prefix.split_last() - { - if snd_last == DATA_SUBKEY && prefix.eq_owned(&self.key) { - return Some(SubKey::Data(index)); - } + /// Check if the given storage key is a valid LazyVec sub-key and if so + /// return which one + pub fn is_valid_sub_key( + &self, + key: &storage::Key, + ) -> ValidationResult> { + let suffix = match key.split_prefix(&self.key) { + None => { + // not matching prefix, irrelevant + return Ok(None); + } + Some(None) => { + // no suffix, invalid + return Err(ValidationError::InvalidSubKey(key.clone())); + } + Some(Some(suffix)) => suffix, + }; + + // Match the suffix against expected sub-keys + match &suffix.segments[..] { + [DbKeySeg::StringSeg(sub)] if sub == LEN_SUBKEY => { + Ok(Some(SubKey::Len)) + } + [DbKeySeg::StringSeg(sub_a), DbKeySeg::StringSeg(sub_b)] + if sub_a == DATA_SUBKEY => + { + // TODO rebase on ordered prefix iter (https://github.com/anoma/namada/pull/458) and parse with `KeySeg::parse` + if let Ok(index) = Index::from_str(sub_b) { + Ok(Some(SubKey::Data(index))) + } else { + Err(ValidationError::InvalidSubKey(key.clone())) } - } else if last == LEN_SUBKEY && prefix.eq_owned(&self.key) { - return Some(SubKey::Len); } + _ => Err(ValidationError::InvalidSubKey(key.clone())), } - None } /// Accumulate storage changes inside a [`ValidationBuilder`]. This is /// typically done by the validity predicate while looping through the /// changed keys. If the resulting `builder` is not `None`, one must /// call `fn build()` on it to get the validation result. + /// This function will return `Ok(true)` if the storage key is a valid + /// sub-key of this collection, `Ok(false)` if the storage key doesn't match + /// the prefix of this collection, or fail with + /// [`ValidationError::InvalidSubKey`] if the prefix matches this + /// collection, but the key itself is not recognized. pub fn accumulate( &self, env: &ENV, builder: &mut Option>, key_changed: &storage::Key, - ) -> std::result::Result<(), ENV::Error> + ) -> ValidationResult where ENV: VpEnv, { - if let Some(sub) = self.is_sub_key(key_changed) { + if let Some(sub) = self.is_valid_sub_key(key_changed)? { let change = match sub { SubKey::Len => { - let data = validation::read_data(env, key_changed)?; + let data = validation::read_data(env, key_changed) + // TODO this has to propagate errors from VpEnv rather + // then replace them (e.g. it could be out-of-gas), but + // VpEnv::Error is generic, maybe we should make it + // concrete (only the VpEnv impls have extensible error + // types) + .map_err(|_| { + ValidationError::StorageError(key_changed.clone()) + })?; data.map(SubKeyWithData::Len) } SubKey::Data(index) => { - let data = validation::read_data(env, key_changed)?; + let data = validation::read_data(env, key_changed) + .map_err(|_| { + ValidationError::StorageError(key_changed.clone()) + })?; data.map(|data| SubKeyWithData::Data(index, data)) } }; if let Some(change) = change { let builder = builder.get_or_insert(ValidationBuilder::default()); - builder.changes.push(change) + builder.changes.push(change); + return Ok(true); } } - Ok(()) + Ok(false) } } diff --git a/shared/src/types/storage.rs b/shared/src/types/storage.rs index 49abe7197d..b5d72b7b02 100644 --- a/shared/src/types/storage.rs +++ b/shared/src/types/storage.rs @@ -372,6 +372,25 @@ impl Key { }), } } + + /// Check if the key begins with the given prefix and returns: + /// - `Some(Some(suffix))` the suffix after the match with, if any, or + /// - `Some(None)` if the prefix is matched, but it has no suffix, or + /// - `None` if it doesn't match + pub fn split_prefix(&self, prefix: &Self) -> Option> { + if self.segments.len() < prefix.len() { + return None; + } else if self == prefix { + return Some(None); + } + let mut self_prefix = self.segments.clone(); + let rest = self_prefix.split_off(prefix.len()); + if self_prefix == prefix.segments { + Some(Some(Key { segments: rest })) + } else { + None + } + } } impl Display for Key { From f854d8285c7b5e80aa8131d9a14cbbb99b5fa0d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 13 Sep 2022 18:56:32 +0200 Subject: [PATCH 70/81] storage/lazy_vec: add state machine test for lazy vec API --- .../storage_api/collections/lazy_hashmap.rs | 4 +- .../storage_api/collections/lazy_hashset.rs | 4 +- .../storage_api/collections/lazy_map.rs | 6 +- .../storage_api/collections/lazy_set.rs | 4 +- .../storage_api/collections/lazy_vec.rs | 257 +++++++++++++++++- .../src/ledger/storage_api/collections/mod.rs | 2 +- 6 files changed, 264 insertions(+), 13 deletions(-) diff --git a/shared/src/ledger/storage_api/collections/lazy_hashmap.rs b/shared/src/ledger/storage_api/collections/lazy_hashmap.rs index 46bc55fa6e..9a60fd18f0 100644 --- a/shared/src/ledger/storage_api/collections/lazy_hashmap.rs +++ b/shared/src/ledger/storage_api/collections/lazy_hashmap.rs @@ -45,7 +45,7 @@ struct KeyVal { impl LazyCollection for LazyHashMap { /// Create or use an existing map with the given storage `key`. - fn new(key: storage::Key) -> Self { + fn open(key: storage::Key) -> Self { Self { key, phantom_k: PhantomData, @@ -225,7 +225,7 @@ mod test { let mut storage = TestStorage::default(); let key = storage::Key::parse("test").unwrap(); - let lazy_map = LazyHashMap::::new(key); + let lazy_map = LazyHashMap::::open(key); // The map should be empty at first assert!(lazy_map.is_empty(&storage)?); diff --git a/shared/src/ledger/storage_api/collections/lazy_hashset.rs b/shared/src/ledger/storage_api/collections/lazy_hashset.rs index 06a6ef2295..63ac5c845c 100644 --- a/shared/src/ledger/storage_api/collections/lazy_hashset.rs +++ b/shared/src/ledger/storage_api/collections/lazy_hashset.rs @@ -37,7 +37,7 @@ pub struct LazyHashSet { impl LazyCollection for LazyHashSet { /// Create or use an existing set with the given storage `key`. - fn new(key: storage::Key) -> Self { + fn open(key: storage::Key) -> Self { Self { key, phantom: PhantomData, @@ -149,7 +149,7 @@ mod test { let mut storage = TestStorage::default(); let key = storage::Key::parse("test").unwrap(); - let lazy_set = LazyHashSet::::new(key); + let lazy_set = LazyHashSet::::open(key); // The set should be empty at first assert!(lazy_set.is_empty(&storage)?); diff --git a/shared/src/ledger/storage_api/collections/lazy_map.rs b/shared/src/ledger/storage_api/collections/lazy_map.rs index 103508404e..a47ff0734a 100644 --- a/shared/src/ledger/storage_api/collections/lazy_map.rs +++ b/shared/src/ledger/storage_api/collections/lazy_map.rs @@ -75,7 +75,7 @@ where K: storage::KeySeg, { /// Create or use an existing map with the given storage `key`. - fn new(key: storage::Key) -> Self { + fn open(key: storage::Key) -> Self { Self { key, phantom_k: PhantomData, @@ -119,7 +119,7 @@ where /// collection at the given key, a new empty one will be provided. The /// nested collection may be manipulated through its methods. pub fn at(&self, key: &K) -> V { - V::new(self.get_data_key(key)) + V::open(self.get_data_key(key)) } } @@ -259,7 +259,7 @@ mod test { let mut storage = TestStorage::default(); let key = storage::Key::parse("test").unwrap(); - let lazy_map = LazyMap::::new(key); + let lazy_map = LazyMap::::open(key); // The map should be empty at first assert!(lazy_map.is_empty(&storage)?); diff --git a/shared/src/ledger/storage_api/collections/lazy_set.rs b/shared/src/ledger/storage_api/collections/lazy_set.rs index fc301b09f1..9635724543 100644 --- a/shared/src/ledger/storage_api/collections/lazy_set.rs +++ b/shared/src/ledger/storage_api/collections/lazy_set.rs @@ -31,7 +31,7 @@ pub struct LazySet { impl LazyCollection for LazySet { /// Create or use an existing set with the given storage `key`. - fn new(key: storage::Key) -> Self { + fn open(key: storage::Key) -> Self { Self { key, phantom: PhantomData, @@ -150,7 +150,7 @@ mod test { let mut storage = TestStorage::default(); let key = storage::Key::parse("test").unwrap(); - let lazy_set = LazySet::::new(key); + let lazy_set = LazySet::::open(key); // The set should be empty at first assert!(lazy_set.is_empty(&storage)?); diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index c13d40855d..f3a8c2bac8 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -29,7 +29,7 @@ pub type Index = u64; /// vector, the elements do not reside in memory but are instead read and /// written to storage sub-keys of the storage `key` used to construct the /// vector. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct LazyVec { key: storage::Key, phantom: PhantomData, @@ -124,7 +124,7 @@ pub struct ValidationBuilder { impl LazyCollection for LazyVec { /// Create or use an existing vector with the given storage `key`. - fn new(key: storage::Key) -> Self { + fn open(key: storage::Key) -> Self { Self { key, phantom: PhantomData, @@ -523,6 +523,12 @@ impl ValidationBuilder { #[cfg(test)] mod test { + use proptest::prelude::*; + use proptest::prop_state_machine; + use proptest::state_machine::{AbstractStateMachine, StateMachineTest}; + use proptest::test_runner::Config; + use test_log::test; + use super::*; use crate::ledger::storage::testing::TestStorage; @@ -531,7 +537,7 @@ mod test { let mut storage = TestStorage::default(); let key = storage::Key::parse("test").unwrap(); - let lazy_vec = LazyVec::::new(key); + let lazy_vec = LazyVec::::open(key); // The vec should be empty at first assert!(lazy_vec.is_empty(&storage)?); @@ -561,4 +567,249 @@ mod test { Ok(()) } + + prop_state_machine! { + #![proptest_config(Config { + // Instead of the default 256, we only run 5 because otherwise it + // takes too long and it's preferable to crank up the number of + // transitions instead, to allow each case to run for more epochs as + // some issues only manifest once the model progresses further. + // Additionally, more cases will be explored every time this test is + // executed in the CI. + cases: 5, + .. Config::default() + })] + #[test] + /// A `StateMachineTest` implemented on `LazyVec` that manipulates + /// it with `Transition`s and checks its state against an in-memory + /// `std::collections::Vec`. + fn lazy_vec_api_state_machine_test(sequential 1..100 => ConcreteLazyVecState); + + } + + /// Some borsh-serializable type with arbitrary fields to be used inside + /// LazyVec state machine test + #[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, + PartialOrd, + Ord, + )] + struct TestVecItem { + x: u64, + y: bool, + } + + #[derive(Debug)] + struct ConcreteLazyVecState { + // The eager vec in `AbstractLazyVecState` is not visible in `impl + // StateMachineTest for ConcreteLazyVecState`, it's only used to drive + // transition generation, so we duplicate it here and apply the + // transitions on it the same way (with + // `fn apply_transition_on_eager_vec`) + eager_vec: Vec, + lazy_vec: LazyVec, + storage: TestStorage, + } + + #[derive(Clone, Debug)] + struct AbstractLazyVecState(Vec); + + /// Possible transitions that can modify a [`LazyVec`]. This roughly + /// corresponds to the methods that have `StorageWrite` access and is very + /// similar to [`Action`] + #[derive(Clone, Debug)] + pub enum Transition { + /// Push a value `T` into a [`LazyVec`] + Push(T), + /// Pop a value from a [`LazyVec`] + Pop, + /// Update a value `T` at index from pre to post state in a + /// [`LazyVec`] + Update { + /// index at which the value is updated + index: Index, + /// value to update the element to + value: T, + }, + } + + impl AbstractStateMachine for AbstractLazyVecState { + type State = Self; + type Transition = Transition; + + fn init_state() -> BoxedStrategy { + Just(Self(vec![])).boxed() + } + + fn transitions(state: &Self::State) -> BoxedStrategy { + if state.0.is_empty() { + prop_oneof![arb_test_vec_item().prop_map(Transition::Push)] + .boxed() + } else { + let indices: Vec = + (0_usize..state.0.len()).map(|ix| ix as Index).collect(); + let arb_index = proptest::sample::select(indices); + prop_oneof![ + Just(Transition::Pop), + arb_test_vec_item().prop_map(Transition::Push), + (arb_index, arb_test_vec_item()).prop_map( + |(index, value)| Transition::Update { index, value } + ) + ] + .boxed() + } + } + + fn apply_abstract( + mut state: Self::State, + transition: &Self::Transition, + ) -> Self::State { + apply_transition_on_eager_vec(&mut state.0, transition); + state + } + + fn preconditions( + state: &Self::State, + transition: &Self::Transition, + ) -> bool { + if state.0.is_empty() { + !matches!( + transition, + Transition::Pop | Transition::Update { .. } + ) + } else if let Transition::Update { index, .. } = transition { + *index < (state.0.len() - 1) as Index + } else { + true + } + } + } + + impl StateMachineTest for ConcreteLazyVecState { + type Abstract = AbstractLazyVecState; + type ConcreteState = Self; + + fn init_test( + _initial_state: ::State, + ) -> Self::ConcreteState { + Self { + eager_vec: vec![], + lazy_vec: LazyVec::open( + storage::Key::parse("key_path/arbitrary").unwrap(), + ), + storage: TestStorage::default(), + } + } + + fn apply_concrete( + mut state: Self::ConcreteState, + transition: ::Transition, + ) -> Self::ConcreteState { + // Transition application on lazy vec and post-conditions: + match dbg!(&transition) { + Transition::Push(value) => { + let old_len = state.lazy_vec.len(&state.storage).unwrap(); + + state + .lazy_vec + .push(&mut state.storage, value.clone()) + .unwrap(); + + // Post-conditions: + let new_len = state.lazy_vec.len(&state.storage).unwrap(); + let stored_value = state + .lazy_vec + .get(&state.storage, new_len - 1) + .unwrap() + .unwrap(); + assert_eq!( + &stored_value, value, + "the new item must be added to the back" + ); + assert_eq!(old_len + 1, new_len, "length must increment"); + } + Transition::Pop => { + let old_len = state.lazy_vec.len(&state.storage).unwrap(); + + let popped = state + .lazy_vec + .pop(&mut state.storage) + .unwrap() + .unwrap(); + + // Post-conditions: + let new_len = state.lazy_vec.len(&state.storage).unwrap(); + assert_eq!(old_len, new_len + 1, "length must decrement"); + assert_eq!( + &popped, + state.eager_vec.last().unwrap(), + "popped element matches the last element in eager vec \ + before it's updated" + ); + } + Transition::Update { index, value } => { + state + .lazy_vec + .update(&mut state.storage, *index, value.clone()) + .unwrap(); + } + } + + // Apply transition in the eager vec for comparison + apply_transition_on_eager_vec(&mut state.eager_vec, &transition); + + // Global post-conditions: + + // All items in eager vec must be present in lazy vec + for (ix, expected_item) in state.eager_vec.iter().enumerate() { + let got = state + .lazy_vec + .get(&state.storage, ix as Index) + .unwrap() + .expect("The expected item must be present in lazy vec"); + assert_eq!(expected_item, &got, "at index {ix}"); + } + + // All items in lazy vec must be present in eager vec + for (ix, expected_item) in + state.lazy_vec.iter(&state.storage).unwrap().enumerate() + { + let expected_item = expected_item.unwrap(); + let got = state + .eager_vec + .get(ix) + .expect("The expected item must be present in eager vec"); + assert_eq!(&expected_item, got, "at index {ix}"); + } + + state + } + } + + /// Generate an arbitrary `TestVecItem` + fn arb_test_vec_item() -> impl Strategy { + (any::(), any::()).prop_map(|(x, y)| TestVecItem { x, y }) + } + + /// Apply `Transition` on an eager `Vec`. + fn apply_transition_on_eager_vec( + vec: &mut Vec, + transition: &Transition, + ) { + match transition { + Transition::Push(value) => vec.push(value.clone()), + Transition::Pop => { + let _popped = vec.pop(); + } + Transition::Update { index, value } => { + let entry = vec.get_mut(*index as usize).unwrap(); + *entry = value.clone(); + } + } + } } diff --git a/shared/src/ledger/storage_api/collections/mod.rs b/shared/src/ledger/storage_api/collections/mod.rs index 01bcae439e..b3e1b4af0c 100644 --- a/shared/src/ledger/storage_api/collections/mod.rs +++ b/shared/src/ledger/storage_api/collections/mod.rs @@ -41,5 +41,5 @@ pub enum ReadError { /// An empty collection must be deleted from storage. pub trait LazyCollection { /// Create or use an existing vector with the given storage `key`. - fn new(key: storage::Key) -> Self; + fn open(key: storage::Key) -> Self; } From 8c4e2ca33466a0a54ae751ecf693ca042eb29345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 14 Sep 2022 10:46:45 +0200 Subject: [PATCH 71/81] update after rebase on #458, #465 handle TODOs to fix LazyVec's iter order to fix the API SM test --- .../storage_api/collections/lazy_vec.rs | 35 +++++++------------ .../src/ledger/storage_api/validation/mod.rs | 5 +-- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index f3a8c2bac8..0c91b0dbbc 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -2,7 +2,6 @@ use std::collections::BTreeSet; use std::marker::PhantomData; -use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; use derivative::Derivative; @@ -159,8 +158,7 @@ impl LazyVec { /// Get the sub-key of vector's elements storage fn get_data_key(&self, index: Index) -> storage::Key { - // TODO rebase on ordered prefix iter (https://github.com/anoma/namada/pull/458) and remove `.to_string()` - self.get_data_prefix().push(&index.to_string()).unwrap() + self.get_data_prefix().push(&index).unwrap() } /// Get the sub-key of vector's length storage @@ -258,7 +256,7 @@ where pub fn is_valid_sub_key( &self, key: &storage::Key, - ) -> ValidationResult> { + ) -> storage_api::Result> { let suffix = match key.split_prefix(&self.key) { None => { // not matching prefix, irrelevant @@ -266,7 +264,8 @@ where } Some(None) => { // no suffix, invalid - return Err(ValidationError::InvalidSubKey(key.clone())); + return Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result(); } Some(Some(suffix)) => suffix, }; @@ -279,14 +278,15 @@ where [DbKeySeg::StringSeg(sub_a), DbKeySeg::StringSeg(sub_b)] if sub_a == DATA_SUBKEY => { - // TODO rebase on ordered prefix iter (https://github.com/anoma/namada/pull/458) and parse with `KeySeg::parse` - if let Ok(index) = Index::from_str(sub_b) { + if let Ok(index) = storage::KeySeg::parse(sub_b.clone()) { Ok(Some(SubKey::Data(index))) } else { Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result() } } - _ => Err(ValidationError::InvalidSubKey(key.clone())), + _ => Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result(), } } @@ -304,29 +304,18 @@ where env: &ENV, builder: &mut Option>, key_changed: &storage::Key, - ) -> ValidationResult + ) -> storage_api::Result where - ENV: VpEnv, + ENV: for<'a> VpEnv<'a>, { if let Some(sub) = self.is_valid_sub_key(key_changed)? { let change = match sub { SubKey::Len => { - let data = validation::read_data(env, key_changed) - // TODO this has to propagate errors from VpEnv rather - // then replace them (e.g. it could be out-of-gas), but - // VpEnv::Error is generic, maybe we should make it - // concrete (only the VpEnv impls have extensible error - // types) - .map_err(|_| { - ValidationError::StorageError(key_changed.clone()) - })?; + let data = validation::read_data(env, key_changed)?; data.map(SubKeyWithData::Len) } SubKey::Data(index) => { - let data = validation::read_data(env, key_changed) - .map_err(|_| { - ValidationError::StorageError(key_changed.clone()) - })?; + let data = validation::read_data(env, key_changed)?; data.map(|data| SubKeyWithData::Data(index, data)) } }; diff --git a/shared/src/ledger/storage_api/validation/mod.rs b/shared/src/ledger/storage_api/validation/mod.rs index 30c1e2566b..ca0e779a75 100644 --- a/shared/src/ledger/storage_api/validation/mod.rs +++ b/shared/src/ledger/storage_api/validation/mod.rs @@ -4,6 +4,7 @@ use std::fmt::Debug; use borsh::BorshDeserialize; +use crate::ledger::storage_api; use crate::ledger::vp_env::VpEnv; use crate::types::storage; @@ -33,10 +34,10 @@ pub enum Data { pub fn read_data( env: &ENV, key: &storage::Key, -) -> Result>, ENV::Error> +) -> Result>, storage_api::Error> where T: BorshDeserialize, - ENV: VpEnv, + ENV: for<'a> VpEnv<'a>, { let pre = env.read_pre(key)?; let post = env.read_post(key)?; From 35b5f4013719d224895235e105d0e336c64a05fd Mon Sep 17 00:00:00 2001 From: brentstone Date: Wed, 14 Sep 2022 14:38:29 +0200 Subject: [PATCH 72/81] quick bug and documentation fix --- shared/src/ledger/storage_api/collections/lazy_vec.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index 0c91b0dbbc..c086297ea7 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -336,7 +336,7 @@ impl ValidationBuilder { /// /// The validation rules for a [`LazyVec`] are: /// - A difference in the vector's length must correspond to the - /// difference in how many elements where pushed versus how many + /// difference in how many elements were pushed versus how many /// elements were popped. /// - An empty vector must be deleted from storage /// - In addition, we check that indices of any changes are within an @@ -400,7 +400,7 @@ impl ValidationBuilder { }, } } - let added_len: u64 = deleted + let added_len: u64 = added .len() .try_into() .map_err(ValidationError::IndexOverflow)?; From 5396df98e460a30471c6785661b6d3b333f97cb1 Mon Sep 17 00:00:00 2001 From: brentstone Date: Wed, 14 Sep 2022 16:47:22 +0200 Subject: [PATCH 73/81] post-conditions for Transition::Update + some comments --- .../storage_api/collections/lazy_vec.rs | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index c086297ea7..79967cb9c3 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -336,8 +336,8 @@ impl ValidationBuilder { /// /// The validation rules for a [`LazyVec`] are: /// - A difference in the vector's length must correspond to the - /// difference in how many elements were pushed versus how many - /// elements were popped. + /// difference in how many elements were pushed versus how many elements + /// were popped. /// - An empty vector must be deleted from storage /// - In addition, we check that indices of any changes are within an /// expected range (i.e. the vectors indices should always be @@ -635,6 +635,7 @@ mod test { Just(Self(vec![])).boxed() } + // Apply a random transition to the state fn transitions(state: &Self::State) -> BoxedStrategy { if state.0.is_empty() { prop_oneof![arb_test_vec_item().prop_map(Transition::Push)] @@ -667,11 +668,14 @@ mod test { transition: &Self::Transition, ) -> bool { if state.0.is_empty() { + // Ensure that the pop or update transitions are not applied to + // an empty state !matches!( transition, Transition::Pop | Transition::Update { .. } ) } else if let Transition::Update { index, .. } = transition { + // Ensure that the update index is a valid one *index < (state.0.len() - 1) as Index } else { true @@ -742,10 +746,37 @@ mod test { ); } Transition::Update { index, value } => { + let old_len = state.lazy_vec.len(&state.storage).unwrap(); + let old_val = state + .lazy_vec + .get(&state.storage, *index) + .unwrap() + .unwrap(); + state .lazy_vec .update(&mut state.storage, *index, value.clone()) .unwrap(); + + // Post-conditions: + let new_len = state.lazy_vec.len(&state.storage).unwrap(); + let new_val = state + .lazy_vec + .get(&state.storage, *index) + .unwrap() + .unwrap(); + assert_eq!(old_len, new_len, "length must not change"); + assert_eq!( + &old_val, + state.eager_vec.get(*index as usize).unwrap(), + "old value must match the value at the same index in \ + the eager vec before it's updated" + ); + assert_eq!( + &new_val, value, + "new value must match that which was passed into the \ + Transition::Update" + ); } } From dad616b883c76a9ede8ea0275fdae8cfdbe1089f Mon Sep 17 00:00:00 2001 From: brentstone Date: Wed, 14 Sep 2022 20:07:58 +0200 Subject: [PATCH 74/81] WIP: StateMachine tests for lazy_vec validation update Cargo.lock --- .../storage_api/collections/lazy_vec.rs | 322 +-------- shared/src/types/storage.rs | 11 +- .../storage_api/collections/lazy_vec.txt | 7 + tests/src/lib.rs | 2 + tests/src/storage_api/collections/lazy_vec.rs | 631 ++++++++++++++++++ tests/src/storage_api/collections/mod.rs | 1 + tests/src/storage_api/mod.rs | 1 + tests/src/vm_host_env/tx.rs | 25 + 8 files changed, 684 insertions(+), 316 deletions(-) create mode 100644 tests/proptest-regressions/storage_api/collections/lazy_vec.txt create mode 100644 tests/src/storage_api/collections/lazy_vec.rs create mode 100644 tests/src/storage_api/collections/mod.rs create mode 100644 tests/src/storage_api/mod.rs diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index 79967cb9c3..d86f33ec08 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -55,7 +55,7 @@ pub enum SubKeyWithData { /// Possible actions that can modify a [`LazyVec`]. This roughly corresponds to /// the methods that have `StorageWrite` access. -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum Action { /// Push a value `T` into a [`LazyVec`] Push(T), @@ -86,10 +86,10 @@ pub enum ValidationError { #[error("Pop at a wrong index. Got {got}, expected {expected}.")] UnexpectedPopIndex { got: Index, expected: Index }, #[error( - "Update (combination of pop and push) at a wrong index. Got {got}, \ - expected {expected}." + "Update (or a combination of pop and push) at a wrong index. Got \ + {got}, expected maximum {max}." )] - UnexpectedUpdateIndex { got: Index, expected: Index }, + UnexpectedUpdateIndex { got: Index, max: Index }, #[error("An index has overflown its representation: {0}")] IndexOverflow(>::Error), #[error("Unexpected underflow in `{0} - {0}`")] @@ -323,8 +323,8 @@ where let builder = builder.get_or_insert(ValidationBuilder::default()); builder.changes.push(change); - return Ok(true); } + return Ok(true); } Ok(false) } @@ -474,34 +474,14 @@ impl ValidationBuilder { } } - // And finally iterate updates in increasing order of indices - let mut last_updated = Option::None; + // And finally iterate updates for index in updated { - if let Some(last_updated) = last_updated { - // Following additions should be at monotonically increasing - // indices - let expected = last_updated + 1; - if expected != index { - return Err(ValidationError::UnexpectedUpdateIndex { - got: index, - expected, - }); - } - } - last_updated = Some(index); - } - if let Some(index) = last_updated { - let expected = len_pre.checked_sub(deleted_len).ok_or( - ValidationError::UnexpectedUnderflow(len_pre, deleted_len), - )?; - if index != expected { - // The last update must be at the pre length value minus - // deleted_len. - // If something is added and then deleted in a - // single tx, it will never be visible here. + // Update index has to be within the length bounds + let max = len_pre + len_diff; + if index >= max { return Err(ValidationError::UnexpectedUpdateIndex { got: index, - expected: len_pre, + max, }); } } @@ -512,12 +492,6 @@ impl ValidationBuilder { #[cfg(test)] mod test { - use proptest::prelude::*; - use proptest::prop_state_machine; - use proptest::state_machine::{AbstractStateMachine, StateMachineTest}; - use proptest::test_runner::Config; - use test_log::test; - use super::*; use crate::ledger::storage::testing::TestStorage; @@ -556,280 +530,4 @@ mod test { Ok(()) } - - prop_state_machine! { - #![proptest_config(Config { - // Instead of the default 256, we only run 5 because otherwise it - // takes too long and it's preferable to crank up the number of - // transitions instead, to allow each case to run for more epochs as - // some issues only manifest once the model progresses further. - // Additionally, more cases will be explored every time this test is - // executed in the CI. - cases: 5, - .. Config::default() - })] - #[test] - /// A `StateMachineTest` implemented on `LazyVec` that manipulates - /// it with `Transition`s and checks its state against an in-memory - /// `std::collections::Vec`. - fn lazy_vec_api_state_machine_test(sequential 1..100 => ConcreteLazyVecState); - - } - - /// Some borsh-serializable type with arbitrary fields to be used inside - /// LazyVec state machine test - #[derive( - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - PartialEq, - Eq, - PartialOrd, - Ord, - )] - struct TestVecItem { - x: u64, - y: bool, - } - - #[derive(Debug)] - struct ConcreteLazyVecState { - // The eager vec in `AbstractLazyVecState` is not visible in `impl - // StateMachineTest for ConcreteLazyVecState`, it's only used to drive - // transition generation, so we duplicate it here and apply the - // transitions on it the same way (with - // `fn apply_transition_on_eager_vec`) - eager_vec: Vec, - lazy_vec: LazyVec, - storage: TestStorage, - } - - #[derive(Clone, Debug)] - struct AbstractLazyVecState(Vec); - - /// Possible transitions that can modify a [`LazyVec`]. This roughly - /// corresponds to the methods that have `StorageWrite` access and is very - /// similar to [`Action`] - #[derive(Clone, Debug)] - pub enum Transition { - /// Push a value `T` into a [`LazyVec`] - Push(T), - /// Pop a value from a [`LazyVec`] - Pop, - /// Update a value `T` at index from pre to post state in a - /// [`LazyVec`] - Update { - /// index at which the value is updated - index: Index, - /// value to update the element to - value: T, - }, - } - - impl AbstractStateMachine for AbstractLazyVecState { - type State = Self; - type Transition = Transition; - - fn init_state() -> BoxedStrategy { - Just(Self(vec![])).boxed() - } - - // Apply a random transition to the state - fn transitions(state: &Self::State) -> BoxedStrategy { - if state.0.is_empty() { - prop_oneof![arb_test_vec_item().prop_map(Transition::Push)] - .boxed() - } else { - let indices: Vec = - (0_usize..state.0.len()).map(|ix| ix as Index).collect(); - let arb_index = proptest::sample::select(indices); - prop_oneof![ - Just(Transition::Pop), - arb_test_vec_item().prop_map(Transition::Push), - (arb_index, arb_test_vec_item()).prop_map( - |(index, value)| Transition::Update { index, value } - ) - ] - .boxed() - } - } - - fn apply_abstract( - mut state: Self::State, - transition: &Self::Transition, - ) -> Self::State { - apply_transition_on_eager_vec(&mut state.0, transition); - state - } - - fn preconditions( - state: &Self::State, - transition: &Self::Transition, - ) -> bool { - if state.0.is_empty() { - // Ensure that the pop or update transitions are not applied to - // an empty state - !matches!( - transition, - Transition::Pop | Transition::Update { .. } - ) - } else if let Transition::Update { index, .. } = transition { - // Ensure that the update index is a valid one - *index < (state.0.len() - 1) as Index - } else { - true - } - } - } - - impl StateMachineTest for ConcreteLazyVecState { - type Abstract = AbstractLazyVecState; - type ConcreteState = Self; - - fn init_test( - _initial_state: ::State, - ) -> Self::ConcreteState { - Self { - eager_vec: vec![], - lazy_vec: LazyVec::open( - storage::Key::parse("key_path/arbitrary").unwrap(), - ), - storage: TestStorage::default(), - } - } - - fn apply_concrete( - mut state: Self::ConcreteState, - transition: ::Transition, - ) -> Self::ConcreteState { - // Transition application on lazy vec and post-conditions: - match dbg!(&transition) { - Transition::Push(value) => { - let old_len = state.lazy_vec.len(&state.storage).unwrap(); - - state - .lazy_vec - .push(&mut state.storage, value.clone()) - .unwrap(); - - // Post-conditions: - let new_len = state.lazy_vec.len(&state.storage).unwrap(); - let stored_value = state - .lazy_vec - .get(&state.storage, new_len - 1) - .unwrap() - .unwrap(); - assert_eq!( - &stored_value, value, - "the new item must be added to the back" - ); - assert_eq!(old_len + 1, new_len, "length must increment"); - } - Transition::Pop => { - let old_len = state.lazy_vec.len(&state.storage).unwrap(); - - let popped = state - .lazy_vec - .pop(&mut state.storage) - .unwrap() - .unwrap(); - - // Post-conditions: - let new_len = state.lazy_vec.len(&state.storage).unwrap(); - assert_eq!(old_len, new_len + 1, "length must decrement"); - assert_eq!( - &popped, - state.eager_vec.last().unwrap(), - "popped element matches the last element in eager vec \ - before it's updated" - ); - } - Transition::Update { index, value } => { - let old_len = state.lazy_vec.len(&state.storage).unwrap(); - let old_val = state - .lazy_vec - .get(&state.storage, *index) - .unwrap() - .unwrap(); - - state - .lazy_vec - .update(&mut state.storage, *index, value.clone()) - .unwrap(); - - // Post-conditions: - let new_len = state.lazy_vec.len(&state.storage).unwrap(); - let new_val = state - .lazy_vec - .get(&state.storage, *index) - .unwrap() - .unwrap(); - assert_eq!(old_len, new_len, "length must not change"); - assert_eq!( - &old_val, - state.eager_vec.get(*index as usize).unwrap(), - "old value must match the value at the same index in \ - the eager vec before it's updated" - ); - assert_eq!( - &new_val, value, - "new value must match that which was passed into the \ - Transition::Update" - ); - } - } - - // Apply transition in the eager vec for comparison - apply_transition_on_eager_vec(&mut state.eager_vec, &transition); - - // Global post-conditions: - - // All items in eager vec must be present in lazy vec - for (ix, expected_item) in state.eager_vec.iter().enumerate() { - let got = state - .lazy_vec - .get(&state.storage, ix as Index) - .unwrap() - .expect("The expected item must be present in lazy vec"); - assert_eq!(expected_item, &got, "at index {ix}"); - } - - // All items in lazy vec must be present in eager vec - for (ix, expected_item) in - state.lazy_vec.iter(&state.storage).unwrap().enumerate() - { - let expected_item = expected_item.unwrap(); - let got = state - .eager_vec - .get(ix) - .expect("The expected item must be present in eager vec"); - assert_eq!(&expected_item, got, "at index {ix}"); - } - - state - } - } - - /// Generate an arbitrary `TestVecItem` - fn arb_test_vec_item() -> impl Strategy { - (any::(), any::()).prop_map(|(x, y)| TestVecItem { x, y }) - } - - /// Apply `Transition` on an eager `Vec`. - fn apply_transition_on_eager_vec( - vec: &mut Vec, - transition: &Transition, - ) { - match transition { - Transition::Push(value) => vec.push(value.clone()), - Transition::Pop => { - let _popped = vec.pop(); - } - Transition::Update { index, value } => { - let entry = vec.get_mut(*index as usize).unwrap(); - *entry = value.clone(); - } - } - } } diff --git a/shared/src/types/storage.rs b/shared/src/types/storage.rs index b5d72b7b02..1c3f6b1313 100644 --- a/shared/src/types/storage.rs +++ b/shared/src/types/storage.rs @@ -378,15 +378,18 @@ impl Key { /// - `Some(None)` if the prefix is matched, but it has no suffix, or /// - `None` if it doesn't match pub fn split_prefix(&self, prefix: &Self) -> Option> { - if self.segments.len() < prefix.len() { + if self.segments.len() < prefix.segments.len() { return None; } else if self == prefix { return Some(None); } - let mut self_prefix = self.segments.clone(); - let rest = self_prefix.split_off(prefix.len()); + // This is safe, because we check that the length of segments in self >= + // in prefix above + let (self_prefix, rest) = self.segments.split_at(prefix.segments.len()); if self_prefix == prefix.segments { - Some(Some(Key { segments: rest })) + Some(Some(Key { + segments: rest.to_vec(), + })) } else { None } diff --git a/tests/proptest-regressions/storage_api/collections/lazy_vec.txt b/tests/proptest-regressions/storage_api/collections/lazy_vec.txt new file mode 100644 index 0000000000..97a16dcbeb --- /dev/null +++ b/tests/proptest-regressions/storage_api/collections/lazy_vec.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 4330a283e32b5ff3f38d0af2298e1e98c30b1901c1027b572070a1af3356688e # shrinks to (initial_state, transitions) = (AbstractLazyVecState { valid_transitions: [], committed_transitions: [] }, [Push(TestVecItem { x: 15352583996758053781, y: true }), Pop, CommitTx, Push(TestVecItem { x: 6904067244182623445, y: false }), CommitTx, Pop, Push(TestVecItem { x: 759762287021483883, y: true }), Push(TestVecItem { x: 7885704082671389345, y: true }), Pop, Pop, Push(TestVecItem { x: 2762344561419437403, y: false }), Push(TestVecItem { x: 11448034977049028254, y: false }), Update { index: 0, value: TestVecItem { x: 7097339541298715775, y: false } }, Pop, Pop, Push(TestVecItem { x: 457884036257686887, y: true }), CommitTx, Push(TestVecItem { x: 17719281119971095810, y: true }), CommitTx, Push(TestVecItem { x: 4612681906563857058, y: false }), CommitTx, CommitTx, Pop, CommitTx, Pop, Push(TestVecItem { x: 4269537158299505726, y: false }), CommitTx, Pop, Pop, CommitTx, CommitTx, CommitTx, CommitTx, Push(TestVecItem { x: 9020889554694833528, y: true }), Push(TestVecItem { x: 4022797489860699620, y: false }), Update { index: 0, value: TestVecItem { x: 6485081152860611495, y: true } }, Pop, CommitTx, Push(TestVecItem { x: 14470031031894733310, y: false }), Push(TestVecItem { x: 1113274973965556867, y: true }), Push(TestVecItem { x: 4122902042678339346, y: false }), Push(TestVecItem { x: 9672639635189564637, y: true }), Pop, Pop, Pop, CommitTx, Update { index: 0, value: TestVecItem { x: 6372193991838429158, y: false } }, Push(TestVecItem { x: 15140852824102579010, y: false }), Pop, Pop, Pop, Push(TestVecItem { x: 4012218522073776592, y: false }), Push(TestVecItem { x: 10637893847792386454, y: true }), Push(TestVecItem { x: 3357788278949652885, y: false }), CommitTx, CommitTx, Pop, Pop, CommitTx, Pop, Push(TestVecItem { x: 11768518086398350214, y: true }), Push(TestVecItem { x: 4361685178396183644, y: true }), Pop, CommitTx, Push(TestVecItem { x: 2450907664540456425, y: false }), Push(TestVecItem { x: 18184919885943118586, y: true }), Update { index: 1, value: TestVecItem { x: 10611906658537706503, y: false } }, Push(TestVecItem { x: 4887827541279511396, y: false }), Update { index: 0, value: TestVecItem { x: 13021774003761931172, y: false } }, Push(TestVecItem { x: 3644118228573898014, y: false }), CommitTx, Update { index: 0, value: TestVecItem { x: 1276840798381751183, y: false } }, Pop, Pop]) diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 1b75f83bdc..78ebf2473c 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -12,6 +12,8 @@ mod e2e; #[cfg(test)] mod native_vp; pub mod storage; +#[cfg(test)] +mod storage_api; /// Using this import requires `tracing` and `tracing-subscriber` dependencies. /// Set env var `RUST_LOG=info` to see the logs from a test run (and diff --git a/tests/src/storage_api/collections/lazy_vec.rs b/tests/src/storage_api/collections/lazy_vec.rs new file mode 100644 index 0000000000..20ee80592d --- /dev/null +++ b/tests/src/storage_api/collections/lazy_vec.rs @@ -0,0 +1,631 @@ +#[cfg(test)] +mod tests { + use std::convert::TryInto; + + use borsh::{BorshDeserialize, BorshSerialize}; + use namada::types::address::{self, Address}; + use namada::types::storage; + use namada_tx_prelude::storage::KeySeg; + use namada_tx_prelude::storage_api::collections::{ + lazy_vec, LazyCollection, LazyVec, + }; + use proptest::prelude::*; + use proptest::prop_state_machine; + use proptest::state_machine::{AbstractStateMachine, StateMachineTest}; + use proptest::test_runner::Config; + use test_log::test; + + use crate::tx::tx_host_env; + use crate::vp::vp_host_env; + + prop_state_machine! { + #![proptest_config(Config { + // Instead of the default 256, we only run 5 because otherwise it + // takes too long and it's preferable to crank up the number of + // transitions instead, to allow each case to run for more epochs as + // some issues only manifest once the model progresses further. + // Additionally, more cases will be explored every time this test is + // executed in the CI. + cases: 5, + .. Config::default() + })] + #[test] + fn lazy_vec_api_state_machine_test(sequential 1..100 => ConcreteLazyVecState); + } + + /// Some borsh-serializable type with arbitrary fields to be used inside + /// LazyVec state machine test + #[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, + PartialOrd, + Ord, + )] + struct TestVecItem { + x: u64, + y: bool, + } + + /// A `StateMachineTest` implemented on this struct manipulates it with + /// `Transition`s, which are also being accumulated into + /// `current_transitions`. It then: + /// + /// - checks its state against an in-memory `std::collections::Vec` + /// - runs validation and checks that the `LazyVec::Action`s reported from + /// validation match with transitions that were applied + /// + /// Additionally, one of the transitions is to commit a block and/or + /// transaction, during which the currently accumulated state changes are + /// persisted, or promoted from transaction write log to block's write log. + #[derive(Debug)] + struct ConcreteLazyVecState { + /// Address is used to prefix the storage key of the `lazy_vec` in + /// order to simulate a transaction and a validity predicate + /// check from changes on the `lazy_vec` + address: Address, + /// In the test, we apply the same transitions on the `lazy_vec` as on + /// `eager_vec` to check that `lazy_vec`'s state is consistent with + /// `eager_vec`. + eager_vec: Vec, + /// Handle to a lazy vec + lazy_vec: LazyVec, + /// Valid LazyVec changes in the current transaction + current_transitions: Vec>, + } + + #[derive(Clone, Debug)] + struct AbstractLazyVecState { + /// Valid LazyVec changes in the current transaction + valid_transitions: Vec>, + /// Valid LazyVec changes committed to storage + committed_transitions: Vec>, + } + + /// Possible transitions that can modify a [`LazyVec`]. This roughly + /// corresponds to the methods that have `StorageWrite` access and is very + /// similar to [`Action`] + #[derive(Clone, Debug)] + pub enum Transition { + /// Commit all valid transitions in the current transaction + CommitTx, + /// Commit all valid transitions in the current transaction and also + /// commit the current block + CommitTxAndBlock, + /// Push a value `T` into a [`LazyVec`] + Push(T), + /// Pop a value from a [`LazyVec`] + Pop, + /// Update a value `T` at index from pre to post state in a + /// [`LazyVec`] + Update { + /// index at which the value is updated + index: lazy_vec::Index, + /// value to update the element to + value: T, + }, + } + + impl AbstractStateMachine for AbstractLazyVecState { + type State = Self; + type Transition = Transition; + + fn init_state() -> BoxedStrategy { + Just(Self { + valid_transitions: vec![], + committed_transitions: vec![], + }) + .boxed() + } + + // Apply a random transition to the state + fn transitions(state: &Self::State) -> BoxedStrategy { + let length = state.len(); + if length == 0 { + prop_oneof![ + 1 => Just(Transition::CommitTx), + 1 => Just(Transition::CommitTxAndBlock), + 3 => arb_test_vec_item().prop_map(Transition::Push) + ] + .boxed() + } else { + let arb_index = || { + let indices: Vec = (0..length).collect(); + proptest::sample::select(indices) + }; + prop_oneof![ + 1 => Just(Transition::CommitTx), + 1 => Just(Transition::CommitTxAndBlock), + 3 => (arb_index(), arb_test_vec_item()).prop_map( + |(index, value)| Transition::Update { index, value } + ), + 3 => Just(Transition::Pop), + 5 => arb_test_vec_item().prop_map(Transition::Push), + ] + .boxed() + } + } + + fn apply_abstract( + mut state: Self::State, + transition: &Self::Transition, + ) -> Self::State { + match transition { + Transition::CommitTx => { + let valid_actions_to_commit = + std::mem::take(&mut state.valid_transitions); + state + .committed_transitions + .extend(valid_actions_to_commit.into_iter()); + } + _ => state.valid_transitions.push(transition.clone()), + } + state + } + + fn preconditions( + state: &Self::State, + transition: &Self::Transition, + ) -> bool { + let length = state.len(); + if length == 0 { + // Ensure that the pop or update transitions are not applied to + // an empty state + !matches!( + transition, + Transition::Pop | Transition::Update { .. } + ) + } else if let Transition::Update { index, .. } = transition { + // Ensure that the update index is a valid one + *index < (length - 1) + } else { + true + } + } + } + + impl StateMachineTest for ConcreteLazyVecState { + type Abstract = AbstractLazyVecState; + type ConcreteState = Self; + + fn init_test( + _initial_state: ::State, + ) -> Self::ConcreteState { + // Init transaction env in which we'll be applying the transitions + tx_host_env::init(); + + // The lazy_vec's path must be prefixed by the address to be able + // to trigger a validity predicate on it + let address = address::testing::established_address_1(); + tx_host_env::with(|env| env.spawn_accounts([&address])); + let lazy_vec_prefix: storage::Key = address.to_db_key().into(); + + Self { + address, + eager_vec: vec![], + lazy_vec: LazyVec::open( + lazy_vec_prefix.push(&"arbitrary".to_string()).unwrap(), + ), + current_transitions: vec![], + } + } + + fn apply_concrete( + mut state: Self::ConcreteState, + transition: ::Transition, + ) -> Self::ConcreteState { + // Apply transitions in transaction env + let ctx = tx_host_env::ctx(); + + // Persist the transitions in the current tx, or clear previous ones + // if we're committing a tx + match &transition { + Transition::CommitTx | Transition::CommitTxAndBlock => { + state.current_transitions = vec![]; + } + _ => { + state.current_transitions.push(transition.clone()); + } + } + + // Transition application on lazy vec and post-conditions: + match &transition { + Transition::CommitTx => { + // commit the tx without committing the block + tx_host_env::with(|env| env.write_log.commit_tx()); + } + Transition::CommitTxAndBlock => { + // commit the tx and the block + tx_host_env::commit_tx_and_block(); + } + Transition::Push(value) => { + let old_len = state.lazy_vec.len(ctx).unwrap(); + + state.lazy_vec.push(ctx, value.clone()).unwrap(); + + // Post-conditions: + let new_len = state.lazy_vec.len(ctx).unwrap(); + let stored_value = + state.lazy_vec.get(ctx, new_len - 1).unwrap().unwrap(); + assert_eq!( + &stored_value, value, + "the new item must be added to the back" + ); + assert_eq!(old_len + 1, new_len, "length must increment"); + + state.assert_validation_accepted(new_len); + } + Transition::Pop => { + let old_len = state.lazy_vec.len(ctx).unwrap(); + + let popped = state.lazy_vec.pop(ctx).unwrap().unwrap(); + + // Post-conditions: + let new_len = state.lazy_vec.len(ctx).unwrap(); + assert_eq!(old_len, new_len + 1, "length must decrement"); + assert_eq!( + &popped, + state.eager_vec.last().unwrap(), + "popped element matches the last element in eager vec \ + before it's updated" + ); + + state.assert_validation_accepted(new_len); + } + Transition::Update { index, value } => { + let old_len = state.lazy_vec.len(ctx).unwrap(); + let old_val = + state.lazy_vec.get(ctx, *index).unwrap().unwrap(); + + state.lazy_vec.update(ctx, *index, value.clone()).unwrap(); + + // Post-conditions: + let new_len = state.lazy_vec.len(ctx).unwrap(); + let new_val = + state.lazy_vec.get(ctx, *index).unwrap().unwrap(); + assert_eq!(old_len, new_len, "length must not change"); + assert_eq!( + &old_val, + state.eager_vec.get(*index as usize).unwrap(), + "old value must match the value at the same index in \ + the eager vec before it's updated" + ); + assert_eq!( + &new_val, value, + "new value must match that which was passed into the \ + Transition::Update" + ); + + state.assert_validation_accepted(new_len); + } + } + + // Apply transition in the eager vec for comparison + apply_transition_on_eager_vec(&mut state.eager_vec, &transition); + + // Global post-conditions: + + // All items in eager vec must be present in lazy vec + for (ix, expected_item) in state.eager_vec.iter().enumerate() { + let got = state + .lazy_vec + .get(ctx, ix as lazy_vec::Index) + .unwrap() + .expect("The expected item must be present in lazy vec"); + assert_eq!(expected_item, &got, "at index {ix}"); + } + + // All items in lazy vec must be present in eager vec + for (ix, expected_item) in + state.lazy_vec.iter(ctx).unwrap().enumerate() + { + let expected_item = expected_item.unwrap(); + let got = state + .eager_vec + .get(ix) + .expect("The expected item must be present in eager vec"); + assert_eq!(&expected_item, got, "at index {ix}"); + } + + state + } + } + + impl AbstractLazyVecState { + /// Find the length of the vector from the applied transitions + fn len(&self) -> u64 { + (vec_len_diff_from_transitions(self.committed_transitions.iter()) + + vec_len_diff_from_transitions(self.valid_transitions.iter())) + .try_into() + .expect( + "It shouldn't be possible to underflow length from all \ + transactions applied in abstract state", + ) + } + } + + /// Find the difference in length of the vector from the applied transitions + fn vec_len_diff_from_transitions<'a>( + all_transitions: impl Iterator>, + ) -> i64 { + let mut push_count: i64 = 0; + let mut pop_count: i64 = 0; + + for trans in all_transitions { + match trans { + Transition::CommitTx + | Transition::CommitTxAndBlock + | Transition::Update { .. } => {} + Transition::Push(_) => push_count += 1, + Transition::Pop => pop_count += 1, + } + } + push_count - pop_count + } + + impl ConcreteLazyVecState { + fn assert_validation_accepted(&self, new_vec_len: u64) { + // Init the VP env from tx env in which we applied the vec + // transitions + let tx_env = tx_host_env::take(); + vp_host_env::init_from_tx(self.address.clone(), tx_env, |_| {}); + + // Simulate a validity predicate run using the lazy vec's validation + // helpers + let changed_keys = + vp_host_env::with(|env| env.all_touched_storage_keys()); + + let mut validation_builder = None; + + // Push followed by pop is a no-op, in which case we'd still see the + // changed keys for these actions, but they wouldn't affect the + // validation result and they never get persisted, but we'd still + // them as changed key here. To guard against this case, + // we check that `vec_len_from_transitions` is not empty. + let vec_len_diff = + vec_len_diff_from_transitions(self.current_transitions.iter()); + + // To help debug validation issues... + dbg!( + &self.current_transitions, + &changed_keys + .iter() + .map(storage::Key::to_string) + .collect::>() + ); + + for key in &changed_keys { + let is_sub_key = self + .lazy_vec + .accumulate( + vp_host_env::ctx(), + &mut validation_builder, + key, + ) + .unwrap(); + + assert!( + is_sub_key, + "We're only modifying the lazy_vec's keys here. Key: \ + \"{key}\", vec length diff {vec_len_diff}" + ); + } + if !changed_keys.is_empty() && vec_len_diff != 0 { + assert!( + validation_builder.is_some(), + "If some keys were changed, the builder must get filled in" + ); + let actions = validation_builder.unwrap().build().expect( + "With valid transitions only, validation should always \ + pass", + ); + let mut actions_to_check = actions.clone(); + + // Check that every transition has a corresponding action from + // validation. We drop the found actions to check that all + // actions are matched too. + let current_transitions = normalize_transitions( + &self.current_transitions, + new_vec_len, + ); + for transition in ¤t_transitions { + match transition { + Transition::CommitTx | Transition::CommitTxAndBlock => { + } + Transition::Push(expected_val) => { + let mut ix = 0; + while ix < actions_to_check.len() { + if let lazy_vec::Action::Push(val) = + &actions_to_check[ix] + { + if expected_val == val { + actions_to_check.remove(ix); + break; + } + } + ix += 1; + } + } + Transition::Pop => { + let mut ix = 0; + while ix < actions_to_check.len() { + if let lazy_vec::Action::Pop(_val) = + &actions_to_check[ix] + { + actions_to_check.remove(ix); + break; + } + ix += 1; + } + } + Transition::Update { + index: expected_index, + value, + } => { + let mut ix = 0; + while ix < actions_to_check.len() { + if let lazy_vec::Action::Update { + index, + pre: _, + post, + } = &actions_to_check[ix] + { + if expected_index == index && post == value + { + actions_to_check.remove(ix); + break; + } + } + ix += 1; + } + } + } + } + + assert!( + actions_to_check.is_empty(), + "All the actions reported from validation {actions:#?} \ + should have been matched with SM transitions \ + {current_transitions:#?}, but these actions didn't \ + match: {actions_to_check:#?}", + ) + } + + // Put the tx_env back before checking the result + tx_host_env::set_from_vp_env(vp_host_env::take()); + } + } + + /// Generate an arbitrary `TestVecItem` + fn arb_test_vec_item() -> impl Strategy { + (any::(), any::()).prop_map(|(x, y)| TestVecItem { x, y }) + } + + /// Apply `Transition` on an eager `Vec`. + fn apply_transition_on_eager_vec( + vec: &mut Vec, + transition: &Transition, + ) { + match transition { + Transition::CommitTx | Transition::CommitTxAndBlock => {} + Transition::Push(value) => vec.push(value.clone()), + Transition::Pop => { + let _popped = vec.pop(); + } + Transition::Update { index, value } => { + let entry = vec.get_mut(*index as usize).unwrap(); + *entry = value.clone(); + } + } + } + + /// Normalize transitions: + /// - pop at ix + push(val) at ix -> update(ix, val) + /// - push(val) at ix + update(ix, new_val) -> push(new_val) at ix + /// - update(ix, val) + update(ix, new_val) -> update(ix, new_val) + /// + /// Note that the normalizable transitions pairs do not have to be directly + /// next to each other, but their order does matter. + fn normalize_transitions( + transitions: &[Transition], + new_vec_len: u64, + ) -> Vec> { + let stack_start_pos = ((new_vec_len as i64) + - vec_len_diff_from_transitions(transitions.iter())) + as u64; + let mut stack_pos = stack_start_pos; + let mut collapsed = vec![]; + 'outer: for transition in transitions { + match transition { + Transition::CommitTx | Transition::CommitTxAndBlock => { + collapsed.push(transition.clone()) + } + Transition::Push(value) => { + // If there are some pops, the last one can be collapsed + // with this push + if stack_pos < stack_start_pos { + // Find the pop from the back + let mut found_ix = None; + for (ix, transition) in + collapsed.iter().enumerate().rev() + { + if let Transition::Pop = transition { + found_ix = Some(ix); + break; + } + } + let ix = found_ix.expect("Pop must be found"); + // pop at ix + push(val) at ix -> update(ix, val) + + // Replace the Pop with an Update and don't insert the + // Push + *collapsed.get_mut(ix).unwrap() = Transition::Update { + index: stack_pos, + value: value.clone(), + }; + } else { + collapsed.push(transition.clone()); + } + stack_pos += 1; + } + Transition::Pop => { + collapsed.push(transition.clone()); + stack_pos -= 1; + } + Transition::Update { index, value } => { + // If there are some pushes, check if one of them is at the + // same index as this update + if stack_pos > stack_start_pos { + let mut current_pos = stack_start_pos; + for (ix, collapsed_transition) in + collapsed.iter().enumerate() + { + match collapsed_transition { + Transition::CommitTx + | Transition::CommitTxAndBlock => {} + Transition::Push(_) => { + if ¤t_pos == index { + // push(val) at `ix` + update(ix, + // new_val) -> + // push(new_val) at `ix` + + // Replace the Push with the new Push of + // Update's + // value and don't insert the Update + *collapsed.get_mut(ix).unwrap() = + Transition::Push(value.clone()); + continue 'outer; + } + current_pos += 1; + } + Transition::Pop => { + current_pos -= 1; + } + Transition::Update { + index: prev_update_index, + value: _, + } => { + if index == prev_update_index { + // update(ix, val) + update(ix, new_val) + // -> update(ix, new_val) + + // Replace the Update with the new + // Update instead of inserting it + *collapsed.get_mut(ix).unwrap() = + transition.clone(); + continue 'outer; + } + } + } + } + } + collapsed.push(transition.clone()) + } + } + } + collapsed + } +} diff --git a/tests/src/storage_api/collections/mod.rs b/tests/src/storage_api/collections/mod.rs new file mode 100644 index 0000000000..d874b88e22 --- /dev/null +++ b/tests/src/storage_api/collections/mod.rs @@ -0,0 +1 @@ +mod lazy_vec; diff --git a/tests/src/storage_api/mod.rs b/tests/src/storage_api/mod.rs new file mode 100644 index 0000000000..bc487bd59e --- /dev/null +++ b/tests/src/storage_api/mod.rs @@ -0,0 +1 @@ +mod collections; diff --git a/tests/src/vm_host_env/tx.rs b/tests/src/vm_host_env/tx.rs index 728c488bca..6e23ced06b 100644 --- a/tests/src/vm_host_env/tx.rs +++ b/tests/src/vm_host_env/tx.rs @@ -18,6 +18,8 @@ use namada::vm::{self, WasmCacheRwAccess}; use namada_tx_prelude::{BorshSerialize, Ctx}; use tempfile::TempDir; +use crate::vp::TestVpEnv; + /// Tx execution context provides access to host env functions static mut CTX: Ctx = unsafe { Ctx::new() }; @@ -235,6 +237,29 @@ mod native_tx_host_env { with(|env| env.commit_tx_and_block()) } + /// Set the [`TestTxEnv`] back from a [`TestVpEnv`]. This is useful when + /// testing validation with multiple transactions that accumulate some state + /// changes. + pub fn set_from_vp_env(vp_env: TestVpEnv) { + let TestVpEnv { + storage, + write_log, + tx, + vp_wasm_cache, + vp_cache_dir, + .. + } = vp_env; + let tx_env = TestTxEnv { + storage, + write_log, + vp_wasm_cache, + vp_cache_dir, + tx, + ..Default::default() + }; + set(tx_env); + } + /// A helper macro to create implementations of the host environment /// functions exported to wasm, which uses the environment from the /// `ENV` variable. From 4b60051a5f72f137dc3321145a0ff878132df0fe Mon Sep 17 00:00:00 2001 From: brentstone Date: Fri, 16 Sep 2022 17:15:14 +0200 Subject: [PATCH 75/81] WIP: validation for lazy_map --- .../storage_api/collections/lazy_map.rs | 148 ++++- .../storage_api/collections/lazy_map.txt | 7 + tests/src/storage_api/collections/lazy_map.rs | 608 ++++++++++++++++++ tests/src/storage_api/collections/mod.rs | 1 + 4 files changed, 761 insertions(+), 3 deletions(-) create mode 100644 tests/proptest-regressions/storage_api/collections/lazy_map.txt create mode 100644 tests/src/storage_api/collections/lazy_map.rs diff --git a/shared/src/ledger/storage_api/collections/lazy_map.rs b/shared/src/ledger/storage_api/collections/lazy_map.rs index a47ff0734a..3801848b8f 100644 --- a/shared/src/ledger/storage_api/collections/lazy_map.rs +++ b/shared/src/ledger/storage_api/collections/lazy_map.rs @@ -3,12 +3,15 @@ use std::marker::PhantomData; use borsh::{BorshDeserialize, BorshSerialize}; +use derivative::Derivative; +use thiserror::Error; use super::super::Result; use super::{LazyCollection, ReadError}; -use crate::ledger::storage_api::validation::Data; +use crate::ledger::storage_api::validation::{self, Data}; use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; -use crate::types::storage::{self, KeySeg}; +use crate::ledger::vp_env::VpEnv; +use crate::types::storage::{self, DbKeySeg, KeySeg}; /// Subkey corresponding to the data elements of the LazyMap pub const DATA_SUBKEY: &str = "data"; @@ -52,12 +55,21 @@ pub enum SubKeyWithData { /// the methods that have `StorageWrite` access. /// TODO: In a nested collection, `V` may be an action inside the nested /// collection. -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum Action { /// Insert or update a value `V` at key `K` in a [`LazyMap`]. Insert(K, V), /// Remove a value `V` at key `K` from a [`LazyMap`]. Remove(K, V), + /// Update a value `V` at key `K` in a [`LazyMap`]. + Update { + /// key at which the value is updated + key: K, + /// value before the update + pre: V, + /// value after the update + post: V, + }, } /// TODO: In a nested collection, `V` may be an action inside the nested @@ -70,6 +82,46 @@ pub enum Nested { Remove(K, V), } +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum ValidationError { + #[error("Storage error in reading key {0}")] + StorageError(storage::Key), + // #[error("Incorrect difference in LazyVec's length")] + // InvalidLenDiff, + // #[error("An empty LazyVec must be deleted from storage")] + // EmptyVecShouldBeDeleted, + // #[error("Push at a wrong index. Got {got}, expected {expected}.")] + // UnexpectedPushIndex { got: Index, expected: Index }, + // #[error("Pop at a wrong index. Got {got}, expected {expected}.")] + // UnexpectedPopIndex { got: Index, expected: Index }, + // #[error( + // "Update (combination of pop and push) at a wrong index. Got {got}, + // \ expected {expected}." + // )] + // UnexpectedUpdateIndex { got: Index, expected: Index }, + // #[error("An index has overflown its representation: {0}")] + // IndexOverflow(>::Error), + // #[error("Unexpected underflow in `{0} - {0}`")] + // UnexpectedUnderflow(Index, Index), + #[error("Invalid storage key {0}")] + InvalidSubKey(storage::Key), +} + +/// [`LazyMap`] validation result +pub type ValidationResult = std::result::Result; + +/// [`LazyMap`] validation builder from storage changes. The changes can be +/// accumulated with `LazyMap::validate()` and then turned into a list +/// of valid actions on the map with `ValidationBuilder::build()`. +#[derive(Debug, Derivative)] +// https://mcarton.github.io/rust-derivative/latest/Default.html#custom-bound +#[derivative(Default(bound = ""))] +pub struct ValidationBuilder { + /// The accumulator of found changes under the vector + pub changes: Vec>, +} + impl LazyCollection for LazyMap where K: storage::KeySeg, @@ -247,6 +299,96 @@ where ) -> Result<()> { storage.write(storage_key, val) } + + /// Check if the given storage key is a valid LazyMap sub-key and if so + /// return which one + pub fn is_valid_sub_key( + &self, + key: &storage::Key, + ) -> storage_api::Result>> { + let suffix = match key.split_prefix(&self.key) { + None => { + // not matching prefix, irrelevant + return Ok(None); + } + Some(None) => { + // no suffix, invalid + return Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result(); + } + Some(Some(suffix)) => suffix, + }; + + // Match the suffix against expected sub-keys + match &suffix.segments[..] { + [DbKeySeg::StringSeg(sub_a), DbKeySeg::StringSeg(sub_b)] + if sub_a == DATA_SUBKEY => + { + if let Ok(key_in_kv) = storage::KeySeg::parse(sub_b.clone()) { + Ok(Some(SubKey::Data(key_in_kv))) + } else { + Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result() + } + } + _ => Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result(), + } + } + + /// Accumulate storage changes inside a [`ValidationBuilder`]. This is + /// typically done by the validity predicate while looping through the + /// changed keys. If the resulting `builder` is not `None`, one must + /// call `fn build()` on it to get the validation result. + /// This function will return `Ok(true)` if the storage key is a valid + /// sub-key of this collection, `Ok(false)` if the storage key doesn't match + /// the prefix of this collection, or fail with + /// [`ValidationError::InvalidSubKey`] if the prefix matches this + /// collection, but the key itself is not recognized. + pub fn accumulate( + &self, + env: &ENV, + builder: &mut Option>, + key_changed: &storage::Key, + ) -> storage_api::Result + where + ENV: for<'a> VpEnv<'a>, + { + if let Some(sub) = self.is_valid_sub_key(key_changed)? { + let SubKey::Data(key) = sub; + let data = validation::read_data(env, key_changed)?; + let change = data.map(|data| SubKeyWithData::Data(key, data)); + if let Some(change) = change { + let builder = + builder.get_or_insert(ValidationBuilder::default()); + builder.changes.push(change); + } + return Ok(true); + } + Ok(false) + } +} + +impl ValidationBuilder +where + K: storage::KeySeg + Ord + Clone, +{ + /// Build a list of actions from storage changes. + pub fn build(self) -> Vec> { + self.changes + .into_iter() + .map(|change| { + let SubKeyWithData::Data(key, data) = change; + match data { + Data::Add { post } => Action::Insert(key, post), + Data::Update { pre, post } => { + Action::Update { key, pre, post } + } + Data::Delete { pre } => Action::Remove(key, pre), + } + }) + .collect() + } } #[cfg(test)] diff --git a/tests/proptest-regressions/storage_api/collections/lazy_map.txt b/tests/proptest-regressions/storage_api/collections/lazy_map.txt new file mode 100644 index 0000000000..2de7510923 --- /dev/null +++ b/tests/proptest-regressions/storage_api/collections/lazy_map.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 59b8eaaf5d8e03e58b346ef229a2487f68fea488197420f150682f7275ce2b83 # shrinks to (initial_state, transitions) = (AbstractLazyMapState { valid_transitions: [], committed_transitions: [] }, [Insert(11178241982156558453, TestVal { x: 9618691367534591266, y: true }), CommitTx, Update(11178241982156558453, TestVal { x: 2635377083098935189, y: false }), Update(11178241982156558453, TestVal { x: 11485387163946255361, y: false }), Insert(4380901092919801530, TestVal { x: 17235291421018840542, y: false }), Update(11178241982156558453, TestVal { x: 1936190700145956620, y: false }), Update(11178241982156558453, TestVal { x: 6934621224353358508, y: false }), Update(11178241982156558453, TestVal { x: 16175036327810390362, y: true }), Remove(5606457884982633480), Insert(7124206407862523505, TestVal { x: 5513772825695605555, y: true }), CommitTxAndBlock, CommitTx, Insert(13347045100814804679, TestVal { x: 5157295776286367034, y: false }), Update(7124206407862523505, TestVal { x: 1989909525753197955, y: false }), Update(4380901092919801530, TestVal { x: 13085578877588425331, y: false }), Update(7124206407862523505, TestVal { x: 1620781139263176467, y: true }), Insert(5806457332157050619, TestVal { x: 14632354209749334932, y: true }), Remove(1613213961397167063), Update(7124206407862523505, TestVal { x: 3848976302483310370, y: true }), Update(4380901092919801530, TestVal { x: 15281186775251770467, y: false }), Remove(5303306623647571548), Insert(5905425607805327902, TestVal { x: 1274794101048822414, y: false }), Insert(2305446651611241243, TestVal { x: 7872403441503057017, y: true }), Insert(2843165193114615911, TestVal { x: 13698490566286768452, y: false }), Insert(3364298091459048760, TestVal { x: 8891279000465212397, y: true }), CommitTx, Insert(17278527568142155478, TestVal { x: 8166151895050476136, y: false }), Remove(9206713523174765253), Remove(1148985045479283759), Insert(13346103305566843535, TestVal { x: 13148026974798633058, y: true }), Remove(17185699086139524651), CommitTx, Update(7124206407862523505, TestVal { x: 3047872255943216792, y: false }), CommitTxAndBlock, CommitTxAndBlock, Remove(4672009405538026945), Update(5905425607805327902, TestVal { x: 6635343936644805461, y: false }), Insert(14100441716981493843, TestVal { x: 8068697312326956479, y: true }), Insert(8370580326875672309, TestVal { x: 18416630552728813406, y: false }), Update(2305446651611241243, TestVal { x: 3777718192999015176, y: false }), Remove(1532142753559370584), Remove(10097030807802775125), Insert(10080356901530935857, TestVal { x: 17171047520093964037, y: false }), Update(3364298091459048760, TestVal { x: 702372485798608773, y: true }), Insert(5504969092734638033, TestVal { x: 314752460808087203, y: true }), Remove(5486040497128339175), Insert(7884678026881625058, TestVal { x: 4313610278903495077, y: true }), CommitTx, Insert(11228024342874184864, TestVal { x: 428512502841968552, y: false }), Insert(4684666745142518471, TestVal { x: 13122515680485564107, y: true }), Remove(14243063045921130600), Remove(4530767959521683042), Insert(10236349778753659715, TestVal { x: 3138294567956031715, y: true }), Update(2305446651611241243, TestVal { x: 8133236604817109805, y: false }), Update(2843165193114615911, TestVal { x: 12001998927296899868, y: false }), CommitTxAndBlock, CommitTx, CommitTxAndBlock]) diff --git a/tests/src/storage_api/collections/lazy_map.rs b/tests/src/storage_api/collections/lazy_map.rs new file mode 100644 index 0000000000..c7a309ab4d --- /dev/null +++ b/tests/src/storage_api/collections/lazy_map.rs @@ -0,0 +1,608 @@ +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + use std::convert::TryInto; + + use borsh::{BorshDeserialize, BorshSerialize}; + use namada::types::address::{self, Address}; + use namada::types::storage; + use namada_tx_prelude::storage::KeySeg; + use namada_tx_prelude::storage_api::collections::{ + lazy_map, LazyCollection, LazyMap, + }; + use proptest::prelude::*; + use proptest::prop_state_machine; + use proptest::state_machine::{AbstractStateMachine, StateMachineTest}; + use proptest::test_runner::Config; + use test_log::test; + + use crate::tx::tx_host_env; + use crate::vp::vp_host_env; + + prop_state_machine! { + #![proptest_config(Config { + // Instead of the default 256, we only run 5 because otherwise it + // takes too long and it's preferable to crank up the number of + // transitions instead, to allow each case to run for more epochs as + // some issues only manifest once the model progresses further. + // Additionally, more cases will be explored every time this test is + // executed in the CI. + cases: 5, + .. Config::default() + })] + #[test] + fn lazy_map_api_state_machine_test(sequential 1..100 => ConcreteLazyMapState); + } + + /// Type of key used in the map + type TestKey = u64; + + /// Some borsh-serializable type with arbitrary fields to be used inside + /// LazyMap state machine test + #[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, + PartialOrd, + Ord, + )] + struct TestVal { + x: u64, + y: bool, + } + + /// A `StateMachineTest` implemented on this struct manipulates it with + /// `Transition`s, which are also being accumulated into + /// `current_transitions`. It then: + /// + /// - checks its state against an in-memory `std::collections::HashMap` + /// - runs validation and checks that the `LazyMap::Action`s reported from + /// validation match with transitions that were applied + /// + /// Additionally, one of the transitions is to commit a block and/or + /// transaction, during which the currently accumulated state changes are + /// persisted, or promoted from transaction write log to block's write log. + #[derive(Debug)] + struct ConcreteLazyMapState { + /// Address is used to prefix the storage key of the `lazy_map` in + /// order to simulate a transaction and a validity predicate + /// check from changes on the `lazy_map` + address: Address, + /// In the test, we apply the same transitions on the `lazy_map` as on + /// `eager_map` to check that `lazy_map`'s state is consistent with + /// `eager_map`. + eager_map: BTreeMap, + /// Handle to a lazy map + lazy_map: LazyMap, + /// Valid LazyMap changes in the current transaction + current_transitions: Vec, + } + + #[derive(Clone, Debug, Default)] + struct AbstractLazyMapState { + /// Valid LazyMap changes in the current transaction + valid_transitions: Vec, + /// Valid LazyMap changes committed to storage + committed_transitions: Vec, + } + + /// Possible transitions that can modify a [`LazyMap`]. + /// This roughly corresponds to the methods that have `StorageWrite` + /// access and is very similar to [`Action`] + #[derive(Clone, Debug)] + enum Transition { + /// Commit all valid transitions in the current transaction + CommitTx, + /// Commit all valid transitions in the current transaction and also + /// commit the current block + CommitTxAndBlock, + /// Insert a key-val into a [`LazyMap`] + Insert(TestKey, TestVal), + /// Remove a key-val from a [`LazyMap`] + Remove(TestKey), + /// Update a value at key from pre to post state in a + /// [`LazyMap`] + Update(TestKey, TestVal), + } + + impl AbstractStateMachine for AbstractLazyMapState { + type State = Self; + type Transition = Transition; + + fn init_state() -> BoxedStrategy { + Just(Self::default()).boxed() + } + + // Apply a random transition to the state + fn transitions(state: &Self::State) -> BoxedStrategy { + let length = state.len(); + if length == 0 { + prop_oneof![ + 1 => Just(Transition::CommitTx), + 1 => Just(Transition::CommitTxAndBlock), + 3 => (arb_map_key(), arb_map_val()).prop_map(|(key, val)| Transition::Insert(key, val)) + ] + .boxed() + } else { + let keys = state.find_existing_keys(); + let arb_existing_map_key = + || proptest::sample::select(keys.clone()); + prop_oneof![ + 1 => Just(Transition::CommitTx), + 1 => Just(Transition::CommitTxAndBlock), + 3 => (arb_existing_map_key(), arb_map_val()).prop_map(|(key, val)| + Transition::Update(key, val) + ), + 3 => arb_existing_map_key().prop_map(Transition::Remove), + 5 => (arb_map_key().prop_filter("insert on non-existing keys only", move |key| !keys.contains(&key)), arb_map_val()).prop_map(|(key, val)| Transition::Insert(key, val)) + ] + .boxed() + } + } + + fn apply_abstract( + mut state: Self::State, + transition: &Self::Transition, + ) -> Self::State { + match transition { + Transition::CommitTx | Transition::CommitTxAndBlock => { + let valid_actions_to_commit = + std::mem::take(&mut state.valid_transitions); + state + .committed_transitions + .extend(valid_actions_to_commit.into_iter()); + } + _ => state.valid_transitions.push(transition.clone()), + } + state + } + + fn preconditions( + state: &Self::State, + transition: &Self::Transition, + ) -> bool { + let length = state.len(); + // Ensure that the remove or update transitions are not applied + // to an empty state + if length == 0 + && matches!( + transition, + Transition::Remove(_) | Transition::Update(_, _) + ) + { + return false; + } + match transition { + Transition::Update(key, _) | Transition::Remove(key) => { + let keys = state.find_existing_keys(); + // Ensure that the update/remove key is an existing one + keys.contains(key) + } + Transition::Insert(key, _) => { + let keys = state.find_existing_keys(); + // Ensure that the insert key is not an existing one + !keys.contains(key) + } + _ => true, + } + } + } + + impl StateMachineTest for ConcreteLazyMapState { + type Abstract = AbstractLazyMapState; + type ConcreteState = Self; + + fn init_test( + _initial_state: ::State, + ) -> Self::ConcreteState { + // Init transaction env in which we'll be applying the transitions + tx_host_env::init(); + + // The lazy_map's path must be prefixed by the address to be able + // to trigger a validity predicate on it + let address = address::testing::established_address_1(); + tx_host_env::with(|env| env.spawn_accounts([&address])); + let lazy_map_prefix: storage::Key = address.to_db_key().into(); + + Self { + address, + eager_map: BTreeMap::new(), + lazy_map: LazyMap::open( + lazy_map_prefix.push(&"arbitrary".to_string()).unwrap(), + ), + current_transitions: vec![], + } + } + + fn apply_concrete( + mut state: Self::ConcreteState, + transition: ::Transition, + ) -> Self::ConcreteState { + // Apply transitions in transaction env + let ctx = tx_host_env::ctx(); + + // Persist the transitions in the current tx, or clear previous ones + // if we're committing a tx + match &transition { + Transition::CommitTx | Transition::CommitTxAndBlock => { + state.current_transitions = vec![]; + } + _ => { + state.current_transitions.push(transition.clone()); + } + } + + // Transition application on lazy map and post-conditions: + match &transition { + Transition::CommitTx => { + // commit the tx without committing the block + tx_host_env::with(|env| env.write_log.commit_tx()); + } + Transition::CommitTxAndBlock => { + // commit the tx and the block + tx_host_env::commit_tx_and_block(); + } + Transition::Insert(key, value) => { + state.lazy_map.insert(ctx, *key, value.clone()).unwrap(); + + // Post-conditions: + let stored_value = + state.lazy_map.get(ctx, key).unwrap().unwrap(); + assert_eq!( + &stored_value, value, + "the new item must be added to the back" + ); + + state.assert_validation_accepted(); + } + Transition::Remove(key) => { + let removed = + state.lazy_map.remove(ctx, key).unwrap().unwrap(); + + // Post-conditions: + assert_eq!( + &removed, + state.eager_map.get(key).unwrap(), + "removed element matches the value in eager map \ + before it's updated" + ); + + state.assert_validation_accepted(); + } + Transition::Update(key, value) => { + let old_val = + state.lazy_map.get(ctx, key).unwrap().unwrap(); + + state.lazy_map.insert(ctx, *key, value.clone()).unwrap(); + + // Post-conditions: + let new_val = + state.lazy_map.get(ctx, key).unwrap().unwrap(); + assert_eq!( + &old_val, + state.eager_map.get(key).unwrap(), + "old value must match the value at the same key in \ + the eager map before it's updated" + ); + assert_eq!( + &new_val, value, + "new value must match that which was passed into the \ + Transition::Update" + ); + + state.assert_validation_accepted(); + } + } + + // Apply transition in the eager map for comparison + apply_transition_on_eager_map(&mut state.eager_map, &transition); + + // Global post-conditions: + + // All items in eager map must be present in lazy map + for (key, expected_item) in state.eager_map.iter() { + let got = + state.lazy_map.get(ctx, key).unwrap().expect( + "The expected item must be present in lazy map", + ); + assert_eq!(expected_item, &got, "at key {key}"); + } + + // All items in lazy map must be present in eager map + for key_val in state.lazy_map.iter(ctx).unwrap() { + let (key, expected_val) = key_val.unwrap(); + let got = state + .eager_map + .get(&key) + .expect("The expected item must be present in eager map"); + assert_eq!(&expected_val, got, "at key {key}"); + } + + state + } + } + + impl AbstractLazyMapState { + /// Find the length of the map from the applied transitions + fn len(&self) -> u64 { + (map_len_diff_from_transitions(self.committed_transitions.iter()) + + map_len_diff_from_transitions(self.valid_transitions.iter())) + .try_into() + .expect( + "It shouldn't be possible to underflow length from all \ + transactions applied in abstract state", + ) + } + + /// Build an eager map from the committed and current transitions + fn eager_map(&self) -> BTreeMap { + let mut eager_map = BTreeMap::new(); + for transition in &self.committed_transitions { + apply_transition_on_eager_map(&mut eager_map, transition); + } + for transition in &self.valid_transitions { + apply_transition_on_eager_map(&mut eager_map, transition); + } + eager_map + } + + /// Find the keys currently present in the map + fn find_existing_keys(&self) -> Vec { + self.eager_map().keys().cloned().collect() + } + } + + /// Find the difference in length of the map from the applied transitions + fn map_len_diff_from_transitions<'a>( + transitions: impl Iterator, + ) -> i64 { + let mut insert_count: i64 = 0; + let mut remove_count: i64 = 0; + + for trans in transitions { + match trans { + Transition::CommitTx + | Transition::CommitTxAndBlock + | Transition::Update(_, _) => {} + Transition::Insert(_, _) => insert_count += 1, + Transition::Remove(_) => remove_count += 1, + } + } + insert_count - remove_count + } + + impl ConcreteLazyMapState { + fn assert_validation_accepted(&self) { + // Init the VP env from tx env in which we applied the map + // transitions + let tx_env = tx_host_env::take(); + vp_host_env::init_from_tx(self.address.clone(), tx_env, |_| {}); + + // Simulate a validity predicate run using the lazy map's validation + // helpers + let changed_keys = + vp_host_env::with(|env| env.all_touched_storage_keys()); + + let mut validation_builder = None; + + // Push followed by pop is a no-op, in which case we'd still see the + // changed keys for these actions, but they wouldn't affect the + // validation result and they never get persisted, but we'd still + // them as changed key here. To guard against this case, + // we check that `map_len_from_transitions` is not empty. + let map_len_diff = + map_len_diff_from_transitions(self.current_transitions.iter()); + + // To help debug validation issues... + dbg!( + &self.current_transitions, + &changed_keys + .iter() + .map(storage::Key::to_string) + .collect::>() + ); + + for key in &changed_keys { + let is_sub_key = self + .lazy_map + .accumulate( + vp_host_env::ctx(), + &mut validation_builder, + key, + ) + .unwrap(); + + assert!( + is_sub_key, + "We're only modifying the lazy_map's keys here. Key: \ + \"{key}\", map length diff {map_len_diff}" + ); + } + if !changed_keys.is_empty() && map_len_diff != 0 { + assert!( + validation_builder.is_some(), + "If some keys were changed, the builder must get filled in" + ); + let actions = validation_builder.unwrap().build(); + let mut actions_to_check = actions.clone(); + + // Check that every transition has a corresponding action from + // validation. We drop the found actions to check that all + // actions are matched too. + let current_transitions = + normalize_transitions(&self.current_transitions); + for transition in ¤t_transitions { + match transition { + Transition::CommitTx | Transition::CommitTxAndBlock => { + } + Transition::Insert(expected_key, expected_val) => { + for (ix, action) in + actions_to_check.iter().enumerate() + { + if let lazy_map::Action::Insert(key, val) = + action + { + if expected_key == key + && expected_val == val + { + actions_to_check.remove(ix); + break; + } + } + } + } + Transition::Remove(expected_key) => { + for (ix, action) in + actions_to_check.iter().enumerate() + { + if let lazy_map::Action::Remove(key, _val) = + action + { + if expected_key == key { + actions_to_check.remove(ix); + break; + } + } + } + } + Transition::Update(expected_key, value) => { + for (ix, action) in + actions_to_check.iter().enumerate() + { + if let lazy_map::Action::Update { + key, + pre: _, + post, + } = action + { + if expected_key == key && post == value { + actions_to_check.remove(ix); + break; + } + } + } + } + } + } + + assert!( + actions_to_check.is_empty(), + "All the actions reported from validation {actions:#?} \ + should have been matched with SM transitions \ + {current_transitions:#?}, but these actions didn't \ + match: {actions_to_check:#?}", + ) + } + + // Put the tx_env back before checking the result + tx_host_env::set_from_vp_env(vp_host_env::take()); + } + } + + /// Generate an arbitrary `TestKey` + fn arb_map_key() -> impl Strategy { + any::() + } + + /// Generate an arbitrary `TestVal` + fn arb_map_val() -> impl Strategy { + (any::(), any::()).prop_map(|(x, y)| TestVal { x, y }) + } + + /// Apply `Transition` on an eager `Map`. + fn apply_transition_on_eager_map( + map: &mut BTreeMap, + transition: &Transition, + ) { + match transition { + Transition::CommitTx | Transition::CommitTxAndBlock => {} + Transition::Insert(key, value) => { + map.insert(*key, value.clone()); + } + Transition::Remove(key) => { + let _popped = map.remove(key); + } + Transition::Update(key, value) => { + let entry = map.get_mut(key).unwrap(); + *entry = value.clone(); + } + } + } + + /// Normalize transitions: + /// - remove(key) + insert(key, val) -> update(key, val) + /// - insert(key, val) + update(key, new_val) -> insert(key, new_val) + /// - update(key, val) + update(key, new_val) -> update(key, new_val) + /// + /// Note that the normalizable transitions pairs do not have to be directly + /// next to each other, but their order does matter. + fn normalize_transitions(transitions: &[Transition]) -> Vec { + let mut collapsed = vec![]; + 'outer: for transition in transitions { + match transition { + Transition::CommitTx + | Transition::CommitTxAndBlock + | Transition::Remove(_) => collapsed.push(transition.clone()), + Transition::Insert(key, val) => { + for (ix, collapsed_transition) in + collapsed.iter().enumerate() + { + if let Transition::Remove(remove_key) = + collapsed_transition + { + if key == remove_key { + // remove(key) + insert(key, val) -> update(key, + // val) + + // Replace the Remove with an Update instead of + // inserting the Insert + *collapsed.get_mut(ix).unwrap() = + Transition::Update(*key, val.clone()); + continue 'outer; + } + } + } + collapsed.push(transition.clone()); + } + Transition::Update(key, value) => { + for (ix, collapsed_transition) in + collapsed.iter().enumerate() + { + if let Transition::Insert(insert_key, _) = + collapsed_transition + { + if key == insert_key { + // insert(key, val) + update(key, new_val) -> + // insert(key, new_val) + + // Replace the insert with the new update's + // value instead of inserting it + *collapsed.get_mut(ix).unwrap() = + Transition::Insert(*key, value.clone()); + continue 'outer; + } + } else if let Transition::Update(update_key, _) = + collapsed_transition + { + if key == update_key { + // update(key, val) + update(key, new_val) -> + // update(key, new_val) + + // Replace the insert with the new update's + // value instead of inserting it + *collapsed.get_mut(ix).unwrap() = + Transition::Update(*key, value.clone()); + continue 'outer; + } + } + } + collapsed.push(transition.clone()); + } + } + } + collapsed + } +} diff --git a/tests/src/storage_api/collections/mod.rs b/tests/src/storage_api/collections/mod.rs index d874b88e22..fc7c5832ce 100644 --- a/tests/src/storage_api/collections/mod.rs +++ b/tests/src/storage_api/collections/mod.rs @@ -1 +1,2 @@ +mod lazy_map; mod lazy_vec; From 8d2ad09fdcf8bce99dd846824f8fe75bbac0a4fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 19 Sep 2022 16:43:20 +0200 Subject: [PATCH 76/81] WIP: Nested LazyMap validation and testing --- .../storage_api/collections/lazy_map.rs | 401 ++++++---- .../storage_api/collections/lazy_vec.rs | 353 ++++----- .../src/ledger/storage_api/collections/mod.rs | 118 ++- shared/src/types/storage.rs | 5 + .../collections/nested_lazy_map.txt | 7 + tests/src/storage_api/collections/lazy_map.rs | 9 +- tests/src/storage_api/collections/lazy_vec.rs | 5 +- tests/src/storage_api/collections/mod.rs | 1 + .../collections/nested_lazy_map.rs | 723 ++++++++++++++++++ 9 files changed, 1290 insertions(+), 332 deletions(-) create mode 100644 tests/proptest-regressions/storage_api/collections/nested_lazy_map.txt create mode 100644 tests/src/storage_api/collections/nested_lazy_map.rs diff --git a/shared/src/ledger/storage_api/collections/lazy_map.rs b/shared/src/ledger/storage_api/collections/lazy_map.rs index 3801848b8f..686f9d336e 100644 --- a/shared/src/ledger/storage_api/collections/lazy_map.rs +++ b/shared/src/ledger/storage_api/collections/lazy_map.rs @@ -1,9 +1,11 @@ //! Lazy map. +use std::collections::HashMap; +use std::fmt::Debug; +use std::hash::Hash; use std::marker::PhantomData; use borsh::{BorshDeserialize, BorshSerialize}; -use derivative::Derivative; use thiserror::Error; use super::super::Result; @@ -30,14 +32,18 @@ pub const DATA_SUBKEY: &str = "data"; /// This is different from [`super::LazyHashMap`], which hashes borsh encoded /// key. #[derive(Debug)] -pub struct LazyMap { +pub struct LazyMap { key: storage::Key, phantom_k: PhantomData, phantom_v: PhantomData, + phantom_son: PhantomData, } +/// A `LazyMap` with another `LazyCollection` inside it's value `V` +pub type NestedMap = LazyMap; + /// Possible sub-keys of a [`LazyMap`] -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum SubKey { /// Data sub-key, further sub-keyed by its literal map key Data(K), @@ -45,16 +51,14 @@ pub enum SubKey { /// Possible sub-keys of a [`LazyMap`], together with their [`validation::Data`] /// that contains prior and posterior state. -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum SubKeyWithData { /// Data sub-key, further sub-keyed by its literal map key Data(K, Data), } -/// Possible actions that can modify a [`LazyMap`]. This roughly corresponds to -/// the methods that have `StorageWrite` access. -/// TODO: In a nested collection, `V` may be an action inside the nested -/// collection. +/// Possible actions that can modify a simple (not nested) [`LazyMap`]. This +/// roughly corresponds to the methods that have `StorageWrite` access. #[derive(Clone, Debug)] pub enum Action { /// Insert or update a value `V` at key `K` in a [`LazyMap`]. @@ -72,14 +76,23 @@ pub enum Action { }, } -/// TODO: In a nested collection, `V` may be an action inside the nested -/// collection. -#[derive(Debug)] -pub enum Nested { - /// Insert or update a value `V` at key `K` in a [`LazyMap`]. - Insert(K, V), - /// Remove a value `V` at key `K` from a [`LazyMap`]. - Remove(K, V), +/// Possible actions that can modify a nested [`LazyMap`]. +#[derive(Clone, Debug)] +pub enum NestedAction { + /// Nested collection action `A` at key `K` + At(K, A), +} + +/// Possible sub-keys of a nested [`LazyMap`] +#[derive(Clone, Debug)] +pub enum NestedSubKey { + /// Data sub-key + Data { + /// Literal map key + key: K, + /// Sub-key in the nested collection + nested_sub_key: S, + }, } #[allow(missing_docs)] @@ -87,57 +100,228 @@ pub enum Nested { pub enum ValidationError { #[error("Storage error in reading key {0}")] StorageError(storage::Key), - // #[error("Incorrect difference in LazyVec's length")] - // InvalidLenDiff, - // #[error("An empty LazyVec must be deleted from storage")] - // EmptyVecShouldBeDeleted, - // #[error("Push at a wrong index. Got {got}, expected {expected}.")] - // UnexpectedPushIndex { got: Index, expected: Index }, - // #[error("Pop at a wrong index. Got {got}, expected {expected}.")] - // UnexpectedPopIndex { got: Index, expected: Index }, - // #[error( - // "Update (combination of pop and push) at a wrong index. Got {got}, - // \ expected {expected}." - // )] - // UnexpectedUpdateIndex { got: Index, expected: Index }, - // #[error("An index has overflown its representation: {0}")] - // IndexOverflow(>::Error), - // #[error("Unexpected underflow in `{0} - {0}`")] - // UnexpectedUnderflow(Index, Index), #[error("Invalid storage key {0}")] InvalidSubKey(storage::Key), + #[error("Invalid nested storage key {0}")] + InvalidNestedSubKey(storage::Key), } /// [`LazyMap`] validation result pub type ValidationResult = std::result::Result; -/// [`LazyMap`] validation builder from storage changes. The changes can be -/// accumulated with `LazyMap::validate()` and then turned into a list -/// of valid actions on the map with `ValidationBuilder::build()`. -#[derive(Debug, Derivative)] -// https://mcarton.github.io/rust-derivative/latest/Default.html#custom-bound -#[derivative(Default(bound = ""))] -pub struct ValidationBuilder { - /// The accumulator of found changes under the vector - pub changes: Vec>, +impl LazyCollection for LazyMap +where + K: storage::KeySeg + Clone + Hash + Eq + Debug, + V: LazyCollection + Debug, +{ + type Action = NestedAction::Action>; + type SubKey = NestedSubKey::SubKey>; + type SubKeyWithData = + NestedSubKey::SubKeyWithData>; + type Value = ::Value; + + fn open(key: storage::Key) -> Self { + Self { + key, + phantom_k: PhantomData, + phantom_v: PhantomData, + phantom_son: PhantomData, + } + } + + fn is_valid_sub_key( + &self, + key: &storage::Key, + ) -> storage_api::Result> { + let suffix = match key.split_prefix(&self.key) { + None => { + // not matching prefix, irrelevant + return Ok(None); + } + Some(None) => { + // no suffix, invalid + return Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result(); + } + Some(Some(suffix)) => suffix, + }; + + // Match the suffix against expected sub-keys + match &suffix.segments[..2] { + [DbKeySeg::StringSeg(sub_a), DbKeySeg::StringSeg(sub_b)] + if sub_a == DATA_SUBKEY => + { + if let Ok(key_in_kv) = storage::KeySeg::parse(sub_b.clone()) { + let nested = self.at(&key_in_kv).is_valid_sub_key(key)?; + match nested { + Some(nested_sub_key) => Ok(Some(NestedSubKey::Data { + key: key_in_kv, + nested_sub_key, + })), + None => Err(ValidationError::InvalidNestedSubKey( + key.clone(), + )) + .into_storage_result(), + } + } else { + Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result() + } + } + _ => Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result(), + } + } + + fn read_sub_key_data( + env: &ENV, + storage_key: &storage::Key, + sub_key: Self::SubKey, + ) -> storage_api::Result> + where + ENV: for<'a> VpEnv<'a>, + { + let NestedSubKey::Data { + key, + // In here, we just have a nested sub-key without data + nested_sub_key, + } = sub_key; + // Try to read data from the nested collection + let nested_data = ::read_sub_key_data( + env, + storage_key, + nested_sub_key, + )?; + // If found, transform it back into a `NestedSubKey`, but with + // `nested_sub_key` replaced with the one we read + Ok(nested_data.map(|nested_sub_key| NestedSubKey::Data { + key, + nested_sub_key, + })) + } + + fn validate_changed_sub_keys( + keys: Vec, + ) -> storage_api::Result> { + // We have to group the nested sub-keys by the key from this map + let mut grouped_by_key: HashMap< + K, + Vec<::SubKeyWithData>, + > = HashMap::new(); + for NestedSubKey::Data { + key, + nested_sub_key, + } in keys + { + grouped_by_key + .entry(key) + .or_insert_with(Vec::new) + .push(nested_sub_key); + } + + // Recurse for each sub-keys group + let mut actions = vec![]; + for (key, sub_keys) in grouped_by_key { + let nested_actions = + ::validate_changed_sub_keys(sub_keys)?; + actions.extend( + nested_actions + .into_iter() + .map(|action| NestedAction::At(key.clone(), action)), + ); + } + Ok(actions) + } } -impl LazyCollection for LazyMap +impl LazyCollection for LazyMap where - K: storage::KeySeg, + K: storage::KeySeg + Debug, + V: BorshDeserialize + BorshSerialize + 'static + Debug, { + type Action = Action; + type SubKey = SubKey; + type SubKeyWithData = SubKeyWithData; + type Value = V; + /// Create or use an existing map with the given storage `key`. fn open(key: storage::Key) -> Self { Self { key, phantom_k: PhantomData, phantom_v: PhantomData, + phantom_son: PhantomData, } } + + fn is_valid_sub_key( + &self, + key: &storage::Key, + ) -> storage_api::Result> { + let suffix = match key.split_prefix(&self.key) { + None => { + // not matching prefix, irrelevant + return Ok(None); + } + Some(None) => { + // no suffix, invalid + return Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result(); + } + Some(Some(suffix)) => suffix, + }; + + // Match the suffix against expected sub-keys + match &suffix.segments[..] { + [DbKeySeg::StringSeg(sub_a), DbKeySeg::StringSeg(sub_b)] + if sub_a == DATA_SUBKEY => + { + if let Ok(key_in_kv) = storage::KeySeg::parse(sub_b.clone()) { + Ok(Some(SubKey::Data(key_in_kv))) + } else { + Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result() + } + } + _ => Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result(), + } + } + + fn read_sub_key_data( + env: &ENV, + storage_key: &storage::Key, + sub_key: Self::SubKey, + ) -> storage_api::Result> + where + ENV: for<'a> VpEnv<'a>, + { + let SubKey::Data(key) = sub_key; + let data = validation::read_data(env, storage_key)?; + Ok(data.map(|data| SubKeyWithData::Data(key, data))) + } + + fn validate_changed_sub_keys( + keys: Vec, + ) -> storage_api::Result> { + Ok(keys + .into_iter() + .map(|change| { + let SubKeyWithData::Data(key, data) = change; + match data { + Data::Add { post } => Action::Insert(key, post), + Data::Update { pre, post } => { + Action::Update { key, pre, post } + } + Data::Delete { pre } => Action::Remove(key, pre), + } + }) + .collect()) + } } // Generic `LazyMap` methods that require no bounds on values `V` -impl LazyMap +impl LazyMap where K: storage::KeySeg, { @@ -162,10 +346,10 @@ where } // `LazyMap` methods with nested `LazyCollection`s `V` -impl LazyMap +impl LazyMap where - K: storage::KeySeg, - V: LazyCollection, + K: storage::KeySeg + Clone + Hash + Eq + Debug, + V: LazyCollection + Debug, { /// Get a nested collection at given key `key`. If there is no nested /// collection at the given key, a new empty one will be provided. The @@ -173,10 +357,39 @@ where pub fn at(&self, key: &K) -> V { V::open(self.get_data_key(key)) } + + /// An iterator visiting all key-value elements, where the values are from + /// the inner-most collection. The iterator element type is `Result<_>`, + /// because iterator's call to `next` may fail with e.g. out of gas or + /// data decoding error. + /// + /// Note that this function shouldn't be used in transactions and VPs code + /// on unbounded maps to avoid gas usage increasing with the length of the + /// map. + pub fn iter<'iter>( + &'iter self, + storage: &'iter impl StorageRead<'iter>, + ) -> Result< + impl Iterator< + Item = Result<( + ::SubKey, + ::Value, + )>, + > + 'iter, + > { + let iter = storage_api::iter_prefix(storage, &self.get_data_prefix())?; + Ok(iter.map(|key_val_res| { + let (key, val) = key_val_res?; + let sub_key = LazyCollection::is_valid_sub_key(self, &key)? + .ok_or(ReadError::UnexpectedlyEmptyStorageKey) + .into_storage_result()?; + Ok((sub_key, val)) + })) + } } // `LazyMap` methods with borsh encoded values `V` -impl LazyMap +impl LazyMap where K: storage::KeySeg, V: BorshDeserialize + BorshSerialize + 'static, @@ -299,96 +512,6 @@ where ) -> Result<()> { storage.write(storage_key, val) } - - /// Check if the given storage key is a valid LazyMap sub-key and if so - /// return which one - pub fn is_valid_sub_key( - &self, - key: &storage::Key, - ) -> storage_api::Result>> { - let suffix = match key.split_prefix(&self.key) { - None => { - // not matching prefix, irrelevant - return Ok(None); - } - Some(None) => { - // no suffix, invalid - return Err(ValidationError::InvalidSubKey(key.clone())) - .into_storage_result(); - } - Some(Some(suffix)) => suffix, - }; - - // Match the suffix against expected sub-keys - match &suffix.segments[..] { - [DbKeySeg::StringSeg(sub_a), DbKeySeg::StringSeg(sub_b)] - if sub_a == DATA_SUBKEY => - { - if let Ok(key_in_kv) = storage::KeySeg::parse(sub_b.clone()) { - Ok(Some(SubKey::Data(key_in_kv))) - } else { - Err(ValidationError::InvalidSubKey(key.clone())) - .into_storage_result() - } - } - _ => Err(ValidationError::InvalidSubKey(key.clone())) - .into_storage_result(), - } - } - - /// Accumulate storage changes inside a [`ValidationBuilder`]. This is - /// typically done by the validity predicate while looping through the - /// changed keys. If the resulting `builder` is not `None`, one must - /// call `fn build()` on it to get the validation result. - /// This function will return `Ok(true)` if the storage key is a valid - /// sub-key of this collection, `Ok(false)` if the storage key doesn't match - /// the prefix of this collection, or fail with - /// [`ValidationError::InvalidSubKey`] if the prefix matches this - /// collection, but the key itself is not recognized. - pub fn accumulate( - &self, - env: &ENV, - builder: &mut Option>, - key_changed: &storage::Key, - ) -> storage_api::Result - where - ENV: for<'a> VpEnv<'a>, - { - if let Some(sub) = self.is_valid_sub_key(key_changed)? { - let SubKey::Data(key) = sub; - let data = validation::read_data(env, key_changed)?; - let change = data.map(|data| SubKeyWithData::Data(key, data)); - if let Some(change) = change { - let builder = - builder.get_or_insert(ValidationBuilder::default()); - builder.changes.push(change); - } - return Ok(true); - } - Ok(false) - } -} - -impl ValidationBuilder -where - K: storage::KeySeg + Ord + Clone, -{ - /// Build a list of actions from storage changes. - pub fn build(self) -> Vec> { - self.changes - .into_iter() - .map(|change| { - let SubKeyWithData::Data(key, data) = change; - match data { - Data::Add { post } => Action::Insert(key, post), - Data::Update { pre, post } => { - Action::Update { key, pre, post } - } - Data::Delete { pre } => Action::Remove(key, pre), - } - }) - .collect() - } } #[cfg(test)] diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index d86f33ec08..fd61bef804 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -1,10 +1,10 @@ //! Lazy dynamically-sized vector. use std::collections::BTreeSet; +use std::fmt::Debug; use std::marker::PhantomData; use borsh::{BorshDeserialize, BorshSerialize}; -use derivative::Derivative; use thiserror::Error; use super::super::Result; @@ -110,18 +110,15 @@ pub enum UpdateError { /// [`LazyVec`] validation result pub type ValidationResult = std::result::Result; -/// [`LazyVec`] validation builder from storage changes. The changes can be -/// accumulated with `LazyVec::validate()` and then turned into a list -/// of valid actions on the vector with `ValidationBuilder::build()`. -#[derive(Debug, Derivative)] -// https://mcarton.github.io/rust-derivative/latest/Default.html#custom-bound -#[derivative(Default(bound = ""))] -pub struct ValidationBuilder { - /// The accumulator of found changes under the vector - pub changes: Vec>, -} +impl LazyCollection for LazyVec +where + T: BorshSerialize + BorshDeserialize + 'static + Debug, +{ + type Action = Action; + type SubKey = SubKey; + type SubKeyWithData = SubKeyWithData; + type Value = T; -impl LazyCollection for LazyVec { /// Create or use an existing vector with the given storage `key`. fn open(key: storage::Key) -> Self { Self { @@ -129,131 +126,10 @@ impl LazyCollection for LazyVec { phantom: PhantomData, } } -} - -// Generic `LazyVec` methods that require no bounds on values `T` -impl LazyVec { - /// Reads the number of elements in the vector. - #[allow(clippy::len_without_is_empty)] - pub fn len(&self, storage: &S) -> Result - where - S: for<'iter> StorageRead<'iter>, - { - let len = storage.read(&self.get_len_key())?; - Ok(len.unwrap_or_default()) - } - - /// Returns `true` if the vector contains no elements. - pub fn is_empty(&self, storage: &S) -> Result - where - S: for<'iter> StorageRead<'iter>, - { - Ok(self.len(storage)? == 0) - } - - /// Get the prefix of set's elements storage - fn get_data_prefix(&self) -> storage::Key { - self.key.push(&DATA_SUBKEY.to_owned()).unwrap() - } - - /// Get the sub-key of vector's elements storage - fn get_data_key(&self, index: Index) -> storage::Key { - self.get_data_prefix().push(&index).unwrap() - } - - /// Get the sub-key of vector's length storage - fn get_len_key(&self) -> storage::Key { - self.key.push(&LEN_SUBKEY.to_owned()).unwrap() - } -} - -// `LazyVec` methods with borsh encoded values `T` -impl LazyVec -where - T: BorshSerialize + BorshDeserialize + 'static, -{ - /// Appends an element to the back of a collection. - pub fn push(&self, storage: &mut S, val: T) -> Result<()> - where - S: StorageWrite + for<'iter> StorageRead<'iter>, - { - let len = self.len(storage)?; - let data_key = self.get_data_key(len); - storage.write(&data_key, val)?; - storage.write(&self.get_len_key(), len + 1) - } - - /// Removes the last element from a vector and returns it, or `Ok(None)` if - /// it is empty. - /// - /// Note that an empty vector is completely removed from storage. - pub fn pop(&self, storage: &mut S) -> Result> - where - S: StorageWrite + for<'iter> StorageRead<'iter>, - { - let len = self.len(storage)?; - if len == 0 { - Ok(None) - } else { - let index = len - 1; - let data_key = self.get_data_key(index); - if len == 1 { - storage.delete(&self.get_len_key())?; - } else { - storage.write(&self.get_len_key(), index)?; - } - let popped_val = storage.read(&data_key)?; - storage.delete(&data_key)?; - Ok(popped_val) - } - } - - /// Update an element at the given index. - /// - /// The index must be smaller than the length of the vector, otherwise this - /// will fail with `UpdateError::InvalidIndex`. - pub fn update(&self, storage: &mut S, index: Index, val: T) -> Result<()> - where - S: StorageWrite + for<'iter> StorageRead<'iter>, - { - let len = self.len(storage)?; - if index >= len { - return Err(UpdateError::InvalidIndex { index, len }) - .into_storage_result(); - } - let data_key = self.get_data_key(index); - storage.write(&data_key, val) - } - - /// Read an element at the index or `Ok(None)` if out of bounds. - pub fn get(&self, storage: &S, index: Index) -> Result> - where - S: for<'iter> StorageRead<'iter>, - { - storage.read(&self.get_data_key(index)) - } - - /// An iterator visiting all elements. The iterator element type is - /// `Result`, because iterator's call to `next` may fail with e.g. out of - /// gas or data decoding error. - /// - /// Note that this function shouldn't be used in transactions and VPs code - /// on unbounded sets to avoid gas usage increasing with the length of the - /// set. - pub fn iter<'iter>( - &self, - storage: &'iter impl StorageRead<'iter>, - ) -> Result> + 'iter> { - let iter = storage_api::iter_prefix(storage, &self.get_data_prefix())?; - Ok(iter.map(|key_val_res| { - let (_key, val) = key_val_res?; - Ok(val) - })) - } /// Check if the given storage key is a valid LazyVec sub-key and if so /// return which one - pub fn is_valid_sub_key( + fn is_valid_sub_key( &self, key: &storage::Key, ) -> storage_api::Result> { @@ -290,50 +166,27 @@ where } } - /// Accumulate storage changes inside a [`ValidationBuilder`]. This is - /// typically done by the validity predicate while looping through the - /// changed keys. If the resulting `builder` is not `None`, one must - /// call `fn build()` on it to get the validation result. - /// This function will return `Ok(true)` if the storage key is a valid - /// sub-key of this collection, `Ok(false)` if the storage key doesn't match - /// the prefix of this collection, or fail with - /// [`ValidationError::InvalidSubKey`] if the prefix matches this - /// collection, but the key itself is not recognized. - pub fn accumulate( - &self, + fn read_sub_key_data( env: &ENV, - builder: &mut Option>, - key_changed: &storage::Key, - ) -> storage_api::Result + storage_key: &storage::Key, + sub_key: Self::SubKey, + ) -> storage_api::Result> where ENV: for<'a> VpEnv<'a>, { - if let Some(sub) = self.is_valid_sub_key(key_changed)? { - let change = match sub { - SubKey::Len => { - let data = validation::read_data(env, key_changed)?; - data.map(SubKeyWithData::Len) - } - SubKey::Data(index) => { - let data = validation::read_data(env, key_changed)?; - data.map(|data| SubKeyWithData::Data(index, data)) - } - }; - if let Some(change) = change { - let builder = - builder.get_or_insert(ValidationBuilder::default()); - builder.changes.push(change); + let change = match sub_key { + SubKey::Len => { + let data = validation::read_data(env, storage_key)?; + data.map(SubKeyWithData::Len) } - return Ok(true); - } - Ok(false) + SubKey::Data(index) => { + let data = validation::read_data(env, storage_key)?; + data.map(|data| SubKeyWithData::Data(index, data)) + } + }; + Ok(change) } -} -impl ValidationBuilder { - /// Validate storage changes and if valid, build from them a list of - /// actions. - /// /// The validation rules for a [`LazyVec`] are: /// - A difference in the vector's length must correspond to the /// difference in how many elements were pushed versus how many elements @@ -342,7 +195,9 @@ impl ValidationBuilder { /// - In addition, we check that indices of any changes are within an /// expected range (i.e. the vectors indices should always be /// monotonically increasing from zero) - pub fn build(self) -> ValidationResult>> { + fn validate_changed_sub_keys( + keys: Vec, + ) -> storage_api::Result> { let mut actions = vec![]; // We need to accumulate some values for what's changed @@ -353,14 +208,15 @@ impl ValidationBuilder { let mut updated = BTreeSet::::default(); let mut deleted = BTreeSet::::default(); - for change in self.changes { - match change { + for key in keys { + match key { SubKeyWithData::Len(data) => match data { Data::Add { post } => { if post == 0 { return Err( ValidationError::EmptyVecShouldBeDeleted, - ); + ) + .into_storage_result(); } post_gt_pre = true; len_diff = post; @@ -369,7 +225,8 @@ impl ValidationBuilder { if post == 0 { return Err( ValidationError::EmptyVecShouldBeDeleted, - ); + ) + .into_storage_result(); } if post > pre { post_gt_pre = true; @@ -403,11 +260,13 @@ impl ValidationBuilder { let added_len: u64 = added .len() .try_into() - .map_err(ValidationError::IndexOverflow)?; + .map_err(ValidationError::IndexOverflow) + .into_storage_result()?; let deleted_len: u64 = deleted .len() .try_into() - .map_err(ValidationError::IndexOverflow)?; + .map_err(ValidationError::IndexOverflow) + .into_storage_result()?; if len_diff != 0 && !(if post_gt_pre { @@ -416,7 +275,7 @@ impl ValidationBuilder { added_len + len_diff == deleted_len }) { - return Err(ValidationError::InvalidLenDiff); + return Err(ValidationError::InvalidLenDiff).into_storage_result(); } let mut last_added = Option::None; @@ -430,7 +289,8 @@ impl ValidationBuilder { return Err(ValidationError::UnexpectedPushIndex { got: index, expected, - }); + }) + .into_storage_result(); } } else if index != len_pre { // The first addition must be at the pre length value. @@ -440,7 +300,8 @@ impl ValidationBuilder { return Err(ValidationError::UnexpectedPushIndex { got: index, expected: len_pre, - }); + }) + .into_storage_result(); } last_added = Some(index); } @@ -456,7 +317,8 @@ impl ValidationBuilder { return Err(ValidationError::UnexpectedPopIndex { got: index, expected, - }); + }) + .into_storage_result(); } } last_deleted = Some(index); @@ -469,7 +331,8 @@ impl ValidationBuilder { return Err(ValidationError::UnexpectedPopIndex { got: index, expected: len_pre, - }); + }) + .into_storage_result(); } } } @@ -482,7 +345,8 @@ impl ValidationBuilder { return Err(ValidationError::UnexpectedUpdateIndex { got: index, max, - }); + }) + .into_storage_result(); } } @@ -490,6 +354,127 @@ impl ValidationBuilder { } } +// Generic `LazyVec` methods that require no bounds on values `T` +impl LazyVec { + /// Reads the number of elements in the vector. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self, storage: &S) -> Result + where + S: for<'iter> StorageRead<'iter>, + { + let len = storage.read(&self.get_len_key())?; + Ok(len.unwrap_or_default()) + } + + /// Returns `true` if the vector contains no elements. + pub fn is_empty(&self, storage: &S) -> Result + where + S: for<'iter> StorageRead<'iter>, + { + Ok(self.len(storage)? == 0) + } + + /// Get the prefix of set's elements storage + fn get_data_prefix(&self) -> storage::Key { + self.key.push(&DATA_SUBKEY.to_owned()).unwrap() + } + + /// Get the sub-key of vector's elements storage + fn get_data_key(&self, index: Index) -> storage::Key { + self.get_data_prefix().push(&index).unwrap() + } + + /// Get the sub-key of vector's length storage + fn get_len_key(&self) -> storage::Key { + self.key.push(&LEN_SUBKEY.to_owned()).unwrap() + } +} + +// `LazyVec` methods with borsh encoded values `T` +impl LazyVec +where + T: BorshSerialize + BorshDeserialize + 'static, +{ + /// Appends an element to the back of a collection. + pub fn push(&self, storage: &mut S, val: T) -> Result<()> + where + S: StorageWrite + for<'iter> StorageRead<'iter>, + { + let len = self.len(storage)?; + let data_key = self.get_data_key(len); + storage.write(&data_key, val)?; + storage.write(&self.get_len_key(), len + 1) + } + + /// Removes the last element from a vector and returns it, or `Ok(None)` if + /// it is empty. + /// + /// Note that an empty vector is completely removed from storage. + pub fn pop(&self, storage: &mut S) -> Result> + where + S: StorageWrite + for<'iter> StorageRead<'iter>, + { + let len = self.len(storage)?; + if len == 0 { + Ok(None) + } else { + let index = len - 1; + let data_key = self.get_data_key(index); + if len == 1 { + storage.delete(&self.get_len_key())?; + } else { + storage.write(&self.get_len_key(), index)?; + } + let popped_val = storage.read(&data_key)?; + storage.delete(&data_key)?; + Ok(popped_val) + } + } + + /// Update an element at the given index. + /// + /// The index must be smaller than the length of the vector, otherwise this + /// will fail with `UpdateError::InvalidIndex`. + pub fn update(&self, storage: &mut S, index: Index, val: T) -> Result<()> + where + S: StorageWrite + for<'iter> StorageRead<'iter>, + { + let len = self.len(storage)?; + if index >= len { + return Err(UpdateError::InvalidIndex { index, len }) + .into_storage_result(); + } + let data_key = self.get_data_key(index); + storage.write(&data_key, val) + } + + /// Read an element at the index or `Ok(None)` if out of bounds. + pub fn get(&self, storage: &S, index: Index) -> Result> + where + S: for<'iter> StorageRead<'iter>, + { + storage.read(&self.get_data_key(index)) + } + + /// An iterator visiting all elements. The iterator element type is + /// `Result`, because iterator's call to `next` may fail with e.g. out of + /// gas or data decoding error. + /// + /// Note that this function shouldn't be used in transactions and VPs code + /// on unbounded sets to avoid gas usage increasing with the length of the + /// set. + pub fn iter<'iter>( + &self, + storage: &'iter impl StorageRead<'iter>, + ) -> Result> + 'iter> { + let iter = storage_api::iter_prefix(storage, &self.get_data_prefix())?; + Ok(iter.map(|key_val_res| { + let (_key, val) = key_val_res?; + Ok(val) + })) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/shared/src/ledger/storage_api/collections/mod.rs b/shared/src/ledger/storage_api/collections/mod.rs index b3e1b4af0c..b77b207c7f 100644 --- a/shared/src/ledger/storage_api/collections/mod.rs +++ b/shared/src/ledger/storage_api/collections/mod.rs @@ -7,21 +7,27 @@ //! just receive the storage sub-keys that have experienced changes without //! having to check any of the unchanged elements. +use std::fmt::Debug; + +use borsh::BorshDeserialize; +use derivative::Derivative; use thiserror::Error; mod hasher; -pub mod lazy_hashmap; -pub mod lazy_hashset; +// pub mod lazy_hashmap; +// pub mod lazy_hashset; pub mod lazy_map; -pub mod lazy_set; +// pub mod lazy_set; pub mod lazy_vec; -pub use lazy_hashmap::LazyHashMap; -pub use lazy_hashset::LazyHashSet; +// pub use lazy_hashmap::LazyHashMap; +// pub use lazy_hashset::LazyHashSet; pub use lazy_map::LazyMap; -pub use lazy_set::LazySet; +// pub use lazy_set::LazySet; pub use lazy_vec::LazyVec; +use crate::ledger::storage_api; +use crate::ledger::vp_env::VpEnv; use crate::types::storage; #[allow(missing_docs)] @@ -31,6 +37,14 @@ pub enum ReadError { UnexpectedlyEmptyStorageKey, } +/// Simple lazy collection with borsh deserializable elements +#[derive(Debug)] +pub struct Simple; + +/// Lazy collection with a nested lazy collection +#[derive(Debug)] +pub struct Nested; + /// A lazy collection of storage values is a handler with some storage prefix /// that is given to its `fn new()`. The values are typically nested under this /// prefix and they can be changed individually (e.g. without reading in the @@ -40,6 +54,98 @@ pub enum ReadError { /// /// An empty collection must be deleted from storage. pub trait LazyCollection { + /// Actions on the collection determined from changed storage keys by + /// `Self::validate` + type Action; + + /// Possible sub-keys in the collection + type SubKey: Debug; + + /// Possible sub-keys together with the data read from storage + type SubKeyWithData: Debug; + + /// A type of a value in the inner-most collection + type Value: BorshDeserialize; + /// Create or use an existing vector with the given storage `key`. fn open(key: storage::Key) -> Self; + + /// Check if the given storage key is a valid LazyVec sub-key and if so + /// return which one. Returns: + /// - `Ok(Some(_))` if it's a valid sub-key + /// - `Ok(None)` if it's not a sub-key + /// - `Err(_)` if it's an invalid sub-key + fn is_valid_sub_key( + &self, + key: &storage::Key, + ) -> storage_api::Result>; + + /// Try to read and decode the data for each change storage key in prior and + /// posterior state. If there is no value in neither prior or posterior + /// state (which is a possible state when transaction e.g. writes and then + /// deletes one storage key, but it is treated as a no-op as it doesn't + /// affect result of validation), returns `Ok(None)`. + fn read_sub_key_data( + env: &ENV, + storage_key: &storage::Key, + sub_key: Self::SubKey, + ) -> storage_api::Result> + where + ENV: for<'a> VpEnv<'a>; + + /// Validate changed sub-keys associated with their data and return back + /// a vector of `Self::Action`s, if the changes are valid + fn validate_changed_sub_keys( + keys: Vec, + ) -> storage_api::Result>; + + /// Accumulate storage changes inside a `ValidationBuilder`. This is + /// typically done by the validity predicate while looping through the + /// changed keys. If the resulting `builder` is not `None`, one must + /// call `fn build()` on it to get the validation result. + /// This function will return `Ok(true)` if the storage key is a valid + /// sub-key of this collection, `Ok(false)` if the storage key doesn't match + /// the prefix of this collection, or fail with + /// [`ValidationError::InvalidSubKey`] if the prefix matches this + /// collection, but the key itself is not recognized. + fn accumulate( + &self, + env: &ENV, + builder: &mut Option>, + key_changed: &storage::Key, + ) -> storage_api::Result + where + ENV: for<'a> VpEnv<'a>, + { + if let Some(sub) = self.is_valid_sub_key(key_changed)? { + let change = Self::read_sub_key_data(env, key_changed, sub)?; + if let Some(change) = change { + let builder = + builder.get_or_insert(ValidationBuilder::default()); + builder.changes.push(change); + } + return Ok(true); + } + Ok(false) + } + + /// Execute validation on the validation builder, to be called when + /// `accumulate` instantiates the builder to `Some(_)`, after all the + /// changes storage keys have been processed. + fn validate( + builder: ValidationBuilder, + ) -> storage_api::Result> { + Self::validate_changed_sub_keys(builder.changes) + } +} + +/// Validation builder from storage changes. The changes can +/// be accumulated with `LazyCollection::accumulate()` and then turned into a +/// list of valid actions on the collection with `LazyCollection::validate()`. +#[derive(Debug, Derivative)] +// https://mcarton.github.io/rust-derivative/latest/Default.html#custom-bound +#[derivative(Default(bound = ""))] +pub struct ValidationBuilder { + /// The accumulator of found changes under the vector + pub changes: Vec, } diff --git a/shared/src/types/storage.rs b/shared/src/types/storage.rs index 1c3f6b1313..c9f87908fe 100644 --- a/shared/src/types/storage.rs +++ b/shared/src/types/storage.rs @@ -285,6 +285,11 @@ impl Key { self.len() == 0 } + /// Returns the first segment of the key, or `None` if it is empty. + pub fn first(&self) -> Option<&DbKeySeg> { + self.segments.first() + } + /// Returns the last segment of the key, or `None` if it is empty. pub fn last(&self) -> Option<&DbKeySeg> { self.segments.last() diff --git a/tests/proptest-regressions/storage_api/collections/nested_lazy_map.txt b/tests/proptest-regressions/storage_api/collections/nested_lazy_map.txt new file mode 100644 index 0000000000..d587a9680e --- /dev/null +++ b/tests/proptest-regressions/storage_api/collections/nested_lazy_map.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc b5ce7502439712f95a4b50de0d5455e0a6788cc95dbd535e749d5717da0ee8e1 # shrinks to (initial_state, transitions) = (AbstractLazyMapState { valid_transitions: [], committed_transitions: [] }, [Insert((22253647846329582, -2060910714, -85), TestVal { x: 16862967849328560500, y: true })]) diff --git a/tests/src/storage_api/collections/lazy_map.rs b/tests/src/storage_api/collections/lazy_map.rs index c7a309ab4d..afff09bbf1 100644 --- a/tests/src/storage_api/collections/lazy_map.rs +++ b/tests/src/storage_api/collections/lazy_map.rs @@ -137,7 +137,9 @@ mod tests { Transition::Update(key, val) ), 3 => arb_existing_map_key().prop_map(Transition::Remove), - 5 => (arb_map_key().prop_filter("insert on non-existing keys only", move |key| !keys.contains(&key)), arb_map_val()).prop_map(|(key, val)| Transition::Insert(key, val)) + 5 => (arb_map_key().prop_filter("insert on non-existing keys only", + move |key| !keys.contains(key)), arb_map_val()) + .prop_map(|(key, val)| Transition::Insert(key, val)) ] .boxed() } @@ -426,7 +428,10 @@ mod tests { validation_builder.is_some(), "If some keys were changed, the builder must get filled in" ); - let actions = validation_builder.unwrap().build(); + let actions = LazyMap::::validate( + validation_builder.unwrap(), + ) + .unwrap(); let mut actions_to_check = actions.clone(); // Check that every transition has a corresponding action from diff --git a/tests/src/storage_api/collections/lazy_vec.rs b/tests/src/storage_api/collections/lazy_vec.rs index 20ee80592d..65e08b4ca7 100644 --- a/tests/src/storage_api/collections/lazy_vec.rs +++ b/tests/src/storage_api/collections/lazy_vec.rs @@ -418,7 +418,10 @@ mod tests { validation_builder.is_some(), "If some keys were changed, the builder must get filled in" ); - let actions = validation_builder.unwrap().build().expect( + let actions = LazyVec::::validate( + validation_builder.unwrap(), + ) + .expect( "With valid transitions only, validation should always \ pass", ); diff --git a/tests/src/storage_api/collections/mod.rs b/tests/src/storage_api/collections/mod.rs index fc7c5832ce..f39b880c09 100644 --- a/tests/src/storage_api/collections/mod.rs +++ b/tests/src/storage_api/collections/mod.rs @@ -1,2 +1,3 @@ mod lazy_map; mod lazy_vec; +mod nested_lazy_map; diff --git a/tests/src/storage_api/collections/nested_lazy_map.rs b/tests/src/storage_api/collections/nested_lazy_map.rs new file mode 100644 index 0000000000..80b066c18f --- /dev/null +++ b/tests/src/storage_api/collections/nested_lazy_map.rs @@ -0,0 +1,723 @@ +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + use std::convert::TryInto; + + use borsh::{BorshDeserialize, BorshSerialize}; + use namada::types::address::{self, Address}; + use namada::types::storage; + use namada_tx_prelude::storage::KeySeg; + use namada_tx_prelude::storage_api::collections::lazy_map::{ + NestedMap, NestedSubKey, SubKey, + }; + use namada_tx_prelude::storage_api::collections::{ + lazy_map, LazyCollection, LazyMap, + }; + use proptest::prelude::*; + use proptest::prop_state_machine; + use proptest::state_machine::{AbstractStateMachine, StateMachineTest}; + use proptest::test_runner::Config; + use test_log::test; + + use crate::tx::tx_host_env; + use crate::vp::vp_host_env; + + prop_state_machine! { + #![proptest_config(Config { + // Instead of the default 256, we only run 5 because otherwise it + // takes too long and it's preferable to crank up the number of + // transitions instead, to allow each case to run for more epochs as + // some issues only manifest once the model progresses further. + // Additionally, more cases will be explored every time this test is + // executed in the CI. + cases: 5, + .. Config::default() + })] + #[test] + fn nested_lazy_map_api_state_machine_test(sequential 1..100 => ConcreteLazyMapState); + } + + /// Some borsh-serializable type with arbitrary fields to be used inside + /// LazyMap state machine test + #[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, + PartialOrd, + Ord, + )] + struct TestVal { + x: u64, + y: bool, + } + + type KeyOuter = u64; + type KeyMiddle = i32; + type KeyInner = i8; + + type NestedTestMap = + NestedMap>>; + + type NestedEagerMap = + BTreeMap>>; + + /// A `StateMachineTest` implemented on this struct manipulates it with + /// `Transition`s, which are also being accumulated into + /// `current_transitions`. It then: + /// + /// - checks its state against an in-memory `std::collections::HashMap` + /// - runs validation and checks that the `LazyMap::Action`s reported from + /// validation match with transitions that were applied + /// + /// Additionally, one of the transitions is to commit a block and/or + /// transaction, during which the currently accumulated state changes are + /// persisted, or promoted from transaction write log to block's write log. + #[derive(Debug)] + struct ConcreteLazyMapState { + /// Address is used to prefix the storage key of the `lazy_map` in + /// order to simulate a transaction and a validity predicate + /// check from changes on the `lazy_map` + address: Address, + /// In the test, we apply the same transitions on the `lazy_map` as on + /// `eager_map` to check that `lazy_map`'s state is consistent with + /// `eager_map`. + eager_map: NestedEagerMap, + /// Handle to a lazy map with nested lazy collections + lazy_map: NestedTestMap, + /// Valid LazyMap changes in the current transaction + current_transitions: Vec, + } + + #[derive(Clone, Debug, Default)] + struct AbstractLazyMapState { + /// Valid LazyMap changes in the current transaction + valid_transitions: Vec, + /// Valid LazyMap changes committed to storage + committed_transitions: Vec, + } + + /// Possible transitions that can modify a [`NestedTestMap`]. + /// This roughly corresponds to the methods that have `StorageWrite` + /// access and is very similar to [`Action`] + #[derive(Clone, Debug)] + enum Transition { + /// Commit all valid transitions in the current transaction + CommitTx, + /// Commit all valid transitions in the current transaction and also + /// commit the current block + CommitTxAndBlock, + /// Insert a key-val into a [`LazyMap`] + Insert(Key, TestVal), + /// Remove a key-val from a [`LazyMap`] + Remove(Key), + /// Update a value at key from pre to post state in a + /// [`LazyMap`] + Update(Key, TestVal), + } + + /// A key for transition + type Key = (KeyOuter, KeyMiddle, KeyInner); + + impl AbstractStateMachine for AbstractLazyMapState { + type State = Self; + type Transition = Transition; + + fn init_state() -> BoxedStrategy { + Just(Self::default()).boxed() + } + + // Apply a random transition to the state + fn transitions(state: &Self::State) -> BoxedStrategy { + let length = state.len(); + if length == 0 { + prop_oneof![ + 1 => Just(Transition::CommitTx), + 1 => Just(Transition::CommitTxAndBlock), + 3 => (arb_map_key(), arb_map_val()).prop_map(|(key, val)| Transition::Insert(key, val)) + ] + .boxed() + } else { + let keys = state.find_existing_keys(); + let arb_existing_map_key = + || proptest::sample::select(keys.clone()); + prop_oneof![ + 1 => Just(Transition::CommitTx), + 1 => Just(Transition::CommitTxAndBlock), + 3 => (arb_existing_map_key(), arb_map_val()).prop_map(|(key, val)| + Transition::Update(key, val)), + 3 => arb_existing_map_key().prop_map(Transition::Remove), + 5 => (arb_map_key().prop_filter( + "insert on non-existing keys only", + move |key| !keys.contains(key)), arb_map_val()) + .prop_map(|(key, val)| Transition::Insert(key, val)) + ] + .boxed() + } + } + + fn apply_abstract( + mut state: Self::State, + transition: &Self::Transition, + ) -> Self::State { + match transition { + Transition::CommitTx | Transition::CommitTxAndBlock => { + let valid_actions_to_commit = + std::mem::take(&mut state.valid_transitions); + state + .committed_transitions + .extend(valid_actions_to_commit.into_iter()); + } + _ => state.valid_transitions.push(transition.clone()), + } + state + } + + fn preconditions( + state: &Self::State, + transition: &Self::Transition, + ) -> bool { + let length = state.len(); + // Ensure that the remove or update transitions are not applied + // to an empty state + if length == 0 + && matches!( + transition, + Transition::Remove(_) | Transition::Update(_, _) + ) + { + return false; + } + match transition { + Transition::Update(key, _) | Transition::Remove(key) => { + let keys = state.find_existing_keys(); + // Ensure that the update/remove key is an existing one + keys.contains(key) + } + Transition::Insert(key, _) => { + let keys = state.find_existing_keys(); + // Ensure that the insert key is not an existing one + !keys.contains(key) + } + _ => true, + } + } + } + + impl StateMachineTest for ConcreteLazyMapState { + type Abstract = AbstractLazyMapState; + type ConcreteState = Self; + + fn init_test( + _initial_state: ::State, + ) -> Self::ConcreteState { + // Init transaction env in which we'll be applying the transitions + tx_host_env::init(); + + // The lazy_map's path must be prefixed by the address to be able + // to trigger a validity predicate on it + let address = address::testing::established_address_1(); + tx_host_env::with(|env| env.spawn_accounts([&address])); + let lazy_map_prefix: storage::Key = address.to_db_key().into(); + + Self { + address, + eager_map: BTreeMap::new(), + lazy_map: NestedTestMap::open( + lazy_map_prefix.push(&"arbitrary".to_string()).unwrap(), + ), + current_transitions: vec![], + } + } + + fn apply_concrete( + mut state: Self::ConcreteState, + transition: ::Transition, + ) -> Self::ConcreteState { + // Apply transitions in transaction env + let ctx = tx_host_env::ctx(); + + // Persist the transitions in the current tx, or clear previous ones + // if we're committing a tx + match &transition { + Transition::CommitTx | Transition::CommitTxAndBlock => { + state.current_transitions = vec![]; + } + _ => { + state.current_transitions.push(transition.clone()); + } + } + + // Transition application on lazy map and post-conditions: + match &transition { + Transition::CommitTx => { + // commit the tx without committing the block + tx_host_env::with(|env| env.write_log.commit_tx()); + } + Transition::CommitTxAndBlock => { + // commit the tx and the block + tx_host_env::commit_tx_and_block(); + } + Transition::Insert( + (key_outer, key_middle, key_inner), + value, + ) => { + let inner = state.lazy_map.at(key_outer).at(key_middle); + + inner.insert(ctx, *key_inner, value.clone()).unwrap(); + + // Post-conditions: + let stored_value = + inner.get(ctx, key_inner).unwrap().unwrap(); + assert_eq!( + &stored_value, value, + "the new item must be added to the back" + ); + + state.assert_validation_accepted(); + } + Transition::Remove((key_outer, key_middle, key_inner)) => { + let inner = state.lazy_map.at(key_outer).at(key_middle); + + let removed = + inner.remove(ctx, key_inner).unwrap().unwrap(); + + // Post-conditions: + assert_eq!( + &removed, + state + .eager_map + .get(key_outer) + .unwrap() + .get(key_middle) + .unwrap() + .get(key_inner) + .unwrap(), + "removed element matches the value in eager map \ + before it's updated" + ); + + state.assert_validation_accepted(); + } + Transition::Update( + (key_outer, key_middle, key_inner), + value, + ) => { + let inner = state.lazy_map.at(key_outer).at(key_middle); + + let old_val = inner.get(ctx, key_inner).unwrap().unwrap(); + + inner.insert(ctx, *key_inner, value.clone()).unwrap(); + + // Post-conditions: + let new_val = inner.get(ctx, key_inner).unwrap().unwrap(); + assert_eq!( + &old_val, + state + .eager_map + .get(key_outer) + .unwrap() + .get(key_middle) + .unwrap() + .get(key_inner) + .unwrap(), + "old value must match the value at the same key in \ + the eager map before it's updated" + ); + assert_eq!( + &new_val, value, + "new value must match that which was passed into the \ + Transition::Update" + ); + + state.assert_validation_accepted(); + } + } + + // Apply transition in the eager map for comparison + apply_transition_on_eager_map(&mut state.eager_map, &transition); + + // Global post-conditions: + + // All items in eager map must be present in lazy map + for (key_outer, middle) in state.eager_map.iter() { + for (key_middle, inner) in middle { + for (key_inner, expected_item) in inner { + let got = state + .lazy_map + .at(key_outer) + .at(key_middle) + .get(ctx, key_inner) + .unwrap() + .expect( + "The expected item must be present in lazy map", + ); + assert_eq!( + expected_item, &got, + "at key {key_outer}, {key_middle} {key_inner}" + ); + } + } + } + + // All items in lazy map must be present in eager map + for key_val in state.lazy_map.iter(ctx).unwrap() { + let ( + NestedSubKey::Data { + key: key_outer, + nested_sub_key: + NestedSubKey::Data { + key: key_middle, + nested_sub_key: SubKey::Data(key_inner), + }, + }, + expected_val, + ) = key_val.unwrap(); + let got = state + .eager_map + .get(&key_outer) + .unwrap() + .get(&key_middle) + .unwrap() + .get(&key_inner) + .expect("The expected item must be present in eager map"); + assert_eq!( + &expected_val, got, + "at key {key_outer}, {key_middle} {key_inner})" + ); + } + + state + } + } + + impl AbstractLazyMapState { + /// Find the length of the map from the applied transitions + fn len(&self) -> u64 { + (map_len_diff_from_transitions(self.committed_transitions.iter()) + + map_len_diff_from_transitions(self.valid_transitions.iter())) + .try_into() + .expect( + "It shouldn't be possible to underflow length from all \ + transactions applied in abstract state", + ) + } + + /// Build an eager map from the committed and current transitions + fn eager_map(&self) -> NestedEagerMap { + let mut eager_map = BTreeMap::new(); + for transition in &self.committed_transitions { + apply_transition_on_eager_map(&mut eager_map, transition); + } + for transition in &self.valid_transitions { + apply_transition_on_eager_map(&mut eager_map, transition); + } + eager_map + } + + /// Find the keys currently present in the map + fn find_existing_keys(&self) -> Vec { + let outer_map = self.eager_map(); + outer_map + .into_iter() + .fold(vec![], |acc, (outer, middle_map)| { + middle_map.into_iter().fold( + acc, + |mut acc, (middle, inner_map)| { + acc.extend( + inner_map + .into_iter() + .map(|(inner, _)| (outer, middle, inner)), + ); + acc + }, + ) + }) + } + } + + /// Find the difference in length of the map from the applied transitions + fn map_len_diff_from_transitions<'a>( + transitions: impl Iterator, + ) -> i64 { + let mut insert_count: i64 = 0; + let mut remove_count: i64 = 0; + + for trans in transitions { + match trans { + Transition::CommitTx + | Transition::CommitTxAndBlock + | Transition::Update(_, _) => {} + Transition::Insert(_, _) => insert_count += 1, + Transition::Remove(_) => remove_count += 1, + } + } + insert_count - remove_count + } + + impl ConcreteLazyMapState { + fn assert_validation_accepted(&self) { + // Init the VP env from tx env in which we applied the map + // transitions + let tx_env = tx_host_env::take(); + vp_host_env::init_from_tx(self.address.clone(), tx_env, |_| {}); + + // Simulate a validity predicate run using the lazy map's validation + // helpers + let changed_keys = + vp_host_env::with(|env| env.all_touched_storage_keys()); + + let mut validation_builder = None; + + // Push followed by pop is a no-op, in which case we'd still see the + // changed keys for these actions, but they wouldn't affect the + // validation result and they never get persisted, but we'd still + // them as changed key here. To guard against this case, + // we check that `map_len_from_transitions` is not empty. + let map_len_diff = + map_len_diff_from_transitions(self.current_transitions.iter()); + + // To help debug validation issues... + dbg!( + &self.current_transitions, + &changed_keys + .iter() + .map(storage::Key::to_string) + .collect::>() + ); + + for key in &changed_keys { + let is_sub_key = self + .lazy_map + .accumulate( + vp_host_env::ctx(), + &mut validation_builder, + key, + ) + .unwrap(); + + assert!( + is_sub_key, + "We're only modifying the lazy_map's keys here. Key: \ + \"{key}\", map length diff {map_len_diff}" + ); + } + if !changed_keys.is_empty() && map_len_diff != 0 { + assert!( + validation_builder.is_some(), + "If some keys were changed, the builder must get filled in" + ); + let actions = + NestedTestMap::validate(validation_builder.unwrap()) + .unwrap(); + let mut actions_to_check = actions.clone(); + + // Check that every transition has a corresponding action from + // validation. We drop the found actions to check that all + // actions are matched too. + let current_transitions = + normalize_transitions(&self.current_transitions); + for transition in ¤t_transitions { + use lazy_map::Action; + use lazy_map::NestedAction::At; + + match transition { + Transition::CommitTx | Transition::CommitTxAndBlock => { + } + Transition::Insert(expected_key, expected_val) => { + for (ix, action) in + actions_to_check.iter().enumerate() + { + if let At( + key_outer, + At( + key_middle, + Action::Insert(key_inner, val), + ), + ) = action + { + let key = + (*key_outer, *key_middle, *key_inner); + if expected_key == &key + && expected_val == val + { + actions_to_check.remove(ix); + break; + } + } + } + } + Transition::Remove(expected_key) => { + for (ix, action) in + actions_to_check.iter().enumerate() + { + if let At( + key_outer, + At( + key_middle, + Action::Remove(key_inner, _val), + ), + ) = action + { + let key = + (*key_outer, *key_middle, *key_inner); + if expected_key == &key { + actions_to_check.remove(ix); + break; + } + } + } + } + Transition::Update(expected_key, value) => { + for (ix, action) in + actions_to_check.iter().enumerate() + { + if let At( + key_outer, + At( + key_middle, + Action::Update { + key: key_inner, + pre: _, + post, + }, + ), + ) = action + { + let key = + (*key_outer, *key_middle, *key_inner); + if expected_key == &key && post == value { + actions_to_check.remove(ix); + break; + } + } + } + } + } + } + + assert!( + actions_to_check.is_empty(), + "All the actions reported from validation {actions:#?} \ + should have been matched with SM transitions \ + {current_transitions:#?}, but these actions didn't \ + match: {actions_to_check:#?}", + ) + } + + // Put the tx_env back before checking the result + tx_host_env::set_from_vp_env(vp_host_env::take()); + } + } + + /// Generate an arbitrary `TestKey` + fn arb_map_key() -> impl Strategy { + (any::(), any::(), any::()) + } + + /// Generate an arbitrary `TestVal` + fn arb_map_val() -> impl Strategy { + (any::(), any::()).prop_map(|(x, y)| TestVal { x, y }) + } + + /// Apply `Transition` on an eager `Map`. + fn apply_transition_on_eager_map( + map: &mut NestedEagerMap, + transition: &Transition, + ) { + match transition { + Transition::CommitTx | Transition::CommitTxAndBlock => {} + Transition::Insert((key_outer, key_middle, key_inner), value) + | Transition::Update((key_outer, key_middle, key_inner), value) => { + let middle = + map.entry(*key_outer).or_insert_with(Default::default); + let inner = + middle.entry(*key_middle).or_insert_with(Default::default); + inner.insert(*key_inner, value.clone()); + } + Transition::Remove((key_outer, key_middle, key_inner)) => { + let middle = + map.entry(*key_outer).or_insert(Default::default()); + let inner = + middle.entry(*key_middle).or_insert_with(Default::default); + let _popped = inner.remove(key_inner); + } + } + } + + /// Normalize transitions: + /// - remove(key) + insert(key, val) -> update(key, val) + /// - insert(key, val) + update(key, new_val) -> insert(key, new_val) + /// - update(key, val) + update(key, new_val) -> update(key, new_val) + /// + /// Note that the normalizable transitions pairs do not have to be directly + /// next to each other, but their order does matter. + fn normalize_transitions(transitions: &[Transition]) -> Vec { + let mut collapsed = vec![]; + 'outer: for transition in transitions { + match transition { + Transition::CommitTx + | Transition::CommitTxAndBlock + | Transition::Remove(_) => collapsed.push(transition.clone()), + Transition::Insert(key, val) => { + for (ix, collapsed_transition) in + collapsed.iter().enumerate() + { + if let Transition::Remove(remove_key) = + collapsed_transition + { + if key == remove_key { + // remove(key) + insert(key, val) -> update(key, + // val) + + // Replace the Remove with an Update instead of + // inserting the Insert + *collapsed.get_mut(ix).unwrap() = + Transition::Update(*key, val.clone()); + continue 'outer; + } + } + } + collapsed.push(transition.clone()); + } + Transition::Update(key, value) => { + for (ix, collapsed_transition) in + collapsed.iter().enumerate() + { + if let Transition::Insert(insert_key, _) = + collapsed_transition + { + if key == insert_key { + // insert(key, val) + update(key, new_val) -> + // insert(key, new_val) + + // Replace the insert with the new update's + // value instead of inserting it + *collapsed.get_mut(ix).unwrap() = + Transition::Insert(*key, value.clone()); + continue 'outer; + } + } else if let Transition::Update(update_key, _) = + collapsed_transition + { + if key == update_key { + // update(key, val) + update(key, new_val) -> + // update(key, new_val) + + // Replace the insert with the new update's + // value instead of inserting it + *collapsed.get_mut(ix).unwrap() = + Transition::Update(*key, value.clone()); + continue 'outer; + } + } + } + collapsed.push(transition.clone()); + } + } + } + collapsed + } +} From 4adf30130baa0152a5738f8f84a89124f8bd65b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 22 Sep 2022 18:01:30 +0200 Subject: [PATCH 77/81] ledger/storage/lazy: remove unused error cases --- shared/src/ledger/storage_api/collections/lazy_map.rs | 2 -- shared/src/ledger/storage_api/collections/lazy_vec.rs | 2 -- 2 files changed, 4 deletions(-) diff --git a/shared/src/ledger/storage_api/collections/lazy_map.rs b/shared/src/ledger/storage_api/collections/lazy_map.rs index 686f9d336e..6e32b5399b 100644 --- a/shared/src/ledger/storage_api/collections/lazy_map.rs +++ b/shared/src/ledger/storage_api/collections/lazy_map.rs @@ -98,8 +98,6 @@ pub enum NestedSubKey { #[allow(missing_docs)] #[derive(Error, Debug)] pub enum ValidationError { - #[error("Storage error in reading key {0}")] - StorageError(storage::Key), #[error("Invalid storage key {0}")] InvalidSubKey(storage::Key), #[error("Invalid nested storage key {0}")] diff --git a/shared/src/ledger/storage_api/collections/lazy_vec.rs b/shared/src/ledger/storage_api/collections/lazy_vec.rs index fd61bef804..59eaa225e5 100644 --- a/shared/src/ledger/storage_api/collections/lazy_vec.rs +++ b/shared/src/ledger/storage_api/collections/lazy_vec.rs @@ -75,8 +75,6 @@ pub enum Action { #[allow(missing_docs)] #[derive(Error, Debug)] pub enum ValidationError { - #[error("Storage error in reading key {0}")] - StorageError(storage::Key), #[error("Incorrect difference in LazyVec's length")] InvalidLenDiff, #[error("An empty LazyVec must be deleted from storage")] From e7c868f0d4a5534251adfb44ae4b970829030b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 22 Sep 2022 18:02:42 +0200 Subject: [PATCH 78/81] ledger/storage/lazy: update lazy_set for updated trait LazyCollection --- .../storage_api/collections/lazy_set.rs | 103 +++++++++++++++++- .../src/ledger/storage_api/collections/mod.rs | 3 +- .../collections/nested_lazy_map.rs | 2 +- 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/shared/src/ledger/storage_api/collections/lazy_set.rs b/shared/src/ledger/storage_api/collections/lazy_set.rs index 9635724543..3a001e6048 100644 --- a/shared/src/ledger/storage_api/collections/lazy_set.rs +++ b/shared/src/ledger/storage_api/collections/lazy_set.rs @@ -1,11 +1,14 @@ //! Lazy set. +use std::fmt::Debug; use std::marker::PhantomData; +use thiserror::Error; + use super::super::Result; use super::{LazyCollection, ReadError}; use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; -use crate::types::storage::{self, KeySeg}; +use crate::types::storage::{self, DbKeySeg, KeySeg}; /// Subkey corresponding to the data elements of the LazySet pub const DATA_SUBKEY: &str = "data"; @@ -29,7 +32,42 @@ pub struct LazySet { phantom: PhantomData, } -impl LazyCollection for LazySet { +/// Possible sub-keys of a [`LazySet`] +#[derive(Clone, Debug)] +pub enum SubKey { + /// Data sub-key with its literal set value + Data(T), +} + +/// Possible actions that can modify a [`LazySet`]. This +/// roughly corresponds to the methods that have `StorageWrite` access. +#[derive(Clone, Debug)] +pub enum Action { + /// Insert or update a value `T` in a [`LazySet`]. + Insert(T), + /// Remove a value `T` from a [`LazySet`]. + Remove(T), +} + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum ValidationError { + #[error("Invalid storage key {0}")] + InvalidSubKey(storage::Key), +} + +impl LazyCollection for LazySet +where + T: storage::KeySeg + Debug, +{ + type Action = Action; + type SubKey = SubKey; + // In a set, the `SubKey` already contains the data, but we have to + // distinguish `Insert` from `Remove` + type SubKeyWithData = Action; + // There is no "value" for LazySet, `T` is written into the key + type Value = (); + /// Create or use an existing set with the given storage `key`. fn open(key: storage::Key) -> Self { Self { @@ -37,6 +75,67 @@ impl LazyCollection for LazySet { phantom: PhantomData, } } + + fn is_valid_sub_key( + &self, + key: &storage::Key, + ) -> storage_api::Result> { + let suffix = match key.split_prefix(&self.key) { + None => { + // not matching prefix, irrelevant + return Ok(None); + } + Some(None) => { + // no suffix, invalid + return Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result(); + } + Some(Some(suffix)) => suffix, + }; + + // Match the suffix against expected sub-keys + match &suffix.segments[..] { + [DbKeySeg::StringSeg(sub_a), DbKeySeg::StringSeg(sub_b)] + if sub_a == DATA_SUBKEY => + { + if let Ok(key_in_kv) = storage::KeySeg::parse(sub_b.clone()) { + Ok(Some(SubKey::Data(key_in_kv))) + } else { + Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result() + } + } + _ => Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result(), + } + } + + fn read_sub_key_data( + env: &ENV, + storage_key: &storage::Key, + sub_key: Self::SubKey, + ) -> storage_api::Result> + where + ENV: for<'a> crate::ledger::vp_env::VpEnv<'a>, + { + // There is no "value" for LazySet, `T` is written into the key + let SubKey::Data(sub_key) = sub_key; + let has_pre = env.has_key_pre(storage_key)?; + let has_post = env.has_key_post(storage_key)?; + if has_pre && !has_post { + Ok(Some(Action::Remove(sub_key))) + } else if !has_pre && has_post { + Ok(Some(Action::Insert(sub_key))) + } else { + Ok(None) + } + } + + fn validate_changed_sub_keys( + keys: Vec, + ) -> storage_api::Result> { + Ok(keys) + } } impl LazySet diff --git a/shared/src/ledger/storage_api/collections/mod.rs b/shared/src/ledger/storage_api/collections/mod.rs index b77b207c7f..3a9236fc71 100644 --- a/shared/src/ledger/storage_api/collections/mod.rs +++ b/shared/src/ledger/storage_api/collections/mod.rs @@ -105,8 +105,7 @@ pub trait LazyCollection { /// call `fn build()` on it to get the validation result. /// This function will return `Ok(true)` if the storage key is a valid /// sub-key of this collection, `Ok(false)` if the storage key doesn't match - /// the prefix of this collection, or fail with - /// [`ValidationError::InvalidSubKey`] if the prefix matches this + /// the prefix of this collection, or error if the prefix matches this /// collection, but the key itself is not recognized. fn accumulate( &self, diff --git a/tests/src/storage_api/collections/nested_lazy_map.rs b/tests/src/storage_api/collections/nested_lazy_map.rs index 80b066c18f..037decce46 100644 --- a/tests/src/storage_api/collections/nested_lazy_map.rs +++ b/tests/src/storage_api/collections/nested_lazy_map.rs @@ -639,7 +639,7 @@ mod tests { } Transition::Remove((key_outer, key_middle, key_inner)) => { let middle = - map.entry(*key_outer).or_insert(Default::default()); + map.entry(*key_outer).or_insert_with(Default::default); let inner = middle.entry(*key_middle).or_insert_with(Default::default); let _popped = inner.remove(key_inner); From a5d4a67d41ea17394256c7ea09b5f8c216fd0ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 22 Sep 2022 18:06:09 +0200 Subject: [PATCH 79/81] remove unfinished lazy_set, lazy_hashset and lazy_hashmap for now --- .../ledger/storage_api/collections/hasher.rs | 9 - .../storage_api/collections/lazy_hashmap.rs | 269 ----------------- .../storage_api/collections/lazy_hashset.rs | 184 ------------ .../storage_api/collections/lazy_map.rs | 3 - .../storage_api/collections/lazy_set.rs | 284 ------------------ .../src/ledger/storage_api/collections/mod.rs | 7 - 6 files changed, 756 deletions(-) delete mode 100644 shared/src/ledger/storage_api/collections/hasher.rs delete mode 100644 shared/src/ledger/storage_api/collections/lazy_hashmap.rs delete mode 100644 shared/src/ledger/storage_api/collections/lazy_hashset.rs delete mode 100644 shared/src/ledger/storage_api/collections/lazy_set.rs diff --git a/shared/src/ledger/storage_api/collections/hasher.rs b/shared/src/ledger/storage_api/collections/hasher.rs deleted file mode 100644 index 0f864259f5..0000000000 --- a/shared/src/ledger/storage_api/collections/hasher.rs +++ /dev/null @@ -1,9 +0,0 @@ -use borsh::BorshSerialize; - -/// Hash borsh encoded data into a storage sub-key. -/// This is a sha256 as an uppercase hexadecimal string. -pub fn hash_for_storage_key(data: impl BorshSerialize) -> String { - let bytes = data.try_to_vec().unwrap(); - let hash = crate::types::hash::Hash::sha256(bytes); - hash.to_string() -} diff --git a/shared/src/ledger/storage_api/collections/lazy_hashmap.rs b/shared/src/ledger/storage_api/collections/lazy_hashmap.rs deleted file mode 100644 index 9a60fd18f0..0000000000 --- a/shared/src/ledger/storage_api/collections/lazy_hashmap.rs +++ /dev/null @@ -1,269 +0,0 @@ -//! Lazy hash map. - -use std::marker::PhantomData; - -use borsh::{BorshDeserialize, BorshSerialize}; - -use super::super::Result; -use super::hasher::hash_for_storage_key; -use super::LazyCollection; -use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; -use crate::types::storage; - -/// Subkey corresponding to the data elements of the LazyMap -pub const DATA_SUBKEY: &str = "data"; - -/// Lazy hash map. -/// -/// This can be used as an alternative to `std::collections::HashMap` and -/// `BTreeMap`. In the lazy map, the elements do not reside in memory but are -/// instead read and written to storage sub-keys of the storage `key` given to -/// construct the map. -/// -/// In the [`LazyHashMap`], the type of key `K` can be anything that -/// [`BorshSerialize`] and [`BorshDeserialize`] and a hex string of sha256 hash -/// over the borsh encoded keys are used as storage key segments. -/// -/// This is different from [`super::LazyMap`], which uses [`storage::KeySeg`] -/// trait. -/// -/// Additionally, [`LazyHashMap`] also writes the unhashed values into the -/// storage together with the values (using an internal `KeyVal` type). -#[derive(Debug)] -pub struct LazyHashMap { - key: storage::Key, - phantom_k: PhantomData, - phantom_v: PhantomData, -} - -/// Struct to hold a key-value pair -#[derive(Debug, BorshSerialize, BorshDeserialize)] -struct KeyVal { - key: K, - val: V, -} - -impl LazyCollection for LazyHashMap { - /// Create or use an existing map with the given storage `key`. - fn open(key: storage::Key) -> Self { - Self { - key, - phantom_k: PhantomData, - phantom_v: PhantomData, - } - } -} - -impl LazyHashMap -where - K: BorshDeserialize + BorshSerialize + 'static, - V: BorshDeserialize + BorshSerialize + 'static, -{ - /// Inserts a key-value pair into the map. - /// - /// If the map did not have this key present, `None` is returned. - /// If the map did have this key present, the value is updated, and the old - /// value is returned. Unlike in `std::collection::HashMap`, the key is also - /// updated; this matters for types that can be `==` without being - /// identical. - pub fn insert( - &self, - storage: &mut S, - key: K, - val: V, - ) -> Result> - where - S: StorageWrite + for<'iter> StorageRead<'iter>, - { - let previous = self.get(storage, &key)?; - - let data_key = self.get_data_key(&key); - Self::write_key_val(storage, &data_key, key, val)?; - - Ok(previous) - } - - /// Removes a key from the map, returning the value at the key if the key - /// was previously in the map. - pub fn remove(&self, storage: &mut S, key: &K) -> Result> - where - S: StorageWrite + for<'iter> StorageRead<'iter>, - { - let value = self.get(storage, key)?; - - let data_key = self.get_data_key(key); - storage.delete(&data_key)?; - - Ok(value) - } - - /// Returns the value corresponding to the key, if any. - pub fn get(&self, storage: &S, key: &K) -> Result> - where - S: for<'iter> StorageRead<'iter>, - { - let res = self.get_key_val(storage, key)?; - Ok(res.map(|(_key, val)| val)) - } - - /// Returns the key-value corresponding to the key, if any. - pub fn get_key_val(&self, storage: &S, key: &K) -> Result> - where - S: for<'iter> StorageRead<'iter>, - { - let data_key = self.get_data_key(key); - Self::read_key_val(storage, &data_key) - } - - /// Returns the key-value corresponding to the given hash of a key, if any. - pub fn get_key_val_by_hash( - &self, - storage: &S, - key_hash: &str, - ) -> Result> - where - S: for<'iter> StorageRead<'iter>, - { - let data_key = - self.get_data_prefix().push(&key_hash.to_string()).unwrap(); - Self::read_key_val(storage, &data_key) - } - - /// Returns whether the set contains a value. - pub fn contains(&self, storage: &S, key: &K) -> Result - where - S: for<'iter> StorageRead<'iter>, - { - storage.has_key(&self.get_data_key(key)) - } - - /// Returns whether the map contains no elements. - pub fn is_empty(&self, storage: &S) -> Result - where - S: for<'iter> StorageRead<'iter>, - { - let mut iter = - storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; - Ok(iter.next().is_none()) - } - - /// Reads the number of elements in the map. - /// - /// Note that this function shouldn't be used in transactions and VPs code - /// on unbounded maps to avoid gas usage increasing with the length of the - /// set. - #[allow(clippy::len_without_is_empty)] - pub fn len(&self, storage: &S) -> Result - where - S: for<'iter> StorageRead<'iter>, - { - let iter = - storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; - iter.count().try_into().into_storage_result() - } - - /// An iterator visiting all key-value elements. The iterator element type - /// is `Result<(K, V)>`, because iterator's call to `next` may fail with - /// e.g. out of gas or data decoding error. - /// - /// Note that this function shouldn't be used in transactions and VPs code - /// on unbounded maps to avoid gas usage increasing with the length of the - /// map. - pub fn iter<'iter>( - &self, - storage: &'iter impl StorageRead<'iter>, - ) -> Result> + 'iter> { - let iter = storage_api::iter_prefix(storage, &self.get_data_prefix())?; - Ok(iter.map(|key_val_res| { - let (_key, val) = key_val_res?; - let KeyVal { key, val } = val; - Ok((key, val)) - })) - } - - /// Reads a key-value from storage - fn read_key_val( - storage: &S, - storage_key: &storage::Key, - ) -> Result> - where - S: for<'iter> StorageRead<'iter>, - { - let res = storage.read(storage_key)?; - Ok(res.map(|KeyVal { key, val }| (key, val))) - } - - /// Write a key-value into storage - fn write_key_val( - storage: &mut impl StorageWrite, - storage_key: &storage::Key, - key: K, - val: V, - ) -> Result<()> { - storage.write(storage_key, KeyVal { key, val }) - } - - /// Get the prefix of set's elements storage - fn get_data_prefix(&self) -> storage::Key { - self.key.push(&DATA_SUBKEY.to_owned()).unwrap() - } - - /// Get the sub-key of a given element - fn get_data_key(&self, key: &K) -> storage::Key { - let hash_str = hash_for_storage_key(key); - self.get_data_prefix().push(&hash_str).unwrap() - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::ledger::storage::testing::TestStorage; - - #[test] - fn test_lazy_hash_map_basics() -> storage_api::Result<()> { - let mut storage = TestStorage::default(); - - let key = storage::Key::parse("test").unwrap(); - let lazy_map = LazyHashMap::::open(key); - - // The map should be empty at first - assert!(lazy_map.is_empty(&storage)?); - assert!(lazy_map.len(&storage)? == 0); - assert!(!lazy_map.contains(&storage, &0)?); - assert!(!lazy_map.contains(&storage, &1)?); - assert!(lazy_map.iter(&storage)?.next().is_none()); - assert!(lazy_map.get(&storage, &0)?.is_none()); - assert!(lazy_map.get(&storage, &1)?.is_none()); - assert!(lazy_map.remove(&mut storage, &0)?.is_none()); - assert!(lazy_map.remove(&mut storage, &1)?.is_none()); - - // Insert a new value and check that it's added - let (key, val) = (123, "Test".to_string()); - lazy_map.insert(&mut storage, key, val.clone())?; - assert!(!lazy_map.contains(&storage, &0)?); - assert!(lazy_map.contains(&storage, &key)?); - assert!(!lazy_map.is_empty(&storage)?); - assert!(lazy_map.len(&storage)? == 1); - assert_eq!( - lazy_map.iter(&storage)?.next().unwrap()?, - (key, val.clone()) - ); - assert!(lazy_map.get(&storage, &0)?.is_none()); - assert_eq!(lazy_map.get(&storage, &key)?.unwrap(), val); - - // Remove the last value and check that the map is empty again - let removed = lazy_map.remove(&mut storage, &key)?.unwrap(); - assert_eq!(removed, val); - assert!(lazy_map.is_empty(&storage)?); - assert!(lazy_map.len(&storage)? == 0); - assert!(!lazy_map.contains(&storage, &0)?); - assert!(!lazy_map.contains(&storage, &1)?); - assert!(lazy_map.get(&storage, &0)?.is_none()); - assert!(lazy_map.get(&storage, &key)?.is_none()); - assert!(lazy_map.iter(&storage)?.next().is_none()); - assert!(lazy_map.remove(&mut storage, &key)?.is_none()); - - Ok(()) - } -} diff --git a/shared/src/ledger/storage_api/collections/lazy_hashset.rs b/shared/src/ledger/storage_api/collections/lazy_hashset.rs deleted file mode 100644 index 63ac5c845c..0000000000 --- a/shared/src/ledger/storage_api/collections/lazy_hashset.rs +++ /dev/null @@ -1,184 +0,0 @@ -//! Lazy hash set. - -use std::marker::PhantomData; - -use borsh::{BorshDeserialize, BorshSerialize}; - -use super::super::Result; -use super::hasher::hash_for_storage_key; -use super::LazyCollection; -use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; -use crate::types::storage; - -/// Subkey corresponding to the data elements of the LazySet -pub const DATA_SUBKEY: &str = "data"; - -/// Lazy hash set. -/// -/// This can be used as an alternative to `std::collections::HashSet` and -/// `BTreeSet`. In the lazy set, the elements do not reside in memory but are -/// instead read and written to storage sub-keys of the storage `key` given to -/// construct the set. -/// -/// In the [`LazyHashSet`], the type of value `T` can be anything that -/// [`BorshSerialize`] and [`BorshDeserialize`] and a hex string of sha256 hash -/// over the borsh encoded values are used as storage key segments. -/// -/// This is different from [`super::LazySet`], which uses [`storage::KeySeg`] -/// trait. -/// -/// Additionally, [`LazyHashSet`] also writes the unhashed values into the -/// storage. -#[derive(Debug)] -pub struct LazyHashSet { - key: storage::Key, - phantom: PhantomData, -} - -impl LazyCollection for LazyHashSet { - /// Create or use an existing set with the given storage `key`. - fn open(key: storage::Key) -> Self { - Self { - key, - phantom: PhantomData, - } - } -} - -impl LazyHashSet -where - T: BorshSerialize + BorshDeserialize + 'static, -{ - /// Adds a value to the set. If the set did not have this value present, - /// `Ok(true)` is returned, `Ok(false)` otherwise. - pub fn insert(&self, storage: &mut S, val: T) -> Result - where - S: StorageWrite + for<'iter> StorageRead<'iter>, - { - if self.contains(storage, &val)? { - Ok(false) - } else { - let data_key = self.get_data_key(&val); - storage.write(&data_key, &val)?; - Ok(true) - } - } - - /// Removes a value from the set. Returns whether the value was present in - /// the set. - pub fn remove(&self, storage: &mut S, val: &T) -> Result - where - S: StorageWrite + for<'iter> StorageRead<'iter>, - { - let data_key = self.get_data_key(val); - let value: Option = storage.read(&data_key)?; - storage.delete(&data_key)?; - Ok(value.is_some()) - } - - /// Returns whether the set contains a value. - pub fn contains(&self, storage: &S, val: &T) -> Result - where - S: for<'iter> StorageRead<'iter>, - { - let value: Option = storage.read(&self.get_data_key(val))?; - Ok(value.is_some()) - } - - /// Reads the number of elements in the set. - /// - /// Note that this function shouldn't be used in transactions and VPs code - /// on unbounded sets to avoid gas usage increasing with the length of the - /// set. - #[allow(clippy::len_without_is_empty)] - pub fn len(&self, storage: &S) -> Result - where - S: for<'iter> StorageRead<'iter>, - { - let iter = - storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; - iter.count().try_into().into_storage_result() - } - - /// Returns whether the set contains no elements. - pub fn is_empty(&self, storage: &S) -> Result - where - S: for<'iter> StorageRead<'iter>, - { - let mut iter = storage.iter_prefix(&self.get_data_prefix())?; - Ok(storage.iter_next(&mut iter)?.is_none()) - } - - /// An iterator visiting all elements. The iterator element type is - /// `Result`, because iterator's call to `next` may fail with e.g. out of - /// gas or data decoding error. - /// - /// Note that this function shouldn't be used in transactions and VPs code - /// on unbounded sets to avoid gas usage increasing with the length of the - /// set. - pub fn iter<'iter>( - &self, - storage: &'iter impl StorageRead<'iter>, - ) -> Result> + 'iter> { - let iter = storage_api::iter_prefix(storage, &self.get_data_prefix())?; - Ok(iter.map(|key_val_res| { - let (_key, val) = key_val_res?; - Ok(val) - })) - } - - /// Get the prefix of set's elements storage - fn get_data_prefix(&self) -> storage::Key { - self.key.push(&DATA_SUBKEY.to_owned()).unwrap() - } - - /// Get the sub-key of a given element - fn get_data_key(&self, val: &T) -> storage::Key { - let hash_str = hash_for_storage_key(val); - self.get_data_prefix().push(&hash_str).unwrap() - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::ledger::storage::testing::TestStorage; - - #[test] - fn test_lazy_set_basics() -> storage_api::Result<()> { - let mut storage = TestStorage::default(); - - let key = storage::Key::parse("test").unwrap(); - let lazy_set = LazyHashSet::::open(key); - - // The set should be empty at first - assert!(lazy_set.is_empty(&storage)?); - assert!(lazy_set.len(&storage)? == 0); - assert!(!lazy_set.contains(&storage, &0)?); - assert!(lazy_set.is_empty(&storage)?); - assert!(lazy_set.iter(&storage)?.next().is_none()); - assert!(!lazy_set.remove(&mut storage, &0)?); - assert!(!lazy_set.remove(&mut storage, &1)?); - - // Insert a new value and check that it's added - let val = 1337; - lazy_set.insert(&mut storage, val)?; - assert!(!lazy_set.is_empty(&storage)?); - assert!(lazy_set.len(&storage)? == 1); - assert_eq!(lazy_set.iter(&storage)?.next().unwrap()?, val.clone()); - assert!(!lazy_set.contains(&storage, &0)?); - assert!(lazy_set.contains(&storage, &val)?); - - // Remove the last value and check that the set is empty again - let is_removed = lazy_set.remove(&mut storage, &val)?; - assert!(is_removed); - assert!(lazy_set.is_empty(&storage)?); - assert!(lazy_set.len(&storage)? == 0); - assert!(!lazy_set.contains(&storage, &0)?); - assert!(lazy_set.is_empty(&storage)?); - assert!(!lazy_set.remove(&mut storage, &0)?); - assert!(!lazy_set.remove(&mut storage, &1)?); - - Ok(()) - } -} diff --git a/shared/src/ledger/storage_api/collections/lazy_map.rs b/shared/src/ledger/storage_api/collections/lazy_map.rs index 6e32b5399b..34a0f7d891 100644 --- a/shared/src/ledger/storage_api/collections/lazy_map.rs +++ b/shared/src/ledger/storage_api/collections/lazy_map.rs @@ -28,9 +28,6 @@ pub const DATA_SUBKEY: &str = "data"; /// In the [`LazyMap`], the type of key `K` can be anything that implements /// [`storage::KeySeg`] and this trait is used to turn the keys into key /// segments. -/// -/// This is different from [`super::LazyHashMap`], which hashes borsh encoded -/// key. #[derive(Debug)] pub struct LazyMap { key: storage::Key, diff --git a/shared/src/ledger/storage_api/collections/lazy_set.rs b/shared/src/ledger/storage_api/collections/lazy_set.rs deleted file mode 100644 index 3a001e6048..0000000000 --- a/shared/src/ledger/storage_api/collections/lazy_set.rs +++ /dev/null @@ -1,284 +0,0 @@ -//! Lazy set. - -use std::fmt::Debug; -use std::marker::PhantomData; - -use thiserror::Error; - -use super::super::Result; -use super::{LazyCollection, ReadError}; -use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; -use crate::types::storage::{self, DbKeySeg, KeySeg}; - -/// Subkey corresponding to the data elements of the LazySet -pub const DATA_SUBKEY: &str = "data"; - -/// Lazy set. -/// -/// This can be used as an alternative to `std::collections::HashSet` and -/// `BTreeSet`. In the lazy set, the elements do not reside in memory but are -/// instead read and written to storage sub-keys of the storage `key` used to -/// construct the set. -/// -/// In the [`LazySet`], the type of value `T` can be anything that implements -/// [`storage::KeySeg`] and this trait is used to turn the values into key -/// segments. -/// -/// This is different from [`super::LazyHashSet`], which hashes borsh encoded -/// values. -#[derive(Debug)] -pub struct LazySet { - key: storage::Key, - phantom: PhantomData, -} - -/// Possible sub-keys of a [`LazySet`] -#[derive(Clone, Debug)] -pub enum SubKey { - /// Data sub-key with its literal set value - Data(T), -} - -/// Possible actions that can modify a [`LazySet`]. This -/// roughly corresponds to the methods that have `StorageWrite` access. -#[derive(Clone, Debug)] -pub enum Action { - /// Insert or update a value `T` in a [`LazySet`]. - Insert(T), - /// Remove a value `T` from a [`LazySet`]. - Remove(T), -} - -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum ValidationError { - #[error("Invalid storage key {0}")] - InvalidSubKey(storage::Key), -} - -impl LazyCollection for LazySet -where - T: storage::KeySeg + Debug, -{ - type Action = Action; - type SubKey = SubKey; - // In a set, the `SubKey` already contains the data, but we have to - // distinguish `Insert` from `Remove` - type SubKeyWithData = Action; - // There is no "value" for LazySet, `T` is written into the key - type Value = (); - - /// Create or use an existing set with the given storage `key`. - fn open(key: storage::Key) -> Self { - Self { - key, - phantom: PhantomData, - } - } - - fn is_valid_sub_key( - &self, - key: &storage::Key, - ) -> storage_api::Result> { - let suffix = match key.split_prefix(&self.key) { - None => { - // not matching prefix, irrelevant - return Ok(None); - } - Some(None) => { - // no suffix, invalid - return Err(ValidationError::InvalidSubKey(key.clone())) - .into_storage_result(); - } - Some(Some(suffix)) => suffix, - }; - - // Match the suffix against expected sub-keys - match &suffix.segments[..] { - [DbKeySeg::StringSeg(sub_a), DbKeySeg::StringSeg(sub_b)] - if sub_a == DATA_SUBKEY => - { - if let Ok(key_in_kv) = storage::KeySeg::parse(sub_b.clone()) { - Ok(Some(SubKey::Data(key_in_kv))) - } else { - Err(ValidationError::InvalidSubKey(key.clone())) - .into_storage_result() - } - } - _ => Err(ValidationError::InvalidSubKey(key.clone())) - .into_storage_result(), - } - } - - fn read_sub_key_data( - env: &ENV, - storage_key: &storage::Key, - sub_key: Self::SubKey, - ) -> storage_api::Result> - where - ENV: for<'a> crate::ledger::vp_env::VpEnv<'a>, - { - // There is no "value" for LazySet, `T` is written into the key - let SubKey::Data(sub_key) = sub_key; - let has_pre = env.has_key_pre(storage_key)?; - let has_post = env.has_key_post(storage_key)?; - if has_pre && !has_post { - Ok(Some(Action::Remove(sub_key))) - } else if !has_pre && has_post { - Ok(Some(Action::Insert(sub_key))) - } else { - Ok(None) - } - } - - fn validate_changed_sub_keys( - keys: Vec, - ) -> storage_api::Result> { - Ok(keys) - } -} - -impl LazySet -where - T: storage::KeySeg, -{ - /// Adds a value to the set. If the set did not have this value present, - /// `Ok(true)` is returned, `Ok(false)` otherwise. - pub fn insert(&self, storage: &mut S, val: T) -> Result - where - S: StorageWrite + for<'iter> StorageRead<'iter>, - { - if self.contains(storage, &val)? { - Ok(false) - } else { - let data_key = self.get_data_key(&val); - // The actual value is written into the key, so the value written to - // the storage is empty (unit) - storage.write(&data_key, ())?; - Ok(true) - } - } - - /// Removes a value from the set. Returns whether the value was present in - /// the set. - pub fn remove(&self, storage: &mut S, val: &T) -> Result - where - S: StorageWrite + for<'iter> StorageRead<'iter>, - { - let data_key = self.get_data_key(val); - let value: Option<()> = storage.read(&data_key)?; - storage.delete(&data_key)?; - Ok(value.is_some()) - } - - /// Returns whether the set contains a value. - pub fn contains(&self, storage: &S, val: &T) -> Result - where - S: for<'iter> StorageRead<'iter>, - { - storage.has_key(&self.get_data_key(val)) - } - - /// Returns whether the set contains no elements. - pub fn is_empty(&self, storage: &S) -> Result - where - S: for<'iter> StorageRead<'iter>, - { - let mut iter = - storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; - Ok(iter.next().is_none()) - } - - /// Reads the number of elements in the set. - /// - /// Note that this function shouldn't be used in transactions and VPs code - /// on unbounded sets to avoid gas usage increasing with the length of the - /// set. - #[allow(clippy::len_without_is_empty)] - pub fn len(&self, storage: &S) -> Result - where - S: for<'iter> StorageRead<'iter>, - { - let iter = - storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; - iter.count().try_into().into_storage_result() - } - - /// An iterator visiting all elements. The iterator element type is - /// `Result`, because iterator's call to `next` may fail with e.g. out of - /// gas or data decoding error. - /// - /// Note that this function shouldn't be used in transactions and VPs code - /// on unbounded sets to avoid gas usage increasing with the length of the - /// set. - pub fn iter<'iter>( - &self, - storage: &'iter impl StorageRead<'iter>, - ) -> Result> + 'iter> { - let iter = - storage_api::iter_prefix_bytes(storage, &self.get_data_prefix())?; - Ok(iter.map(|key_val_res| { - let (key, _val) = key_val_res?; - let last_key_seg = key - .last() - .ok_or(ReadError::UnexpectedlyEmptyStorageKey) - .into_storage_result()?; - T::parse(last_key_seg.raw()).into_storage_result() - })) - } - - /// Get the prefix of set's elements storage - fn get_data_prefix(&self) -> storage::Key { - self.key.push(&DATA_SUBKEY.to_owned()).unwrap() - } - - /// Get the sub-key of a given element - fn get_data_key(&self, val: &T) -> storage::Key { - let key_str = val.to_db_key(); - self.get_data_prefix().push(&key_str).unwrap() - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::ledger::storage::testing::TestStorage; - - #[test] - fn test_lazy_set_basics() -> storage_api::Result<()> { - let mut storage = TestStorage::default(); - - let key = storage::Key::parse("test").unwrap(); - let lazy_set = LazySet::::open(key); - - // The set should be empty at first - assert!(lazy_set.is_empty(&storage)?); - assert!(lazy_set.len(&storage)? == 0); - assert!(!lazy_set.contains(&storage, &0)?); - assert!(lazy_set.is_empty(&storage)?); - assert!(lazy_set.iter(&storage)?.next().is_none()); - assert!(!lazy_set.remove(&mut storage, &0)?); - assert!(!lazy_set.remove(&mut storage, &1)?); - - // Insert a new value and check that it's added - let val = 1337; - lazy_set.insert(&mut storage, val)?; - assert!(!lazy_set.is_empty(&storage)?); - assert!(lazy_set.len(&storage)? == 1); - assert_eq!(lazy_set.iter(&storage)?.next().unwrap()?, val.clone()); - assert!(!lazy_set.contains(&storage, &0)?); - assert!(lazy_set.contains(&storage, &val)?); - - // Remove the last value and check that the set is empty again - let is_removed = lazy_set.remove(&mut storage, &val)?; - assert!(is_removed); - assert!(lazy_set.is_empty(&storage)?); - assert!(lazy_set.len(&storage)? == 0); - assert!(!lazy_set.contains(&storage, &0)?); - assert!(lazy_set.is_empty(&storage)?); - assert!(!lazy_set.remove(&mut storage, &0)?); - assert!(!lazy_set.remove(&mut storage, &1)?); - - Ok(()) - } -} diff --git a/shared/src/ledger/storage_api/collections/mod.rs b/shared/src/ledger/storage_api/collections/mod.rs index 3a9236fc71..688b76bd49 100644 --- a/shared/src/ledger/storage_api/collections/mod.rs +++ b/shared/src/ledger/storage_api/collections/mod.rs @@ -13,17 +13,10 @@ use borsh::BorshDeserialize; use derivative::Derivative; use thiserror::Error; -mod hasher; -// pub mod lazy_hashmap; -// pub mod lazy_hashset; pub mod lazy_map; -// pub mod lazy_set; pub mod lazy_vec; -// pub use lazy_hashmap::LazyHashMap; -// pub use lazy_hashset::LazyHashSet; pub use lazy_map::LazyMap; -// pub use lazy_set::LazySet; pub use lazy_vec::LazyVec; use crate::ledger::storage_api; From 5cedd450be759ee2e766f2374332c204a7955608 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 22 Sep 2022 16:53:24 +0000 Subject: [PATCH 80/81] [ci] wasm checksums update --- wasm/checksums.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index d8c5d1223f..2247dd1683 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,19 +1,19 @@ { - "tx_bond.wasm": "tx_bond.76d289c2ba2a1d35d24cbe160e878512ea982cee51f6f3e592f96384683714f1.wasm", - "tx_from_intent.wasm": "tx_from_intent.f8d8ecc1fd565a692b9ca208eebfdab8b4f1a097d58a8604f1c1f92dea795ed4.wasm", - "tx_ibc.wasm": "tx_ibc.417bea698b37f8bf4c55d99df79ea51c10c5b110c25729ac6def4f9bc65c4842.wasm", - "tx_init_account.wasm": "tx_init_account.16404b828089ee1c5a3df94d8ba1d1569974b7d10c00f839ed0894d2ee0ad802.wasm", - "tx_init_nft.wasm": "tx_init_nft.409379e7d008d770dadc3a7c002b25a92e32a34eb195ef01eb0b76ae96db84bf.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.f5b7da549aba6432c2686057f66f550e0881cd114f6192ca0f11fef886bad2cd.wasm", - "tx_init_validator.wasm": "tx_init_validator.7052307657586039fe19cc68f91fb7a3ecfaf34e6a5612ca928f2dfbeb1c2feb.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.4ec445c5108994f2f357c68af5468157f57239a27f04b51c04b5952c09a32f06.wasm", - "tx_transfer.wasm": "tx_transfer.f40c4eac4ed8189e73aa1b19a74b7ec1f18ace52b548b83a28766203d87bf16e.wasm", - "tx_unbond.wasm": "tx_unbond.cb56274fac522963f8c0f70c58a722151d9628ba9e4a7977664b2d6c297ca2e8.wasm", - "tx_update_vp.wasm": "tx_update_vp.685c9eddfee14aeeaf8fadf5cc100be638d0b15488ec20f822ae4b07256bce5c.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.67520bf791c1598f687991622c20f7c0f3142c07382037e225c6d4cdf160165e.wasm", - "tx_withdraw.wasm": "tx_withdraw.1728a2d0fe66a0242816ad0fd8005e09c27a585cb9c9a6f6a1c1c63738aeae8f.wasm", - "vp_nft.wasm": "vp_nft.1d4d3898ae605927af793d50beef268641e1ff8f0e3d91a0b463c5da550fa8aa.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.9440526f730a029d82bd5c5f8eb00fa8f6f03a70aca121d045a1b5f13ad5cf9d.wasm", - "vp_token.wasm": "vp_token.1b5e24692cea7998d3ec8b06ee0c299934c19aeec7543737d94bc803853a477d.wasm", - "vp_user.wasm": "vp_user.09225e083d4f28f93350ce7ff0e67dbae47ea096780a388d0338a0945c3b7222.wasm" + "tx_bond.wasm": "tx_bond.7bfc18f1969d0ba119e7ca2fe00e7ca156ff2b60277a380e97b4cdefeb20ea51.wasm", + "tx_from_intent.wasm": "tx_from_intent.229e4c974480899212c1c30374c1344aa95c0366109ff56b316fcfcc0120c732.wasm", + "tx_ibc.wasm": "tx_ibc.7341c8abc400599cbe4787f7f9fbd84f2ca18959f70d61c2a389f3a4a3ef34d3.wasm", + "tx_init_account.wasm": "tx_init_account.ced4788ea1837d0cacd6ba691f09181555c846f420cb46d32e76cccae9cad0e5.wasm", + "tx_init_nft.wasm": "tx_init_nft.411a1fb5c2f5ef8a9484443fa3f2affddb6b11779c961abc274288e3cd4aba28.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.c3e13023ae8444a10c0fcc50fa27344d6c35367966e41cd1f95a609add0aa26a.wasm", + "tx_init_validator.wasm": "tx_init_validator.707d0798265ba501a813321b5d9a1222bda8448650460e78518e2796f0b42c30.wasm", + "tx_mint_nft.wasm": "tx_mint_nft.0910d261e037e3c0633a3898fc00a08a953d87c8a4b9db2b4041877b91f8317e.wasm", + "tx_transfer.wasm": "tx_transfer.486ffcee9265df25b01751d6007b7f07a083288b985396a8b6fd2aeaacd3e7a8.wasm", + "tx_unbond.wasm": "tx_unbond.dbc10595136c99949a86567561857bb7c465a7a1ea6e21a2b9d261510867ec63.wasm", + "tx_update_vp.wasm": "tx_update_vp.a04692ad8b2715c6262b4e3342591ab7bbb3e6577458979c33d196e8d80146fc.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.25dbae81da4255cae3ffeab6857646af46ef76d70984acfc7c89a3aeb8249679.wasm", + "tx_withdraw.wasm": "tx_withdraw.599ecc125b197b22b27127ce61bc17138a4dd05eb1598a64862496f301c0bc28.wasm", + "vp_nft.wasm": "vp_nft.a7f25944fba3d9a2b00e98482535ed4591282bbf794d64cad18d3c7d15a6318c.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.e28867d79578ce4a32d027b6e50580e8e5e0a19b44c60dc10cacb41dfe07e28c.wasm", + "vp_token.wasm": "vp_token.d24443f5683d0d7d0259dab878f811a56c3d19f3158aecfaae6ce7627cb40884.wasm", + "vp_user.wasm": "vp_user.1aa3756e386a883f523534ac76fb4b75e01c06fcde647c2b6fcca807ba683497.wasm" } \ No newline at end of file From 707db38db5f21674cad24766c4df40a011dc1fff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 22 Sep 2022 18:53:49 +0200 Subject: [PATCH 81/81] changelog: #503 --- .changelog/unreleased/features/503-lazy-vec-and-map.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/features/503-lazy-vec-and-map.md diff --git a/.changelog/unreleased/features/503-lazy-vec-and-map.md b/.changelog/unreleased/features/503-lazy-vec-and-map.md new file mode 100644 index 0000000000..d29ee5fd9c --- /dev/null +++ b/.changelog/unreleased/features/503-lazy-vec-and-map.md @@ -0,0 +1,2 @@ +- Added lazy vector and map data structures for ledger storage + ([#503](https://github.com/anoma/namada/pull/503)) \ No newline at end of file