From 6d00606f715a1bed569928151b63d520fa7846c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 19 Sep 2023 15:21:08 +0100 Subject: [PATCH 1/2] add ibc tx host to workaround M1/2 WASM compilation issue https://github.com/anoma/namada/issues/1831 --- shared/src/vm/host_env.rs | 379 +++++++++++++++++++++++++++++++++ shared/src/vm/mod.rs | 4 +- shared/src/vm/wasm/host_env.rs | 1 + tx_prelude/src/lib.rs | 6 + vm_env/src/lib.rs | 4 + wasm/wasm_source/src/tx_ibc.rs | 12 +- 6 files changed, 400 insertions(+), 6 deletions(-) diff --git a/shared/src/vm/host_env.rs b/shared/src/vm/host_env.rs index 179472a890..b327fee1b2 100644 --- a/shared/src/vm/host_env.rs +++ b/shared/src/vm/host_env.rs @@ -59,6 +59,10 @@ pub enum TxRuntimeError { NumConversionError(TryFromIntError), #[error("Memory error: {0}")] MemoryError(Box), + #[error("Missing tx data")] + MissingTxData, + #[error("IBC: {0}")] + Ibc(#[from] namada_core::ledger::ibc::Error), } type TxResult = std::result::Result; @@ -78,6 +82,7 @@ where } /// A transaction's host context +#[derive(Debug)] pub struct TxCtx<'a, DB, H, CA> where DB: storage::DB + for<'iter> storage::DBIter<'iter>, @@ -1908,6 +1913,34 @@ where Ok(()) } +/// Execute IBC tx. +// Temporarily the IBC tx execution is implemented via a host function to +// workaround wasm issue. +pub fn tx_ibc_execute( + env: &TxVmEnv, +) -> TxResult<()> +where + MEM: VmMemory, + DB: storage::DB + for<'iter> storage::DBIter<'iter>, + H: StorageHasher, + CA: WasmCacheAccess, +{ + use std::cell::RefCell; + use std::rc::Rc; + + use namada_core::ledger::ibc::{IbcActions, TransferModule}; + + let tx_data = unsafe { env.ctx.tx.get().data() } + .ok_or(TxRuntimeError::MissingTxData)?; + let ctx = Rc::new(RefCell::new(env.ctx.clone())); + let mut actions = IbcActions::new(ctx.clone()); + let module = TransferModule::new(ctx); + actions.add_transfer_route(module.module_id(), module); + actions.execute(&tx_data)?; + + Ok(()) +} + /// Validate a VP WASM code hash in a tx environment. fn tx_validate_vp_code_hash( env: &TxVmEnv, @@ -2055,6 +2088,352 @@ where Ok(()) } +// Temp. workaround for +impl<'a, DB, H, CA> namada_core::ledger::ibc::IbcStorageContext + for TxCtx<'a, DB, H, CA> +where + DB: storage::DB + for<'iter> storage::DBIter<'iter>, + H: StorageHasher, + CA: WasmCacheAccess, +{ + type Error = TxRuntimeError; + // type PrefixIter<'iter> = KeyValIterator<(String, Vec)>; + type PrefixIter<'iter> = u64 where Self: 'iter; + + fn read( + &self, + key: &Key, + ) -> std::result::Result>, Self::Error> { + let write_log = unsafe { self.write_log.get() }; + let (log_val, gas) = write_log.read(key); + ibc_tx_charge_gas(self, gas)?; + Ok(match log_val { + Some(write_log::StorageModification::Write { ref value }) => { + Some(value.clone()) + } + Some(&write_log::StorageModification::Delete) => None, + Some(write_log::StorageModification::InitAccount { + ref vp_code_hash, + }) => Some(vp_code_hash.to_vec()), + Some(write_log::StorageModification::Temp { ref value }) => { + Some(value.clone()) + } + None => { + // when not found in write log, try to read from the storage + let storage = unsafe { self.storage.get() }; + let (value, gas) = + storage.read(key).map_err(TxRuntimeError::StorageError)?; + ibc_tx_charge_gas(self, gas)?; + value + } + }) + } + + fn has_key(&self, key: &Key) -> Result { + // try to read from the write log first + let write_log = unsafe { self.write_log.get() }; + let (log_val, gas) = write_log.read(key); + ibc_tx_charge_gas(self, gas)?; + Ok(match log_val { + Some(&write_log::StorageModification::Write { .. }) => true, + Some(&write_log::StorageModification::Delete) => false, + Some(&write_log::StorageModification::InitAccount { .. }) => true, + Some(&write_log::StorageModification::Temp { .. }) => true, + None => { + // when not found in write log, try to check the storage + let storage = unsafe { self.storage.get() }; + let (present, gas) = storage + .has_key(key) + .map_err(TxRuntimeError::StorageError)?; + ibc_tx_charge_gas(self, gas)?; + present + } + }) + } + + fn write( + &mut self, + key: &Key, + data: Vec, + ) -> std::result::Result<(), Self::Error> { + let write_log = unsafe { self.write_log.get() }; + let (gas, _size_diff) = write_log + .write(key, data) + .map_err(TxRuntimeError::StorageModificationError)?; + ibc_tx_charge_gas(self, gas) + } + + fn iter_prefix<'iter>( + &'iter self, + prefix: &Key, + ) -> Result, Self::Error> { + let write_log = unsafe { self.write_log.get() }; + let storage = unsafe { self.storage.get() }; + let (iter, gas) = storage::iter_prefix_post(write_log, storage, prefix); + ibc_tx_charge_gas(self, gas)?; + + let iterators = unsafe { self.iterators.get() }; + Ok(iterators.insert(iter).id()) + } + + fn iter_next<'iter>( + &'iter self, + iter_id: &mut Self::PrefixIter<'iter>, + ) -> Result)>, Self::Error> { + let write_log = unsafe { self.write_log.get() }; + let iterators = unsafe { self.iterators.get() }; + let iter_id = PrefixIteratorId::new(*iter_id); + while let Some((key, val, iter_gas)) = iterators.next(iter_id) { + let (log_val, log_gas) = write_log.read( + &Key::parse(key.clone()) + .map_err(TxRuntimeError::StorageDataError)?, + ); + ibc_tx_charge_gas(self, iter_gas + log_gas)?; + match log_val { + Some(write_log::StorageModification::Write { ref value }) => { + return Ok(Some((key, value.clone()))); + } + Some(&write_log::StorageModification::Delete) => { + // check the next because the key has already deleted + continue; + } + Some(&write_log::StorageModification::InitAccount { + .. + }) => { + // a VP of a new account doesn't need to be iterated + continue; + } + Some(write_log::StorageModification::Temp { ref value }) => { + return Ok(Some((key, value.clone()))); + } + None => { + return Ok(Some((key, val))); + } + } + } + Ok(None) + } + + fn delete(&mut self, key: &Key) -> std::result::Result<(), Self::Error> { + if key.is_validity_predicate().is_some() { + return Err(TxRuntimeError::CannotDeleteVp); + } + + let write_log = unsafe { self.write_log.get() }; + let (gas, _size_diff) = write_log + .delete(key) + .map_err(TxRuntimeError::StorageModificationError)?; + ibc_tx_charge_gas(self, gas) + } + + fn emit_ibc_event( + &mut self, + event: IbcEvent, + ) -> std::result::Result<(), Self::Error> { + let write_log = unsafe { self.write_log.get() }; + let gas = write_log.emit_ibc_event(event); + ibc_tx_charge_gas(self, gas) + } + + fn get_ibc_event( + &self, + event_type: impl AsRef, + ) -> Result, Self::Error> { + let write_log = unsafe { self.write_log.get() }; + Ok(write_log + .get_ibc_events() + .iter() + .find(|event| event.event_type == event_type.as_ref()) + .cloned()) + } + + fn transfer_token( + &mut self, + src: &Address, + dest: &Address, + token: &Address, + amount: namada_core::types::token::DenominatedAmount, + ) -> std::result::Result<(), Self::Error> { + use namada_core::types::token; + + if amount.amount != token::Amount::default() && src != dest { + let src_key = token::balance_key(token, src); + let dest_key = token::balance_key(token, dest); + let src_bal: Option = + ibc_read_borsh(self, &src_key)?; + let mut src_bal = src_bal.unwrap_or_else(|| { + self.log_string(format!("src {} has no balance", src_key)); + unreachable!() + }); + src_bal.spend(&amount.amount); + let mut dest_bal: token::Amount = + ibc_read_borsh(self, &dest_key)?.unwrap_or_default(); + dest_bal.receive(&amount.amount); + ibc_write_borsh(self, &src_key, &src_bal)?; + ibc_write_borsh(self, &dest_key, &dest_bal)?; + } + Ok(()) + } + + fn mint_token( + &mut self, + target: &Address, + token: &Address, + amount: namada_core::types::token::DenominatedAmount, + ) -> Result<(), Self::Error> { + use namada_core::types::token; + + let target_key = token::balance_key(token, target); + let mut target_bal: token::Amount = + ibc_read_borsh(self, &target_key)?.unwrap_or_default(); + target_bal.receive(&amount.amount); + + let minted_key = token::minted_balance_key(token); + let mut minted_bal: token::Amount = + ibc_read_borsh(self, &minted_key)?.unwrap_or_default(); + minted_bal.receive(&amount.amount); + + ibc_write_borsh(self, &target_key, &target_bal)?; + ibc_write_borsh(self, &minted_key, &minted_bal)?; + + let minter_key = token::minter_key(token); + ibc_write_borsh( + self, + &minter_key, + &Address::Internal(address::InternalAddress::Ibc), + )?; + + Ok(()) + } + + fn burn_token( + &mut self, + target: &Address, + token: &Address, + amount: namada_core::types::token::DenominatedAmount, + ) -> Result<(), Self::Error> { + use namada_core::types::token; + + let target_key = token::balance_key(token, target); + let mut target_bal: token::Amount = + ibc_read_borsh(self, &target_key)?.unwrap_or_default(); + target_bal.spend(&amount.amount); + + // burn the minted amount + let minted_key = token::minted_balance_key(token); + let mut minted_bal: token::Amount = + ibc_read_borsh(self, &minted_key)?.unwrap_or_default(); + minted_bal.spend(&amount.amount); + + ibc_write_borsh(self, &target_key, &target_bal)?; + ibc_write_borsh(self, &minted_key, &minted_bal)?; + + Ok(()) + } + + fn get_height(&self) -> std::result::Result { + let storage = unsafe { self.storage.get() }; + let (height, gas) = storage.get_block_height(); + ibc_tx_charge_gas(self, gas)?; + Ok(height) + } + + fn get_header( + &self, + height: BlockHeight, + ) -> std::result::Result< + Option, + Self::Error, + > { + let storage = unsafe { self.storage.get() }; + let (header, gas) = storage + .get_block_header(Some(height)) + .map_err(TxRuntimeError::StorageError)?; + ibc_tx_charge_gas(self, gas)?; + Ok(header) + } + + fn log_string(&self, message: String) { + tracing::info!("IBC host env log: {}", message); + } +} + +/// Add a gas cost incured in a transaction +// Temp helper. +fn ibc_tx_charge_gas<'a, DB, H, CA>( + ctx: &TxCtx<'a, DB, H, CA>, + used_gas: u64, +) -> TxResult<()> +where + DB: storage::DB + for<'iter> storage::DBIter<'iter>, + H: StorageHasher, + CA: WasmCacheAccess, +{ + let gas_meter = unsafe { ctx.gas_meter.get() }; + // if we run out of gas, we need to stop the execution + let result = gas_meter + .consume(used_gas) + .map_err(TxRuntimeError::OutOfGas); + if let Err(err) = &result { + tracing::info!( + "Stopping transaction execution because of gas error: {}", + err + ); + } + result +} + +/// Read borsh encoded val by key. +// Temp helper for ibc tx workaround. +fn ibc_read_borsh<'a, T, DB, H, CA>( + ctx: &TxCtx<'a, DB, H, CA>, + key: &Key, +) -> TxResult> +where + T: BorshDeserialize, + DB: storage::DB + for<'iter> storage::DBIter<'iter>, + H: StorageHasher, + CA: WasmCacheAccess, +{ + let bytes = namada_core::ledger::ibc::IbcStorageContext::read(ctx, key)?; + match bytes { + Some(bytes) => { + let val = T::try_from_slice(&bytes) + .map_err(TxRuntimeError::EncodingError)?; + Ok(Some(val)) + } + None => Ok(None), + } +} + +/// Write borsh encoded val by key. +// Temp helper for ibc tx workaround. +fn ibc_write_borsh<'a, T, DB, H, CA>( + ctx: &mut TxCtx<'a, DB, H, CA>, + key: &Key, + val: &T, +) -> TxResult<()> +where + T: BorshSerialize, + DB: storage::DB + for<'iter> storage::DBIter<'iter>, + H: StorageHasher, + CA: WasmCacheAccess, +{ + let bytes = val.try_to_vec().map_err(TxRuntimeError::EncodingError)?; + namada_core::ledger::ibc::IbcStorageContext::write(ctx, key, bytes)?; + Ok(()) +} + +// Temp. workaround for +impl<'a, DB, H, CA> namada_core::ledger::ibc::IbcCommonContext + for TxCtx<'a, DB, H, CA> +where + DB: storage::DB + for<'iter> storage::DBIter<'iter>, + H: StorageHasher, + CA: WasmCacheAccess, +{ +} + /// A helper module for testing #[cfg(feature = "testing")] pub mod testing { diff --git a/shared/src/vm/mod.rs b/shared/src/vm/mod.rs index 860305d565..e7aeeaa6cb 100644 --- a/shared/src/vm/mod.rs +++ b/shared/src/vm/mod.rs @@ -77,7 +77,7 @@ impl WasmCacheAccess for WasmCacheRoAccess { /// reference, so the access is thread-safe, but because of the unsafe /// reference conversion, care must be taken that while this reference is /// borrowed, no other process can modify it. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct HostRef<'a, T: 'a> { data: *const c_void, phantom: PhantomData<&'a T>, @@ -154,7 +154,7 @@ impl<'a, T: 'a> HostSlice<'a, &[T]> { /// which is used for implementing some host calls. Because it's mutable, it's /// not thread-safe. Also, care must be taken that while this reference is /// borrowed, no other process can read or modify it. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct MutHostRef<'a, T: 'a> { data: *mut c_void, phantom: PhantomData<&'a T>, diff --git a/shared/src/vm/wasm/host_env.rs b/shared/src/vm/wasm/host_env.rs index f31832b9bd..7908d2e6b6 100644 --- a/shared/src/vm/wasm/host_env.rs +++ b/shared/src/vm/wasm/host_env.rs @@ -85,6 +85,7 @@ where "namada_tx_get_block_epoch" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_get_block_epoch), "namada_tx_get_native_token" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_get_native_token), "namada_tx_log_string" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_log_string), + "namada_tx_ibc_execute" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_ibc_execute), }, } } diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index 60e33113f0..87d91e07a3 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -351,3 +351,9 @@ impl TxEnv for Ctx { } } } + +/// Execute IBC tx. +// Temp. workaround for +pub fn tx_ibc_execute() { + unsafe { namada_tx_ibc_execute() } +} diff --git a/vm_env/src/lib.rs b/vm_env/src/lib.rs index 099460258b..c110332741 100644 --- a/vm_env/src/lib.rs +++ b/vm_env/src/lib.rs @@ -110,6 +110,10 @@ pub mod tx { /// Charge the provided amount of gas for the current tx pub fn namada_tx_charge_gas(used_gas: u64); + + /// Execute IBC tx. + // Temp. workaround for + pub fn namada_tx_ibc_execute(); } } diff --git a/wasm/wasm_source/src/tx_ibc.rs b/wasm/wasm_source/src/tx_ibc.rs index ebcb529842..5a8bac9d4e 100644 --- a/wasm/wasm_source/src/tx_ibc.rs +++ b/wasm/wasm_source/src/tx_ibc.rs @@ -6,9 +6,13 @@ use namada_tx_prelude::*; #[transaction(gas = 1240000)] -fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { - let signed = tx_data; - let data = signed.data().ok_or_err_msg("Missing data")?; +fn apply_tx(_ctx: &mut Ctx, _tx_data: Tx) -> TxResult { + // let signed = tx_data; + // let data = signed.data().ok_or_err_msg("Missing data")?; - ibc::ibc_actions(ctx).execute(&data).into_storage_result() + // ibc::ibc_actions(ctx).execute(&data).into_storage_result() + + // Temp. workaround for + tx_ibc_execute(); + Ok(()) } From 997ee851e29a2adc2d51e83cebb64fd609813fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 19 Sep 2023 15:26:26 +0100 Subject: [PATCH 2/2] changelog: #1904 --- .changelog/unreleased/bug-fixes/1904-ibc-host-fn-workaround.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/bug-fixes/1904-ibc-host-fn-workaround.md diff --git a/.changelog/unreleased/bug-fixes/1904-ibc-host-fn-workaround.md b/.changelog/unreleased/bug-fixes/1904-ibc-host-fn-workaround.md new file mode 100644 index 0000000000..d98885fdc4 --- /dev/null +++ b/.changelog/unreleased/bug-fixes/1904-ibc-host-fn-workaround.md @@ -0,0 +1,2 @@ +- Implement IBC tx execution via a native host function to workaround Mac M1/2 + WASM compilation issues. ([\#1904](https://github.com/anoma/namada/pull/1904)) \ No newline at end of file