From 5d2cf69fbf90163c0ea68e2aaceb2443f104d6f1 Mon Sep 17 00:00:00 2001 From: Will Spencer Date: Thu, 12 Mar 2026 10:02:38 -0400 Subject: [PATCH 1/4] Refactor storage APIs --- runtime/plaid/src/functions/storage.rs | 827 ------------------ runtime/plaid/src/functions/storage/delete.rs | 197 +++++ runtime/plaid/src/functions/storage/get.rs | 148 ++++ runtime/plaid/src/functions/storage/insert.rs | 299 +++++++ runtime/plaid/src/functions/storage/list.rs | 169 ++++ runtime/plaid/src/functions/storage/mod.rs | 45 + 6 files changed, 858 insertions(+), 827 deletions(-) delete mode 100644 runtime/plaid/src/functions/storage.rs create mode 100644 runtime/plaid/src/functions/storage/delete.rs create mode 100644 runtime/plaid/src/functions/storage/get.rs create mode 100644 runtime/plaid/src/functions/storage/insert.rs create mode 100644 runtime/plaid/src/functions/storage/list.rs create mode 100644 runtime/plaid/src/functions/storage/mod.rs diff --git a/runtime/plaid/src/functions/storage.rs b/runtime/plaid/src/functions/storage.rs deleted file mode 100644 index 689ca63f..00000000 --- a/runtime/plaid/src/functions/storage.rs +++ /dev/null @@ -1,827 +0,0 @@ -use std::sync::{Arc, RwLock}; - -use wasmer::{AsStoreRef, FunctionEnvMut, MemoryView, WasmPtr}; - -use crate::{executor::Env, functions::FunctionErrors, loader::LimitValue, storage::Storage}; - -use super::{ - calculate_max_buffer_size, get_memory, safely_get_memory, safely_get_string, - safely_write_data_back, -}; - -macro_rules! safely_get_guest_string { - ($variable:ident, $memory_view:expr, $buf:expr, $buf_len:expr, $env_data:expr) => { - let $variable = match safely_get_string(&$memory_view, $buf, $buf_len) { - Ok(s) => s, - Err(e) => { - error!( - "{}: error while getting a string from guest memory: {:?}", - $env_data.module.name, e - ); - return FunctionErrors::ParametersNotUtf8 as i32; - } - }; - }; -} - -macro_rules! safely_get_guest_memory { - ($variable:ident, $memory_view:expr, $buf:expr, $buf_len:expr, $env_data:expr) => { - let max_buffer_size = calculate_max_buffer_size($env_data.module.page_limit); - let $variable = match safely_get_memory(&$memory_view, $buf, $buf_len, max_buffer_size) { - Ok(d) => d, - Err(e) => { - error!( - "{}: error while getting bytes from guest memory: {:?}", - $env_data.module.name, e - ); - return FunctionErrors::ParametersNotUtf8 as i32; - } - }; - }; -} - -/// Code which is common to `insert` and `insert_shared` -fn insert_common( - env_data: &Env, - storage: &Arc, - namespace: String, - key: String, - value: Vec, - memory_view: MemoryView, - data_buffer: WasmPtr, - data_buffer_len: u32, - storage_limit: LimitValue, - storage_counter: &Arc>, -) -> i32 { - // ugly, but we are dealing with a couple of "async move"s - let storage_key = key.clone(); - let namespace_clone = namespace.clone(); - - // The insertion proceeds differently depending on whether the storage is limited or not. - let insertion_result = match storage_limit { - LimitValue::Unlimited => { - // The storage is unlimited, so we don't check / update any counters and just proceed with the operation - env_data - .api - .clone() - .runtime - .block_on(async move { storage.insert(namespace, storage_key, value).await }) - } - LimitValue::Limited(storage_limit) => { - // The storage is limited, so we need to check / update counters (with locks) because the operation might have to be rejected. - - // Get a lock on the storage counter. - // This ensures no race conditions if multiple instances of the same module are running in parallel. - // The guard will go out of scope at the end of this block, thus releasing the lock. After this block, we won't touch the counter again. - let mut storage_current = match storage_counter.write() { - Ok(g) => g, - Err(e) => { - error!("Critical error getting a lock on used storage: {:?}", e); - return FunctionErrors::InternalApiError as i32; - } - }; - - // We check if this insert would overwrite some existing data. If so, we need to take that into account when - // computing the storage that would be occupied at the end of the insert operation. - // Note: if we have existing data, then we need to count the key's length as well. This is because at the end - // of a possible insertion, we would have only one key. - let get_key = key.clone(); - let key_len = key.as_bytes().len(); - let existing_data_size = match env_data - .api - .clone() - .runtime - .block_on(async move { storage.get(&namespace, &get_key).await }) - { - Ok(data) => match data { - None => 0u64, - Some(d) => d.len() as u64 + key_len as u64, - }, - Err(_) => { - return FunctionErrors::InternalApiError as i32; - } - }; - - // Calculate the amount of storage that would be used after successfully inserting. - // Note: we _substract_ the size of existing data. If we were to insert the new data, the old data would be overwritten. - let would_be_used_storage = - *storage_current + key_len as u64 + value.len() as u64 - existing_data_size; - // no problem with underflowing because the result will never be negative (since *storage_current >= existing_data_size) - - // If we would go above the limited storage, reject the insert - if would_be_used_storage > storage_limit { - error!("{}: Could not insert key/value with key [{key}] as that would bring us above the configured storage limit.", env_data.module.name); - let _ = env_data.external_logging_system.log_module_error( - env_data.module.name.clone(), - "Could not insert key/value as that would bring us above the configured storage limit.".to_string(), - vec![] - ); - return FunctionErrors::StorageLimitReached as i32; - } - - let result = - env_data.api.clone().runtime.block_on(async move { - storage.insert(namespace_clone, storage_key, value).await - }); - // If the insertion went well, update counter for used storage. - // If the insertion failed for some reason, we don't update the counter and release the lock: no harm done. - if result.is_ok() { - *storage_current = would_be_used_storage; - } - result - } - }; - - // Process the insertion result and return info to the caller - match insertion_result { - Ok(data) => { - match data { - Some(data) => { - // If the data is too large to fit in the buffer that was passed to us. Unfortunately this is a somewhat - // unrecoverable state because we've overwritten the value already. We could fail insertion if the data - // buffer passed is too small in future? That would mean doing a get call first, which the client can do - // too. - match safely_write_data_back(&memory_view, &data, data_buffer, data_buffer_len) - { - Ok(x) => x, - Err(e) => { - error!( - "{}: Data write error in storage_insert: {:?}", - env_data.module.name, e - ); - e as i32 - } - } - } - // This occurs when there is no such key so the number of bytes that have been copied back are 0 - None => 0, - } - } - // If the storage system errors (for example a network problem if using a networked storage provider) - // the error is made opaque to the client here and we log what happened - Err(e) => { - error!( - "There was a storage system error when key [{key}] was accessed by [{}]: {e}", - env_data.module.name - ); - return FunctionErrors::InternalApiError as i32; - } - } -} - -/// Store data in the storage system if one is configured -pub fn insert( - env: FunctionEnvMut, - key_buf: WasmPtr, - key_buf_len: u32, - value_buf: WasmPtr, - value_buf_len: u32, - data_buffer: WasmPtr, - data_buffer_len: u32, -) -> i32 { - let store = env.as_store_ref(); - let env_data = env.data(); - - let storage = if let Some(storage) = &env_data.storage { - storage - } else { - return FunctionErrors::ApiNotConfigured as i32; - }; - - let memory_view = match get_memory(&env, &store) { - Ok(memory_view) => memory_view, - Err(e) => { - error!( - "{}: Memory error in storage_insert: {:?}", - env_data.module.name, e - ); - return FunctionErrors::CouldNotGetAdequateMemory as i32; - } - }; - - safely_get_guest_string!(key, memory_view, key_buf, key_buf_len, env_data); - safely_get_guest_memory!(value, memory_view, value_buf, value_buf_len, env_data); - - insert_common( - env_data, - storage, - env_data.module.name.clone(), - key, - value, - memory_view, - data_buffer, - data_buffer_len, - env_data.module.storage_limit.clone(), - &env_data.module.storage_current, - ) -} - -/// Store data in a shared namespace in the storage system, if one is configured -pub fn insert_shared( - env: FunctionEnvMut, - namespace_buf: WasmPtr, - namespace_buf_len: u32, - key_buf: WasmPtr, - key_buf_len: u32, - value_buf: WasmPtr, - value_buf_len: u32, - data_buffer: WasmPtr, - data_buffer_len: u32, -) -> i32 { - let store = env.as_store_ref(); - let env_data = env.data(); - - let storage = if let Some(storage) = &env_data.storage { - storage - } else { - return FunctionErrors::ApiNotConfigured as i32; - }; - - // Check if we have shared DBs at all, otherwise we just stop - let shared_dbs = match &storage.shared_dbs { - None => { - return FunctionErrors::OperationNotAllowed as i32; - } - Some(x) => x, - }; - - let memory_view = match get_memory(&env, &store) { - Ok(memory_view) => memory_view, - Err(e) => { - error!( - "{}: Memory error in storage_insert_shared: {:?}", - env_data.module.name, e - ); - return FunctionErrors::CouldNotGetAdequateMemory as i32; - } - }; - - safely_get_guest_string!( - namespace, - memory_view, - namespace_buf, - namespace_buf_len, - env_data - ); - - // Check if we can access this namespace, otherwise we just stop - let allowed = match shared_dbs.get(&namespace) { - None => false, - Some(db) => db.config.rw.contains(&env_data.module.name), - }; - if !allowed { - return FunctionErrors::OperationNotAllowed as i32; - } - - safely_get_guest_string!(key, memory_view, key_buf, key_buf_len, env_data); - safely_get_guest_memory!(value, memory_view, value_buf, value_buf_len, env_data); - - // Get storage limit and counter for the shared DB - let (storage_limit, storage_current) = match &storage.shared_dbs { - None => { - return FunctionErrors::SharedDbError as i32; - } - Some(shared_dbs) => match shared_dbs.get(&namespace) { - None => { - return FunctionErrors::SharedDbError as i32; - } - Some(db) => (db.config.size_limit.clone(), db.used_storage.clone()), - }, - }; - - insert_common( - env_data, - storage, - namespace, - key, - value, - memory_view, - data_buffer, - data_buffer_len, - storage_limit, - &storage_current, - ) -} - -/// Code which is common to `get` and `get_shared` -fn get_common( - env_data: &Env, - storage: &Arc, - namespace: &str, - key: &str, - memory_view: MemoryView, - data_buffer: WasmPtr, - data_buffer_len: u32, -) -> i32 { - let result = env_data - .api - .clone() - .runtime - .block_on(async move { storage.get(namespace, key).await }); - - match result { - Ok(Some(data)) => { - match safely_write_data_back(&memory_view, &data, data_buffer, data_buffer_len) { - Ok(x) => x, - Err(e) => { - error!( - "{}: Data write error in storage_get: {:?}", - env_data.module.name, e - ); - e as i32 - } - } - } - Ok(None) => 0, - Err(_) => FunctionErrors::InternalApiError as i32, - } -} - -/// Get data from the storage system if one is configured -pub fn get( - env: FunctionEnvMut, - key_buf: WasmPtr, - key_buf_len: u32, - data_buffer: WasmPtr, - data_buffer_len: u32, -) -> i32 { - let store = env.as_store_ref(); - let env_data = env.data(); - - let storage = if let Some(storage) = &env_data.storage { - storage - } else { - return FunctionErrors::ApiNotConfigured as i32; - }; - - let memory_view = match get_memory(&env, &store) { - Ok(memory_view) => memory_view, - Err(e) => { - error!( - "{}: Memory error in storage_get: {:?}", - env_data.module.name, e - ); - return FunctionErrors::CouldNotGetAdequateMemory as i32; - } - }; - - safely_get_guest_string!(key, memory_view, key_buf, key_buf_len, env_data); - - get_common( - env_data, - storage, - &env_data.module.name, - &key, - memory_view, - data_buffer, - data_buffer_len, - ) -} - -/// Get data from a shared namespace in the storage system, if one is configured -pub fn get_shared( - env: FunctionEnvMut, - namespace_buf: WasmPtr, - namespace_buf_len: u32, - key_buf: WasmPtr, - key_buf_len: u32, - data_buffer: WasmPtr, - data_buffer_len: u32, -) -> i32 { - let store = env.as_store_ref(); - let env_data = env.data(); - - let storage = if let Some(storage) = &env_data.storage { - storage - } else { - return FunctionErrors::ApiNotConfigured as i32; - }; - - // Check if we have shared DBs at all, otherwise we just stop - let shared_dbs = match &storage.shared_dbs { - None => { - return FunctionErrors::OperationNotAllowed as i32; - } - Some(x) => x, - }; - - let memory_view = match get_memory(&env, &store) { - Ok(memory_view) => memory_view, - Err(e) => { - error!( - "{}: Memory error in storage_get_shared: {:?}", - env_data.module.name, e - ); - return FunctionErrors::CouldNotGetAdequateMemory as i32; - } - }; - - safely_get_guest_string!( - namespace, - memory_view, - namespace_buf, - namespace_buf_len, - env_data - ); - - // Check if we can access this namespace, otherwise we just stop - let allowed = match shared_dbs.get(&namespace) { - None => false, - Some(db) => { - db.config.r.contains(&env_data.module.name) - || db.config.rw.contains(&env_data.module.name) - } - }; - if !allowed { - return FunctionErrors::OperationNotAllowed as i32; - } - - safely_get_guest_string!(key, memory_view, key_buf, key_buf_len, env_data); - - get_common( - env_data, - storage, - &namespace, - &key, - memory_view, - data_buffer, - data_buffer_len, - ) -} - -/// Fetch all the keys from the storage system and filter for a prefix -/// before returning the data. -pub fn list_keys( - env: FunctionEnvMut, - prefix_buf: WasmPtr, - prefix_buf_len: u32, - data_buffer: WasmPtr, - data_buffer_len: u32, -) -> i32 { - let store = env.as_store_ref(); - let env_data = env.data(); - - let storage = if let Some(storage) = &env_data.storage { - storage - } else { - return FunctionErrors::ApiNotConfigured as i32; - }; - - let memory_view = match get_memory(&env, &store) { - Ok(memory_view) => memory_view, - Err(e) => { - error!( - "{}: Memory error in storage_list_keys: {:?}", - env_data.module.name, e - ); - return FunctionErrors::CouldNotGetAdequateMemory as i32; - } - }; - - safely_get_guest_string!(prefix, memory_view, prefix_buf, prefix_buf_len, env_data); - - list_keys_common( - env_data, - storage, - env_data.module.name.clone(), - prefix, - memory_view, - data_buffer, - data_buffer_len, - ) -} - -/// Fetch all the keys from a shared namespace in the storage system and filter for a prefix -/// before returning the data. -pub fn list_keys_shared( - env: FunctionEnvMut, - namespace_buf: WasmPtr, - namespace_buf_len: u32, - prefix_buf: WasmPtr, - prefix_buf_len: u32, - data_buffer: WasmPtr, - data_buffer_len: u32, -) -> i32 { - let store = env.as_store_ref(); - let env_data = env.data(); - - let storage = if let Some(storage) = &env_data.storage { - storage - } else { - return FunctionErrors::ApiNotConfigured as i32; - }; - - // Check if we have shared DBs at all, otherwise we just stop - let shared_dbs = match &storage.shared_dbs { - None => { - return FunctionErrors::OperationNotAllowed as i32; - } - Some(x) => x, - }; - - let memory_view = match get_memory(&env, &store) { - Ok(memory_view) => memory_view, - Err(e) => { - error!( - "{}: Memory error in storage_list_keys: {:?}", - env_data.module.name, e - ); - return FunctionErrors::CouldNotGetAdequateMemory as i32; - } - }; - - safely_get_guest_string!( - namespace, - memory_view, - namespace_buf, - namespace_buf_len, - env_data - ); - - // Check if we can access this namespace, otherwise we just stop - let allowed = match shared_dbs.get(&namespace) { - None => false, - Some(db) => { - db.config.r.contains(&env_data.module.name) - || db.config.rw.contains(&env_data.module.name) - } - }; - if !allowed { - return FunctionErrors::OperationNotAllowed as i32; - } - - safely_get_guest_string!(prefix, memory_view, prefix_buf, prefix_buf_len, env_data); - - list_keys_common( - env_data, - storage, - namespace, - prefix, - memory_view, - data_buffer, - data_buffer_len, - ) -} - -/// Code which is common to `list_keys` and `list_keys_shared` -pub fn list_keys_common( - env_data: &Env, - storage: &Arc, - namespace: String, - prefix: String, - memory_view: MemoryView, - data_buffer: WasmPtr, - data_buffer_len: u32, -) -> i32 { - let result = env_data - .api - .clone() - .runtime - .block_on(async move { storage.list_keys(&namespace, Some(prefix.as_str())).await }); - - match result { - Ok(keys) => { - let serialized_keys = match serde_json::to_string(&keys) { - Ok(sk) => sk, - Err(e) => { - error!( - "Could not serialize keys for namespaces {}: {e}", - env_data.module.name - ); - return FunctionErrors::ErrorCouldNotSerialize as i32; - } - }; - - match safely_write_data_back( - &memory_view, - &serialized_keys.as_bytes(), - data_buffer, - data_buffer_len, - ) { - Ok(x) => x, - Err(e) => { - error!( - "{}: Data write error in storage_list: {:?}", - env_data.module.name, e - ); - e as i32 - } - } - } - Err(e) => { - error!( - "Could not list keys for namespace {}: {e}", - &env_data.module.name - ); - return FunctionErrors::InternalApiError as i32; - } - } -} - -/// Code which is common to `delete` and `delete_shared` -pub fn delete_common( - env_data: &Env, - storage: &Arc, - namespace: String, - key: String, - memory_view: MemoryView, - data_buffer: WasmPtr, - data_buffer_len: u32, - storage_limit: LimitValue, - storage_counter: &Arc>, -) -> i32 { - let deletion_result = match data_buffer_len { - // This is a call just to get the size of the buffer, so we do storage.get and don't mess with storage counters - 0 => env_data - .api - .clone() - .runtime - .block_on(async move { storage.get(&namespace, &key).await }), - // This is a call to delete the value, so we will do storage.delete, but first we need to check the storage limit - _ => match storage_limit { - LimitValue::Unlimited => { - // The storage is unlimited, so we don't update any counters and just proceed with the operation - env_data - .api - .clone() - .runtime - .block_on(async move { storage.delete(&namespace, &key).await }) - } - LimitValue::Limited(_) => { - // for the "async move" - let storage_key = key.clone(); - - // The storage is limited, so we need to update counters (with locks) - - // Get a lock on the storage counter. - // This ensures no race conditions if multiple instances of the same module are running in parallel. - // The guard will go out of scope at the end of this block, thus releasing the lock. After this block, we won't touch the counter again. - let mut storage_current = match storage_counter.write() { - Ok(g) => g, - Err(e) => { - error!("Critical error getting a lock on used storage: {:?}", e); - return FunctionErrors::InternalApiError as i32; - } - }; - - let result = env_data - .api - .clone() - .runtime - .block_on(async move { storage.delete(&namespace, &storage_key).await }); - // If the deletion went well, update counter for used storage. - // If the deletion failed for some reason, we don't update the counter and release the lock: no harm done. - if let Ok(Some(ref data)) = result { - let key_len = key.as_bytes().len() as u64; - *storage_current = *storage_current - key_len - data.len() as u64; - } - result - } - }, - }; - - // Process the deletion result and return info to the caller - match deletion_result { - Ok(data) => match data { - Some(data) => { - match safely_write_data_back(&memory_view, &data, data_buffer, data_buffer_len) { - Ok(x) => x, - Err(e) => { - error!( - "{}: Data write error in storage_delete: {:?}", - env_data.module.name, e - ); - e as i32 - } - } - } - None => 0, - }, - Err(_) => FunctionErrors::InternalApiError as i32, - } -} - -/// Delete data from the storage system if one is configured -pub fn delete( - env: FunctionEnvMut, - key_buf: WasmPtr, - key_buf_len: u32, - data_buffer: WasmPtr, - data_buffer_len: u32, -) -> i32 { - let store = env.as_store_ref(); - let env_data = env.data(); - - let storage = if let Some(storage) = &env_data.storage { - storage - } else { - return FunctionErrors::ApiNotConfigured as i32; - }; - - let memory_view = match get_memory(&env, &store) { - Ok(memory_view) => memory_view, - Err(e) => { - error!( - "{}: Memory error in storage_delete: {:?}", - env_data.module.name, e - ); - return FunctionErrors::CouldNotGetAdequateMemory as i32; - } - }; - - safely_get_guest_string!(key, memory_view, key_buf, key_buf_len, env_data); - - delete_common( - env_data, - storage, - env_data.module.name.clone(), - key, - memory_view, - data_buffer, - data_buffer_len, - env_data.module.storage_limit.clone(), - &env_data.module.storage_current, - ) -} - -/// Delete data from a shared namespace in the storage system, if one is configured -pub fn delete_shared( - env: FunctionEnvMut, - namespace_buf: WasmPtr, - namespace_buf_len: u32, - key_buf: WasmPtr, - key_buf_len: u32, - data_buffer: WasmPtr, - data_buffer_len: u32, -) -> i32 { - let store = env.as_store_ref(); - let env_data = env.data(); - - let storage = if let Some(storage) = &env_data.storage { - storage - } else { - return FunctionErrors::ApiNotConfigured as i32; - }; - - // Check if we have shared DBs at all, otherwise we just stop - let shared_dbs = match &storage.shared_dbs { - None => { - return FunctionErrors::OperationNotAllowed as i32; - } - Some(x) => x, - }; - - let memory_view = match get_memory(&env, &store) { - Ok(memory_view) => memory_view, - Err(e) => { - error!( - "{}: Memory error in storage_delete: {:?}", - env_data.module.name, e - ); - return FunctionErrors::CouldNotGetAdequateMemory as i32; - } - }; - - safely_get_guest_string!( - namespace, - memory_view, - namespace_buf, - namespace_buf_len, - env_data - ); - safely_get_guest_string!(key, memory_view, key_buf, key_buf_len, env_data); - - // Check if we can access this namespace, otherwise we just stop - let allowed = match shared_dbs.get(&namespace) { - None => false, - Some(db) => db.config.rw.contains(&env_data.module.name), - }; - if !allowed { - return FunctionErrors::OperationNotAllowed as i32; - } - - // Get storage limit and counter for the shared DB - let (storage_limit, storage_current) = match &storage.shared_dbs { - None => { - return FunctionErrors::ApiNotConfigured as i32; - } - Some(shared_dbs) => match shared_dbs.get(&namespace) { - None => { - return FunctionErrors::ApiNotConfigured as i32; - } - Some(db) => (db.config.size_limit.clone(), db.used_storage.clone()), - }, - }; - - delete_common( - env_data, - storage, - namespace, - key, - memory_view, - data_buffer, - data_buffer_len, - storage_limit, - &storage_current, - ) -} diff --git a/runtime/plaid/src/functions/storage/delete.rs b/runtime/plaid/src/functions/storage/delete.rs new file mode 100644 index 00000000..a54c5cf9 --- /dev/null +++ b/runtime/plaid/src/functions/storage/delete.rs @@ -0,0 +1,197 @@ +use std::sync::{Arc, RwLock}; + +use wasmer::{AsStoreRef, FunctionEnvMut, MemoryView, WasmPtr}; + +use crate::{executor::Env, functions::FunctionErrors, loader::LimitValue, storage::Storage}; + +use super::{get_memory, safely_get_string, safely_write_data_back}; + +/// Delete data from the storage system if one is configured +pub fn delete( + env: FunctionEnvMut, + key_buf: WasmPtr, + key_buf_len: u32, + data_buffer: WasmPtr, + data_buffer_len: u32, +) -> i32 { + let store = env.as_store_ref(); + let env_data = env.data(); + + let Some(storage) = &env_data.storage else { + return FunctionErrors::ApiNotConfigured as i32; + }; + + let memory_view = match get_memory(&env, &store) { + Ok(memory_view) => memory_view, + Err(e) => { + error!( + "{}: Memory error in storage_delete: {e:?}", + env_data.module.name, + ); + return FunctionErrors::CouldNotGetAdequateMemory as i32; + } + }; + + safely_get_guest_string!(key, memory_view, key_buf, key_buf_len, env_data); + + delete_common( + env_data, + storage, + env_data.module.name.clone(), + key, + memory_view, + data_buffer, + data_buffer_len, + env_data.module.storage_limit.clone(), + env_data.module.storage_current.clone(), + ) +} + +/// Delete data from a shared namespace in the storage system, if one is configured +pub fn delete_shared( + env: FunctionEnvMut, + namespace_buf: WasmPtr, + namespace_buf_len: u32, + key_buf: WasmPtr, + key_buf_len: u32, + data_buffer: WasmPtr, + data_buffer_len: u32, +) -> i32 { + let store = env.as_store_ref(); + let env_data = env.data(); + + let Some(storage) = &env_data.storage else { + return FunctionErrors::ApiNotConfigured as i32; + }; + + // Check if we have shared DBs at all, otherwise we just stop + let Some(shared_dbs) = &storage.shared_dbs else { + return FunctionErrors::OperationNotAllowed as i32; + }; + + let memory_view = match get_memory(&env, &store) { + Ok(memory_view) => memory_view, + Err(e) => { + error!( + "{}: Memory error in storage_delete: {e:?}", + env_data.module.name, + ); + return FunctionErrors::CouldNotGetAdequateMemory as i32; + } + }; + + safely_get_guest_string!( + namespace, + memory_view, + namespace_buf, + namespace_buf_len, + env_data + ); + safely_get_guest_string!(key, memory_view, key_buf, key_buf_len, env_data); + + // Check if we can access this namespace, otherwise we just stop + // Get the shared DB, if it exists. Otherwise, exit with an error + let Some(db) = shared_dbs.get(&namespace) else { + return FunctionErrors::SharedDbError as i32; + }; + + // Check if calling module has permission to write to the DB + if !db.config.rw.contains(&env_data.module.name) { + return FunctionErrors::OperationNotAllowed as i32; + } + + delete_common( + env_data, + storage, + namespace, + key, + memory_view, + data_buffer, + data_buffer_len, + db.config.size_limit.clone(), + db.used_storage.clone(), + ) +} + +/// Code which is common to `delete` and `delete_shared` +fn delete_common( + env_data: &Env, + storage: &Arc, + namespace: String, + key: String, + memory_view: MemoryView, + data_buffer: WasmPtr, + data_buffer_len: u32, + storage_limit: LimitValue, + storage_counter: Arc>, +) -> i32 { + let deletion_result = match data_buffer_len { + // This is a call just to get the size of the buffer, so we do storage.get and don't mess with storage counters + 0 => env_data + .api + .clone() + .runtime + .block_on(async move { storage.get(&namespace, &key).await }), + // This is a call to delete the value, so we will do storage.delete, but first we need to check the storage limit + _ => match storage_limit { + LimitValue::Unlimited => { + // The storage is unlimited, so we don't update any counters and just proceed with the operation + env_data + .api + .clone() + .runtime + .block_on(async move { storage.delete(&namespace, &key).await }) + } + LimitValue::Limited(_) => { + // for the "async move" + let storage_key = key.clone(); + + // The storage is limited, so we need to update counters (with locks) + + // Get a lock on the storage counter. + // This ensures no race conditions if multiple instances of the same module are running in parallel. + // The guard will go out of scope at the end of this block, thus releasing the lock. After this block, we won't touch the counter again. + let mut storage_current = match storage_counter.write() { + Ok(g) => g, + Err(e) => { + error!("Critical error getting a lock on used storage: {:?}", e); + return FunctionErrors::InternalApiError as i32; + } + }; + + let result = env_data + .api + .clone() + .runtime + .block_on(async move { storage.delete(&namespace, &storage_key).await }); + // If the deletion went well, update counter for used storage. + // If the deletion failed for some reason, we don't update the counter and release the lock: no harm done. + if let Ok(Some(ref data)) = result { + let key_len = key.as_bytes().len() as u64; + *storage_current = *storage_current - key_len - data.len() as u64; + } + result + } + }, + }; + + // Process the deletion result and return info to the caller + match deletion_result { + Ok(data) => match data { + Some(data) => { + match safely_write_data_back(&memory_view, &data, data_buffer, data_buffer_len) { + Ok(x) => x, + Err(e) => { + error!( + "{}: Data write error in storage_delete: {:?}", + env_data.module.name, e + ); + e as i32 + } + } + } + None => 0, + }, + Err(_) => FunctionErrors::InternalApiError as i32, + } +} diff --git a/runtime/plaid/src/functions/storage/get.rs b/runtime/plaid/src/functions/storage/get.rs new file mode 100644 index 00000000..bde5c3fe --- /dev/null +++ b/runtime/plaid/src/functions/storage/get.rs @@ -0,0 +1,148 @@ +use std::sync::Arc; + +use wasmer::{AsStoreRef, FunctionEnvMut, MemoryView, WasmPtr}; + +use crate::{executor::Env, functions::FunctionErrors, storage::Storage}; + +use super::{get_memory, safely_get_string, safely_write_data_back}; + +/// Get data from the storage system if one is configured +pub fn get( + env: FunctionEnvMut, + key_buf: WasmPtr, + key_buf_len: u32, + data_buffer: WasmPtr, + data_buffer_len: u32, +) -> i32 { + let store = env.as_store_ref(); + let env_data = env.data(); + + let storage = if let Some(storage) = &env_data.storage { + storage + } else { + return FunctionErrors::ApiNotConfigured as i32; + }; + + let memory_view = match get_memory(&env, &store) { + Ok(memory_view) => memory_view, + Err(e) => { + error!( + "{}: Memory error in storage_get: {:?}", + env_data.module.name, e + ); + return FunctionErrors::CouldNotGetAdequateMemory as i32; + } + }; + + safely_get_guest_string!(key, memory_view, key_buf, key_buf_len, env_data); + + get_common( + env_data, + storage, + &env_data.module.name, + &key, + memory_view, + data_buffer, + data_buffer_len, + ) +} + +/// Get data from a shared namespace in the storage system, if one is configured +pub fn get_shared( + env: FunctionEnvMut, + namespace_buf: WasmPtr, + namespace_buf_len: u32, + key_buf: WasmPtr, + key_buf_len: u32, + data_buffer: WasmPtr, + data_buffer_len: u32, +) -> i32 { + let store = env.as_store_ref(); + let env_data = env.data(); + + let Some(storage) = &env_data.storage else { + return FunctionErrors::ApiNotConfigured as i32; + }; + + // Check if we have shared DBs at all, otherwise we just stop + let Some(shared_dbs) = &storage.shared_dbs else { + return FunctionErrors::OperationNotAllowed as i32; + }; + + let memory_view = match get_memory(&env, &store) { + Ok(memory_view) => memory_view, + Err(e) => { + error!( + "{}: Memory error in storage_get_shared: {e:?}", + env_data.module.name, + ); + return FunctionErrors::CouldNotGetAdequateMemory as i32; + } + }; + + safely_get_guest_string!( + namespace, + memory_view, + namespace_buf, + namespace_buf_len, + env_data + ); + + // Check if we can access this namespace, otherwise we just stop + let allowed = match shared_dbs.get(&namespace) { + None => false, + Some(db) => { + db.config.r.contains(&env_data.module.name) + || db.config.rw.contains(&env_data.module.name) + } + }; + if !allowed { + return FunctionErrors::OperationNotAllowed as i32; + } + + safely_get_guest_string!(key, memory_view, key_buf, key_buf_len, env_data); + + get_common( + env_data, + storage, + &namespace, + &key, + memory_view, + data_buffer, + data_buffer_len, + ) +} + +/// Code which is common to `get` and `get_shared` +fn get_common( + env_data: &Env, + storage: &Arc, + namespace: &str, + key: &str, + memory_view: MemoryView, + data_buffer: WasmPtr, + data_buffer_len: u32, +) -> i32 { + let result = env_data + .api + .clone() + .runtime + .block_on(async move { storage.get(namespace, key).await }); + + match result { + Ok(Some(data)) => { + match safely_write_data_back(&memory_view, &data, data_buffer, data_buffer_len) { + Ok(x) => x, + Err(e) => { + error!( + "{}: Data write error in storage_get: {e:?}", + env_data.module.name, + ); + e as i32 + } + } + } + Ok(None) => 0, + Err(_) => FunctionErrors::InternalApiError as i32, + } +} diff --git a/runtime/plaid/src/functions/storage/insert.rs b/runtime/plaid/src/functions/storage/insert.rs new file mode 100644 index 00000000..beeb1482 --- /dev/null +++ b/runtime/plaid/src/functions/storage/insert.rs @@ -0,0 +1,299 @@ +use std::sync::{Arc, RwLock}; + +use wasmer::{AsStoreRef, FunctionEnvMut, MemoryView, WasmPtr}; + +use crate::{executor::Env, functions::FunctionErrors, loader::LimitValue, storage::Storage}; + +use super::{ + calculate_max_buffer_size, get_memory, safely_get_memory, safely_get_string, + safely_write_data_back, +}; + +/// Store data in the storage system if one is configured +pub fn insert( + env: FunctionEnvMut, + key_buf: WasmPtr, + key_buf_len: u32, + value_buf: WasmPtr, + value_buf_len: u32, + data_buffer: WasmPtr, + data_buffer_len: u32, +) -> i32 { + let store = env.as_store_ref(); + let env_data = env.data(); + + let Some(storage) = &env_data.storage else { + return FunctionErrors::ApiNotConfigured as i32; + }; + + let memory_view = match get_memory(&env, &store) { + Ok(memory_view) => memory_view, + Err(e) => { + error!( + "{}: Memory error in storage_insert: {:?}", + env_data.module.name, e + ); + return FunctionErrors::CouldNotGetAdequateMemory as i32; + } + }; + + safely_get_guest_string!(key, memory_view, key_buf, key_buf_len, env_data); + safely_get_guest_memory!(value, memory_view, value_buf, value_buf_len, env_data); + + match insert_common( + env_data, + storage, + env_data.module.name.clone(), + key, + value, + memory_view, + data_buffer, + data_buffer_len, + env_data.module.storage_limit.clone(), + env_data.module.storage_current.clone(), + ) { + Ok(code) => code, + Err(e) => e as i32, + } +} + +/// Store data in a shared namespace in the storage system, if one is configured +pub fn insert_shared( + env: FunctionEnvMut, + namespace_buf: WasmPtr, + namespace_buf_len: u32, + key_buf: WasmPtr, + key_buf_len: u32, + value_buf: WasmPtr, + value_buf_len: u32, + data_buffer: WasmPtr, + data_buffer_len: u32, +) -> i32 { + let store = env.as_store_ref(); + let env_data = env.data(); + + let Some(storage) = &env_data.storage else { + return FunctionErrors::ApiNotConfigured as i32; + }; + + // Check if we have shared DBs at all, otherwise we just stop + let Some(shared_dbs) = &storage.shared_dbs else { + return FunctionErrors::OperationNotAllowed as i32; + }; + + let memory_view = match get_memory(&env, &store) { + Ok(memory_view) => memory_view, + Err(e) => { + error!( + "{}: Memory error in storage_insert_shared: {e:?}", + env_data.module.name, + ); + return FunctionErrors::CouldNotGetAdequateMemory as i32; + } + }; + + safely_get_guest_string!( + namespace, + memory_view, + namespace_buf, + namespace_buf_len, + env_data + ); + + // Get the shared DB, if it exists. Otherwise, exit with an error + let Some(db) = shared_dbs.get(&namespace) else { + return FunctionErrors::SharedDbError as i32; + }; + + // Check if calling module has permission to write to the DB + if !db.config.rw.contains(&env_data.module.name) { + return FunctionErrors::OperationNotAllowed as i32; + } + + safely_get_guest_string!(key, memory_view, key_buf, key_buf_len, env_data); + safely_get_guest_memory!(value, memory_view, value_buf, value_buf_len, env_data); + + match insert_common( + env_data, + storage, + env_data.module.name.clone(), + key, + value, + memory_view, + data_buffer, + data_buffer_len, + db.config.size_limit.clone(), + db.used_storage.clone(), + ) { + Ok(code) => code, + Err(e) => e as i32, + } +} + +/// Code which is common to `insert` and `insert_shared` +fn insert_common( + env_data: &Env, + storage: &Arc, + namespace: String, + key: String, + value: Vec, + memory_view: MemoryView, + data_buffer: WasmPtr, + data_buffer_len: u32, + storage_limit: LimitValue, + storage_counter: Arc>, +) -> Result { + // The insertion proceeds differently depending on whether the storage is limited or not. + let insertion_result = match storage_limit { + LimitValue::Unlimited => { + // The storage is unlimited, so we don't check / update any counters and just proceed with the operation + env_data + .api + .clone() + .runtime + .block_on(async move { storage.insert(namespace, key, value).await }) + } + LimitValue::Limited(limit) => { + // The storage is limited, so we need to check / update counters (with locks) because the operation might have to be rejected. + + let existing_data_size = fetch_existing_data_size(env_data, storage, &namespace, &key)?; + + // Get a lock on the storage counter. + // This ensures no race conditions if multiple instances of the same module are running in parallel. + // The guard is held until the end of this block so that the counter update and the + // actual insertion are atomic with respect to other concurrent module instances. + let mut storage_current = match storage_counter.write() { + Ok(g) => g, + Err(e) => { + error!("Critical error getting a lock on used storage: {e:?}"); + return Err(FunctionErrors::InternalApiError); + } + }; + + let would_be_used_storage = check_storage_limit( + env_data, + *storage_current, + existing_data_size, + key.as_bytes().len() as u64, + value.len() as u64, + limit, + &key, + )?; + + let result = env_data + .api + .clone() + .runtime + .block_on(async move { storage.insert(namespace, key, value).await }); + + // If the insertion went well, update counter for used storage. + // If the insertion failed for some reason, we don't update the counter and release the lock: no harm done. + if result.is_ok() { + *storage_current = would_be_used_storage; + } + result + } + }; + + handle_insertion_result( + env_data, + insertion_result, + &memory_view, + data_buffer, + data_buffer_len, + ) +} + +/// Writes the previously-stored value (returned by the storage insert) back to guest memory +/// and returns the number of bytes written, or an appropriate error code. +fn handle_insertion_result( + env_data: &Env, + insertion_result: Result>, impl std::fmt::Display>, + memory_view: &MemoryView, + data_buffer: WasmPtr, + data_buffer_len: u32, +) -> Result { + match insertion_result { + Ok(Some(data)) => { + // If the data is too large to fit in the buffer that was passed to us. Unfortunately this is a somewhat + // unrecoverable state because we've overwritten the value already. We could fail insertion if the data + // buffer passed is too small in future? That would mean doing a get call first, which the client can do + // too. + safely_write_data_back(memory_view, &data, data_buffer, data_buffer_len).inspect_err( + |e| { + error!( + "{}: Data write error in storage_insert: {e:?}", + env_data.module.name, + ); + }, + ) + } + // No previous value for this key; report zero bytes written back. + Ok(None) => Ok(0), + // If the storage system errors (for example a network problem if using a networked storage provider) + // the error is made opaque to the client here and we log what happened + Err(e) => { + error!( + "There was a storage system error during insert by [{}]: {e}", + env_data.module.name + ); + Err(FunctionErrors::InternalApiError) + } + } +} + +/// Fetches the number of bytes currently occupied by an existing key (value length + key length), +/// or 0 if the key does not exist. Returns `Err(i32)` with a ready-to-return error code on failure. +fn fetch_existing_data_size( + env_data: &Env, + storage: &Arc, + namespace: &str, + key: &str, +) -> Result { + let key_len = key.as_bytes().len() as u64; + match env_data + .api + .clone() + .runtime + .block_on(async { storage.get(namespace, key).await }) + { + Ok(None) => Ok(0u64), + // If we have existing data, count the key length too since at the end of a possible + // insertion there would still be only one key occupying that space. + Ok(Some(d)) => Ok(d.len() as u64 + key_len), + Err(_) => Err(FunctionErrors::InternalApiError), + } +} + +/// Checks whether inserting `new_value_len` bytes under `key` would exceed `storage_limit`. +/// Returns the would-be storage usage on success, or `Err(i32)` with a ready-to-return error +/// code if the limit would be exceeded. +fn check_storage_limit( + env_data: &Env, + current_storage: u64, + existing_data_size: u64, + key_len: u64, + new_value_len: u64, + storage_limit: u64, + key: &str, +) -> Result { + // Note: we subtract existing_data_size because the old value would be overwritten. + // No underflow risk: current_storage >= existing_data_size always holds. + let would_be_used_storage = current_storage + key_len + new_value_len - existing_data_size; + + if would_be_used_storage > storage_limit { + error!( + "{}: Could not insert key/value with key [{key}] as that would bring us above the configured storage limit.", + env_data.module.name + ); + let _ = env_data.external_logging_system.log_module_error( + env_data.module.name.clone(), + "Could not insert key/value as that would bring us above the configured storage limit." + .to_string(), + vec![], + ); + return Err(FunctionErrors::StorageLimitReached); + } + + Ok(would_be_used_storage) +} diff --git a/runtime/plaid/src/functions/storage/list.rs b/runtime/plaid/src/functions/storage/list.rs new file mode 100644 index 00000000..ecf374e1 --- /dev/null +++ b/runtime/plaid/src/functions/storage/list.rs @@ -0,0 +1,169 @@ +use std::sync::Arc; + +use wasmer::{AsStoreRef, FunctionEnvMut, MemoryView, WasmPtr}; + +use crate::{executor::Env, functions::FunctionErrors, storage::Storage}; + +use super::{get_memory, safely_get_string, safely_write_data_back}; + +/// Fetch all the keys from the storage system and filter for a prefix +/// before returning the data. +pub fn list_keys( + env: FunctionEnvMut, + prefix_buf: WasmPtr, + prefix_buf_len: u32, + data_buffer: WasmPtr, + data_buffer_len: u32, +) -> i32 { + let store = env.as_store_ref(); + let env_data = env.data(); + + let Some(storage) = &env_data.storage else { + return FunctionErrors::ApiNotConfigured as i32; + }; + + let memory_view = match get_memory(&env, &store) { + Ok(memory_view) => memory_view, + Err(e) => { + error!( + "{}: Memory error in storage_list_keys: {e:?}", + env_data.module.name, + ); + return FunctionErrors::CouldNotGetAdequateMemory as i32; + } + }; + + safely_get_guest_string!(prefix, memory_view, prefix_buf, prefix_buf_len, env_data); + + list_keys_common( + env_data, + storage, + env_data.module.name.clone(), + prefix, + memory_view, + data_buffer, + data_buffer_len, + ) +} + +/// Fetch all the keys from a shared namespace in the storage system and filter for a prefix +/// before returning the data. +pub fn list_keys_shared( + env: FunctionEnvMut, + namespace_buf: WasmPtr, + namespace_buf_len: u32, + prefix_buf: WasmPtr, + prefix_buf_len: u32, + data_buffer: WasmPtr, + data_buffer_len: u32, +) -> i32 { + let store = env.as_store_ref(); + let env_data = env.data(); + + let Some(storage) = &env_data.storage else { + return FunctionErrors::ApiNotConfigured as i32; + }; + + // Check if we have shared DBs at all, otherwise we just stop + let Some(shared_dbs) = &storage.shared_dbs else { + return FunctionErrors::OperationNotAllowed as i32; + }; + + let memory_view = match get_memory(&env, &store) { + Ok(memory_view) => memory_view, + Err(e) => { + error!( + "{}: Memory error in storage_list_keys: {e:?}", + env_data.module.name, + ); + return FunctionErrors::CouldNotGetAdequateMemory as i32; + } + }; + + safely_get_guest_string!( + namespace, + memory_view, + namespace_buf, + namespace_buf_len, + env_data + ); + + // Check if we can access this namespace, otherwise we just stop + let allowed = match shared_dbs.get(&namespace) { + None => false, + Some(db) => { + db.config.r.contains(&env_data.module.name) + || db.config.rw.contains(&env_data.module.name) + } + }; + if !allowed { + return FunctionErrors::OperationNotAllowed as i32; + } + + safely_get_guest_string!(prefix, memory_view, prefix_buf, prefix_buf_len, env_data); + + list_keys_common( + env_data, + storage, + namespace, + prefix, + memory_view, + data_buffer, + data_buffer_len, + ) +} + +/// Code which is common to `list_keys` and `list_keys_shared` +fn list_keys_common( + env_data: &Env, + storage: &Arc, + namespace: String, + prefix: String, + memory_view: MemoryView, + data_buffer: WasmPtr, + data_buffer_len: u32, +) -> i32 { + let result = env_data + .api + .clone() + .runtime + .block_on(async move { storage.list_keys(&namespace, Some(prefix.as_str())).await }); + + match result { + Ok(keys) => { + let serialized_keys = match serde_json::to_string(&keys) { + Ok(sk) => sk, + Err(e) => { + error!( + "Could not serialize keys for namespaces {}: {e}", + env_data.module.name + ); + return FunctionErrors::ErrorCouldNotSerialize as i32; + } + }; + + match safely_write_data_back( + &memory_view, + &serialized_keys.as_bytes(), + data_buffer, + data_buffer_len, + ) { + Ok(x) => x, + Err(e) => { + error!( + "{}: Data write error in storage_list: {e:?}", + env_data.module.name, + ); + e as i32 + } + } + } + Err(e) => { + error!( + "Could not list keys for namespace {}: {e}", + &env_data.module.name + ); + return FunctionErrors::InternalApiError as i32; + } + } +} diff --git a/runtime/plaid/src/functions/storage/mod.rs b/runtime/plaid/src/functions/storage/mod.rs new file mode 100644 index 00000000..243081fb --- /dev/null +++ b/runtime/plaid/src/functions/storage/mod.rs @@ -0,0 +1,45 @@ +use super::{ + calculate_max_buffer_size, get_memory, safely_get_memory, safely_get_string, + safely_write_data_back, +}; + +pub use delete::{delete, delete_shared}; +pub use get::{get, get_shared}; +pub use insert::{insert, insert_shared}; +pub use list::{list_keys, list_keys_shared}; + +macro_rules! safely_get_guest_string { + ($variable:ident, $memory_view:expr, $buf:expr, $buf_len:expr, $env_data:expr) => { + let $variable = match safely_get_string(&$memory_view, $buf, $buf_len) { + Ok(s) => s, + Err(e) => { + error!( + "{}: error while getting a string from guest memory: {:?}", + $env_data.module.name, e + ); + return FunctionErrors::ParametersNotUtf8 as i32; + } + }; + }; +} + +macro_rules! safely_get_guest_memory { + ($variable:ident, $memory_view:expr, $buf:expr, $buf_len:expr, $env_data:expr) => { + let max_buffer_size = calculate_max_buffer_size($env_data.module.page_limit); + let $variable = match safely_get_memory(&$memory_view, $buf, $buf_len, max_buffer_size) { + Ok(d) => d, + Err(e) => { + error!( + "{}: error while getting bytes from guest memory: {:?}", + $env_data.module.name, e + ); + return FunctionErrors::ParametersNotUtf8 as i32; + } + }; + }; +} + +mod delete; +mod get; +mod insert; +mod list; From 9d6f1fd5f3045141fb191966fc85e63f18277909 Mon Sep 17 00:00:00 2001 From: Will Spencer Date: Thu, 12 Mar 2026 11:58:15 -0400 Subject: [PATCH 2/4] Cleanup code in get.rs --- runtime/plaid/src/functions/storage/get.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/runtime/plaid/src/functions/storage/get.rs b/runtime/plaid/src/functions/storage/get.rs index bde5c3fe..0bc142a0 100644 --- a/runtime/plaid/src/functions/storage/get.rs +++ b/runtime/plaid/src/functions/storage/get.rs @@ -17,9 +17,7 @@ pub fn get( let store = env.as_store_ref(); let env_data = env.data(); - let storage = if let Some(storage) = &env_data.storage { - storage - } else { + let Some(storage) = &env_data.storage else { return FunctionErrors::ApiNotConfigured as i32; }; @@ -27,8 +25,8 @@ pub fn get( Ok(memory_view) => memory_view, Err(e) => { error!( - "{}: Memory error in storage_get: {:?}", - env_data.module.name, e + "{}: Memory error in storage_get: {e:?}", + env_data.module.name, ); return FunctionErrors::CouldNotGetAdequateMemory as i32; } From 5a91d6ccb8ad212fc54d82ab97b8998568fa0fae Mon Sep 17 00:00:00 2001 From: Will Spencer Date: Thu, 12 Mar 2026 10:42:36 -0400 Subject: [PATCH 3/4] lockfile update --- runtime/Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index a4217fb4..6af17a94 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -1914,9 +1914,9 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libm", - "rand 0.9.1", + "rand 0.9.2", "siphasher", ] From 2cbed207bba3a22b7b346b79e11bfd986b702cd7 Mon Sep 17 00:00:00 2001 From: Will Spencer Date: Thu, 12 Mar 2026 14:50:05 -0400 Subject: [PATCH 4/4] Fix bug in insert_common call in insert_shared --- runtime/plaid/src/functions/storage/insert.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/plaid/src/functions/storage/insert.rs b/runtime/plaid/src/functions/storage/insert.rs index beeb1482..c20fc524 100644 --- a/runtime/plaid/src/functions/storage/insert.rs +++ b/runtime/plaid/src/functions/storage/insert.rs @@ -116,7 +116,7 @@ pub fn insert_shared( match insert_common( env_data, storage, - env_data.module.name.clone(), + namespace, key, value, memory_view,