From 907ecd3d403fd06bacb18180e088adb0a23e7467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois?= Date: Tue, 22 Sep 2026 21:32:00 +0200 Subject: [PATCH 1/3] feat: paginated datastore-key ABIs (AS path) Add get_keys_paginated / get_keys_for_paginated to the Interface trait (prefix filter, exclusive start_after cursor, mandatory count), with matching assembly_script_get_keys_paginated[_for] host functions, gas cost entries (provisional, mirrored) and unit tests for the pagination param mapping. Exploratory work for massalabs/massa#5284 (wasmv1 intentionally out: its request type lives in massa-proto-rs, a third repo). --- src/as_execution/abi.rs | 117 +++++++++++++++++++++++++++++++++++- src/as_execution/context.rs | 2 + src/tests/mod.rs | 19 ++++++ src/types.rs | 30 +++++++++ 4 files changed, 167 insertions(+), 1 deletion(-) diff --git a/src/as_execution/abi.rs b/src/as_execution/abi.rs index ecc46e60..64045882 100644 --- a/src/as_execution/abi.rs +++ b/src/as_execution/abi.rs @@ -596,6 +596,98 @@ pub(crate) fn assembly_script_get_keys_for( }) } +/// Read pagination params shared by the paginated get-keys ABIs: +/// empty buffers map to `None`, and a negative count is rejected. +fn read_pagination_params( + prefix: Vec, + start_after: Vec, + count: i32, +) -> ABIResult<(Vec, Vec, u32)> { + let count = u32::try_from(count) + .map_err(|_| ABIError::RuntimeError("pagination count must be non-negative".into()))?; + Ok((prefix, start_after, count)) +} + +/// Map a raw buffer to an optional bound: empty means unbounded. +fn opt_bound(buffer: &[u8]) -> Option<&[u8]> { + if buffer.is_empty() { + None + } else { + Some(buffer) + } +} + +/// Get keys (aka entries) in the datastore, paginated (bounded replacement for +/// `assembly_script_get_keys`, see massa #5284). +pub(crate) fn assembly_script_get_keys_paginated( + mut ctx: FunctionEnvMut, + prefix: i32, + start_after: i32, + count: i32, +) -> ABIResult { + abi_with_memory!(ctx, assembly_script_get_keys_paginated, |memory| { + let prefix = read_buffer(&memory, &ctx, prefix)?; + let start_after = read_buffer(&memory, &ctx, start_after)?; + let (prefix, start_after, count) = read_pagination_params(prefix, start_after, count)?; + let keys = ctx.data().interface.get_keys_paginated( + opt_bound(&prefix), + opt_bound(&start_after), + count, + )?; + let fmt_keys = ser_bytearray_vec(&keys, keys.len(), settings::max_datastore_entry_count())?; + let ffi_env = ctx.data().get_ffi_env().clone(); + let ptr = BufferPtr::alloc(&fmt_keys, &ffi_env, &mut ctx)?.offset(); + + #[cfg(feature = "execution-trace")] + ctx.data_mut().trace.push(AbiTrace { + name: "assembly_script_get_keys_paginated".to_string(), + params: vec![into_trace_value!(prefix), into_trace_value!(start_after)], + return_value: AbiTraceType::ByteArrays(keys.iter().cloned().collect()), + sub_calls: None, + }); + Ok(ptr as i32) + }) +} + +/// Get keys (aka entries) in the datastore for an address, paginated (bounded +/// replacement for `assembly_script_get_keys_for`, see massa #5284). +pub(crate) fn assembly_script_get_keys_for_paginated( + mut ctx: FunctionEnvMut, + address: i32, + prefix: i32, + start_after: i32, + count: i32, +) -> ABIResult { + abi_with_memory!(ctx, assembly_script_get_keys_for_paginated, |memory| { + let address = read_string(&memory, &ctx, address)?; + let prefix = read_buffer(&memory, &ctx, prefix)?; + let start_after = read_buffer(&memory, &ctx, start_after)?; + let (prefix, start_after, count) = read_pagination_params(prefix, start_after, count)?; + let keys = ctx.data().interface.get_keys_for_paginated( + &address, + opt_bound(&prefix), + opt_bound(&start_after), + count, + )?; + let fmt_keys = ser_bytearray_vec(&keys, keys.len(), settings::max_datastore_entry_count())?; + let ffi_env = ctx.data().get_ffi_env().clone(); + let ptr = BufferPtr::alloc(&fmt_keys, &ffi_env, &mut ctx)?.offset(); + + #[cfg(feature = "execution-trace")] + ctx.data_mut().trace.push(AbiTrace { + name: "assembly_script_get_keys_for_paginated".to_string(), + params: vec![ + into_trace_value!(address), + into_trace_value!(prefix), + into_trace_value!(start_after), + ], + return_value: AbiTraceType::ByteArrays(keys.iter().cloned().collect()), + sub_calls: None, + }); + Ok(ptr as i32) + }) +} + /// sets a key-indexed data entry in the datastore, overwriting existing values /// if any pub(crate) fn assembly_script_set_data( @@ -1934,7 +2026,30 @@ pub(crate) fn assembly_script_hash_sha256( #[cfg(test)] mod tests { - use crate::as_execution::abi::ser_bytearray_vec; + use crate::as_execution::abi::{opt_bound, read_pagination_params, ser_bytearray_vec}; + + #[test] + fn test_pagination_params_empty_means_unbounded() { + let (prefix, start_after, count) = read_pagination_params(vec![], vec![], 10).unwrap(); + assert_eq!(count, 10); + assert_eq!(opt_bound(&prefix), None); + assert_eq!(opt_bound(&start_after), None); + } + + #[test] + fn test_pagination_params_buffers_forwarded() { + let (prefix, start_after, count) = + read_pagination_params(b"ab".to_vec(), b"key7".to_vec(), 0).unwrap(); + assert_eq!(count, 0); + assert_eq!(opt_bound(&prefix), Some(b"ab".as_ref())); + assert_eq!(opt_bound(&start_after), Some(b"key7".as_ref())); + } + + #[test] + fn test_pagination_params_negative_count_rejected() { + assert!(read_pagination_params(vec![], vec![], -1).is_err()); + assert!(read_pagination_params(vec![], vec![], i32::MIN).is_err()); + } #[test] fn test_ser() { diff --git a/src/as_execution/context.rs b/src/as_execution/context.rs index f96bcbf7..020a3796 100644 --- a/src/as_execution/context.rs +++ b/src/as_execution/context.rs @@ -273,6 +273,8 @@ impl ASContext { "assembly_script_get_op_keys_prefix" => Function::new_typed_with_env(store, &fenv, assembly_script_get_op_keys_prefix), "assembly_script_get_keys" => Function::new_typed_with_env(store, &fenv, assembly_script_get_keys), "assembly_script_get_keys_for" => Function::new_typed_with_env(store, &fenv, assembly_script_get_keys_for), + "assembly_script_get_keys_paginated" => Function::new_typed_with_env(store, &fenv, assembly_script_get_keys_paginated), + "assembly_script_get_keys_for_paginated" => Function::new_typed_with_env(store, &fenv, assembly_script_get_keys_for_paginated), "assembly_script_has_op_key" => Function::new_typed_with_env(store, &fenv, assembly_script_has_op_key), "assembly_script_get_op_data" => Function::new_typed_with_env(store, &fenv, assembly_script_get_op_data), "assembly_script_get_bytecode" => Function::new_typed_with_env(store, &fenv, assembly_script_get_bytecode), diff --git a/src/tests/mod.rs b/src/tests/mod.rs index b9ffa81b..1a9f0bcf 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -623,6 +623,25 @@ impl Interface for TestInterface { todo!() } + fn get_keys_paginated( + &self, + _prefix: Option<&[u8]>, + _start_after: Option<&[u8]>, + _count: u32, + ) -> Result>> { + todo!() + } + + fn get_keys_for_paginated( + &self, + _address: &str, + _prefix: Option<&[u8]>, + _start_after: Option<&[u8]>, + _count: u32, + ) -> Result>> { + todo!() + } + fn raw_get_bytecode(&self) -> Result> { todo!() } diff --git a/src/types.rs b/src/types.rs index 8d33c837..06667ffe 100644 --- a/src/types.rs +++ b/src/types.rs @@ -273,6 +273,8 @@ pub struct GasCosts { pub assembly_script_get_deferred_call_quote: u64, pub assembly_script_get_keys: u64, pub assembly_script_get_keys_for: u64, + pub assembly_script_get_keys_paginated: u64, + pub assembly_script_get_keys_for_paginated: u64, pub assembly_script_get_op_data: u64, pub assembly_script_get_op_keys: u64, pub assembly_script_get_op_keys_prefix: u64, @@ -465,6 +467,10 @@ impl GasCosts { ), assembly_script_get_keys: get_cost!("assembly_script_get_keys"), assembly_script_get_keys_for: get_cost!("assembly_script_get_keys_for"), + assembly_script_get_keys_paginated: get_cost!("assembly_script_get_keys_paginated"), + assembly_script_get_keys_for_paginated: get_cost!( + "assembly_script_get_keys_for_paginated" + ), assembly_script_get_op_data: get_cost!("assembly_script_get_op_data"), assembly_script_get_op_keys: get_cost!("assembly_script_get_op_keys"), assembly_script_get_op_keys_prefix: get_cost!("assembly_script_get_op_keys_prefix"), @@ -621,6 +627,8 @@ impl Default for GasCosts { assembly_script_get_deferred_call_quote: 1220, assembly_script_get_keys: 1000, assembly_script_get_keys_for: 1195, + assembly_script_get_keys_paginated: 1000, + assembly_script_get_keys_for_paginated: 1195, assembly_script_get_op_data: 50000, assembly_script_get_op_keys: 1400, assembly_script_get_op_keys_prefix: 1400, @@ -821,6 +829,28 @@ pub trait Interface: Send + Sync + InterfaceClone { /// Will only return keys with a given prefix if provided in args fn get_keys_for(&self, address: &str, prefix: Option<&[u8]>) -> Result>>; + /// Return datastore keys, paginated. + /// Only keys with the given prefix (if any) are considered, ordered + /// lexicographically; only keys strictly after `start_after` (if any) are + /// returned, up to `count` keys. This is the bounded replacement for + /// `get_keys` (see massa #5284). + fn get_keys_paginated( + &self, + prefix: Option<&[u8]>, + start_after: Option<&[u8]>, + count: u32, + ) -> Result>>; + + /// Return datastore keys for an address, paginated (same semantics as + /// `get_keys_paginated`, scoped to `address`). + fn get_keys_for_paginated( + &self, + address: &str, + prefix: Option<&[u8]>, + start_after: Option<&[u8]>, + count: u32, + ) -> Result>>; + fn get_ds_keys_wasmv1( &self, prefix: &[u8], From d5f8223a03edbdee94f5a630fec2b8d0e6b5813e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois?= Date: Wed, 23 Sep 2026 17:18:48 +0200 Subject: [PATCH 2/3] feat: gate paginated datastore-key ABIs behind interface version Only resolve the new assembly_script_get_keys_paginated[_for] imports when the host reports an execution component version at or past PAGINATED_DS_KEYS_EXECUTION_VERSION (MIP-0002 massa-side), so an updated node rejects the imports before activation exactly like a non-updated node. Unknown versions fail closed. --- src/as_execution/abi.rs | 108 ++++++++++++++++++++---------------- src/as_execution/context.rs | 90 +++++++++++++++++++++++++++++- src/tests/mod.rs | 6 +- src/types.rs | 60 ++++++++++++++------ 4 files changed, 193 insertions(+), 71 deletions(-) diff --git a/src/as_execution/abi.rs b/src/as_execution/abi.rs index 64045882..f4ff8fc9 100644 --- a/src/as_execution/abi.rs +++ b/src/as_execution/abi.rs @@ -596,80 +596,87 @@ pub(crate) fn assembly_script_get_keys_for( }) } -/// Read pagination params shared by the paginated get-keys ABIs: -/// empty buffers map to `None`, and a negative count is rejected. -fn read_pagination_params( - prefix: Vec, - start_after: Vec, - count: i32, -) -> ABIResult<(Vec, Vec, u32)> { - let count = u32::try_from(count) - .map_err(|_| ABIError::RuntimeError("pagination count must be non-negative".into()))?; - Ok((prefix, start_after, count)) +/// Empty buffer means the bound is unset. +fn opt_bound(buffer: &[u8]) -> Option<&[u8]> { + (!buffer.is_empty()).then_some(buffer) } -/// Map a raw buffer to an optional bound: empty means unbounded. -fn opt_bound(buffer: &[u8]) -> Option<&[u8]> { - if buffer.is_empty() { - None - } else { - Some(buffer) +/// Page size accepted by the paginated datastore-key ABIs: `1..=MAX_DATASTORE_KEYS_PAGE`. +fn checked_page_count(count: i32) -> ABIResult { + let Ok(count) = u32::try_from(count) else { + abi_bail!(format!("negative datastore key page size: {count}")); + }; + if !(1..=crate::MAX_DATASTORE_KEYS_PAGE).contains(&count) { + abi_bail!(format!( + "datastore key page size must be between 1 and {}, got {}", + crate::MAX_DATASTORE_KEYS_PAGE, + count + )); } + Ok(count) } -/// Get keys (aka entries) in the datastore, paginated (bounded replacement for -/// `assembly_script_get_keys`, see massa #5284). +/// Get one page of keys (aka entries) in the datastore. +/// +/// `prefix` and `start_key` are optional (empty means unset). `start_key` is an +/// exclusive cursor. `count` must be in `1..=MAX_DATASTORE_KEYS_PAGE`. pub(crate) fn assembly_script_get_keys_paginated( mut ctx: FunctionEnvMut, prefix: i32, - start_after: i32, + start_key: i32, count: i32, ) -> ABIResult { abi_with_memory!(ctx, assembly_script_get_keys_paginated, |memory| { let prefix = read_buffer(&memory, &ctx, prefix)?; - let start_after = read_buffer(&memory, &ctx, start_after)?; - let (prefix, start_after, count) = read_pagination_params(prefix, start_after, count)?; + let start_key = read_buffer(&memory, &ctx, start_key)?; + let count = checked_page_count(count)?; let keys = ctx.data().interface.get_keys_paginated( opt_bound(&prefix), - opt_bound(&start_after), + opt_bound(&start_key), count, )?; - let fmt_keys = ser_bytearray_vec(&keys, keys.len(), settings::max_datastore_entry_count())?; + let fmt_keys = + ser_bytearray_vec(&keys, keys.len(), crate::MAX_DATASTORE_KEYS_PAGE as usize)?; let ffi_env = ctx.data().get_ffi_env().clone(); let ptr = BufferPtr::alloc(&fmt_keys, &ffi_env, &mut ctx)?.offset(); #[cfg(feature = "execution-trace")] ctx.data_mut().trace.push(AbiTrace { name: "assembly_script_get_keys_paginated".to_string(), - params: vec![into_trace_value!(prefix), into_trace_value!(start_after)], - return_value: AbiTraceType::ByteArrays(keys.iter().cloned().collect()), + params: vec![ + into_trace_value!(prefix), + into_trace_value!(start_key), + into_trace_value!(count), + ], + return_value: fmt_keys.into(), sub_calls: None, }); Ok(ptr as i32) }) } -/// Get keys (aka entries) in the datastore for an address, paginated (bounded -/// replacement for `assembly_script_get_keys_for`, see massa #5284). +/// Get one page of keys (aka entries) in the datastore of a given address. +/// See [`assembly_script_get_keys_paginated`]. pub(crate) fn assembly_script_get_keys_for_paginated( mut ctx: FunctionEnvMut, address: i32, prefix: i32, - start_after: i32, + start_key: i32, count: i32, ) -> ABIResult { abi_with_memory!(ctx, assembly_script_get_keys_for_paginated, |memory| { let address = read_string(&memory, &ctx, address)?; let prefix = read_buffer(&memory, &ctx, prefix)?; - let start_after = read_buffer(&memory, &ctx, start_after)?; - let (prefix, start_after, count) = read_pagination_params(prefix, start_after, count)?; + let start_key = read_buffer(&memory, &ctx, start_key)?; + let count = checked_page_count(count)?; let keys = ctx.data().interface.get_keys_for_paginated( &address, opt_bound(&prefix), - opt_bound(&start_after), + opt_bound(&start_key), count, )?; - let fmt_keys = ser_bytearray_vec(&keys, keys.len(), settings::max_datastore_entry_count())?; + let fmt_keys = + ser_bytearray_vec(&keys, keys.len(), crate::MAX_DATASTORE_KEYS_PAGE as usize)?; let ffi_env = ctx.data().get_ffi_env().clone(); let ptr = BufferPtr::alloc(&fmt_keys, &ffi_env, &mut ctx)?.offset(); @@ -679,7 +686,8 @@ pub(crate) fn assembly_script_get_keys_for_paginated( params: vec![ into_trace_value!(address), into_trace_value!(prefix), - into_trace_value!(start_after), + into_trace_value!(start_key), + into_trace_value!(count), ], return_value: AbiTraceType::ByteArrays(keys.iter().cloned().collect()), sub_calls: None, @@ -2026,29 +2034,33 @@ pub(crate) fn assembly_script_hash_sha256( #[cfg(test)] mod tests { - use crate::as_execution::abi::{opt_bound, read_pagination_params, ser_bytearray_vec}; + use super::{checked_page_count, opt_bound, ser_bytearray_vec}; + use crate::{GasCosts, MAX_DATASTORE_KEYS_PAGE}; #[test] - fn test_pagination_params_empty_means_unbounded() { - let (prefix, start_after, count) = read_pagination_params(vec![], vec![], 10).unwrap(); - assert_eq!(count, 10); - assert_eq!(opt_bound(&prefix), None); - assert_eq!(opt_bound(&start_after), None); + fn test_opt_bound_empty_is_unset() { + assert_eq!(opt_bound(&[]), None); + assert_eq!(opt_bound(b"ab"), Some(b"ab".as_ref())); } #[test] - fn test_pagination_params_buffers_forwarded() { - let (prefix, start_after, count) = - read_pagination_params(b"ab".to_vec(), b"key7".to_vec(), 0).unwrap(); - assert_eq!(count, 0); - assert_eq!(opt_bound(&prefix), Some(b"ab".as_ref())); - assert_eq!(opt_bound(&start_after), Some(b"key7".as_ref())); + fn test_page_count_bounds() { + assert!(checked_page_count(-1).is_err()); + assert!(checked_page_count(i32::MIN).is_err()); + assert!(checked_page_count(0).is_err()); + assert_eq!(checked_page_count(1).unwrap(), 1); + assert_eq!( + checked_page_count(MAX_DATASTORE_KEYS_PAGE as i32).unwrap(), + MAX_DATASTORE_KEYS_PAGE + ); + assert!(checked_page_count(MAX_DATASTORE_KEYS_PAGE as i32 + 1).is_err()); } #[test] - fn test_pagination_params_negative_count_rejected() { - assert!(read_pagination_params(vec![], vec![], -1).is_err()); - assert!(read_pagination_params(vec![], vec![], i32::MIN).is_err()); + fn test_paginated_ds_keys_gas_is_flat_worst_page() { + let costs = GasCosts::default(); + assert_eq!(costs.assembly_script_get_keys_paginated, 190000); + assert_eq!(costs.assembly_script_get_keys_for_paginated, 190000); } #[test] diff --git a/src/as_execution/context.rs b/src/as_execution/context.rs index 020a3796..83af8365 100644 --- a/src/as_execution/context.rs +++ b/src/as_execution/context.rs @@ -214,7 +214,7 @@ impl ASContext { pub(crate) fn resolver(&self, store: &mut Store) -> (Imports, FunctionEnv) { let fenv = FunctionEnv::new(store, self.env.clone()); - let imports = imports! { + let mut imports = imports! { "env" => { // Needed by WASM generated by AssemblyScript "abort" => Function::new_typed_with_env(store, &fenv, assembly_script_abort), @@ -273,8 +273,6 @@ impl ASContext { "assembly_script_get_op_keys_prefix" => Function::new_typed_with_env(store, &fenv, assembly_script_get_op_keys_prefix), "assembly_script_get_keys" => Function::new_typed_with_env(store, &fenv, assembly_script_get_keys), "assembly_script_get_keys_for" => Function::new_typed_with_env(store, &fenv, assembly_script_get_keys_for), - "assembly_script_get_keys_paginated" => Function::new_typed_with_env(store, &fenv, assembly_script_get_keys_paginated), - "assembly_script_get_keys_for_paginated" => Function::new_typed_with_env(store, &fenv, assembly_script_get_keys_for_paginated), "assembly_script_has_op_key" => Function::new_typed_with_env(store, &fenv, assembly_script_has_op_key), "assembly_script_get_op_data" => Function::new_typed_with_env(store, &fenv, assembly_script_get_op_data), "assembly_script_get_bytecode" => Function::new_typed_with_env(store, &fenv, assembly_script_get_bytecode), @@ -291,6 +289,92 @@ impl ASContext { }, }; + // MIP-0002 ABIs. Register them only once the host reports an execution + // version at or past their activation, so an updated node rejects the + // imports before activation exactly like a non-updated node. A host + // that cannot report its version fails closed (old behavior). + if self + .env + .interface + .get_interface_version() + .is_ok_and(paginated_ds_keys_enabled) + { + imports.define( + "massa", + "assembly_script_get_keys_paginated", + Function::new_typed_with_env(store, &fenv, assembly_script_get_keys_paginated), + ); + imports.define( + "massa", + "assembly_script_get_keys_for_paginated", + Function::new_typed_with_env(store, &fenv, assembly_script_get_keys_for_paginated), + ); + } + (imports, fenv) } } + +/// Whether the paginated datastore-key ABIs are exposed for a host reporting +/// this execution component version. +fn paginated_ds_keys_enabled(interface_version: u32) -> bool { + interface_version >= crate::PAGINATED_DS_KEYS_EXECUTION_VERSION +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::{TestInterface, INTERFACE_VERSION}; + use crate::{CondomLimits, GasCosts, PAGINATED_DS_KEYS_EXECUTION_VERSION}; + use serial_test::serial; + use std::sync::atomic::Ordering; + use wasmer::{sys::EngineBuilder, wat2wasm, Module, Store}; + use wasmer_compiler_singlepass::Singlepass; + + #[test] + fn test_paginated_ds_keys_gate_boundaries() { + assert!(!paginated_ds_keys_enabled(0)); + assert!(!paginated_ds_keys_enabled( + PAGINATED_DS_KEYS_EXECUTION_VERSION - 1 + )); + assert!(paginated_ds_keys_enabled( + PAGINATED_DS_KEYS_EXECUTION_VERSION + )); + assert!(paginated_ds_keys_enabled(u32::MAX)); + } + + /// The new imports follow the execution version. The deprecated ones stay + /// registered either way, so existing modules still instantiate. + #[test] + #[serial] + fn test_paginated_ds_keys_imports_follow_execution_version() { + let engine = EngineBuilder::new(Singlepass::default()).engine(); + let mut store = Store::new(engine); + let module = Module::new(&store, wat2wasm(b"(module)").unwrap()).unwrap(); + let interface = TestInterface; + let ctx = ASContext::new( + &interface, + module, + GasCosts::default(), + CondomLimits::default(), + ); + + INTERFACE_VERSION.store(0, Ordering::SeqCst); + let (before, _) = ctx.resolver(&mut store); + let before_legacy = before.exists("massa", "assembly_script_get_keys") + && before.exists("massa", "assembly_script_get_keys_for"); + let before_paginated = before.exists("massa", "assembly_script_get_keys_paginated") + || before.exists("massa", "assembly_script_get_keys_for_paginated"); + + INTERFACE_VERSION.store(PAGINATED_DS_KEYS_EXECUTION_VERSION, Ordering::SeqCst); + let (after, _) = ctx.resolver(&mut store); + let after_paginated = after.exists("massa", "assembly_script_get_keys_paginated") + && after.exists("massa", "assembly_script_get_keys_for_paginated"); + + INTERFACE_VERSION.store(0, Ordering::SeqCst); + + assert!(before_legacy); + assert!(!before_paginated); + assert!(after_paginated); + } +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 1a9f0bcf..82307569 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -13,7 +13,7 @@ pub(crate) static INTERFACE_VERSION: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); #[derive(Clone)] -struct TestInterface; +pub(crate) struct TestInterface; impl InterfaceClone for TestInterface { fn clone_box(&self) -> Box { @@ -626,7 +626,7 @@ impl Interface for TestInterface { fn get_keys_paginated( &self, _prefix: Option<&[u8]>, - _start_after: Option<&[u8]>, + _start_key: Option<&[u8]>, _count: u32, ) -> Result>> { todo!() @@ -636,7 +636,7 @@ impl Interface for TestInterface { &self, _address: &str, _prefix: Option<&[u8]>, - _start_after: Option<&[u8]>, + _start_key: Option<&[u8]>, _count: u32, ) -> Result>> { todo!() diff --git a/src/types.rs b/src/types.rs index 06667ffe..5a7323f6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -273,8 +273,8 @@ pub struct GasCosts { pub assembly_script_get_deferred_call_quote: u64, pub assembly_script_get_keys: u64, pub assembly_script_get_keys_for: u64, - pub assembly_script_get_keys_paginated: u64, pub assembly_script_get_keys_for_paginated: u64, + pub assembly_script_get_keys_paginated: u64, pub assembly_script_get_op_data: u64, pub assembly_script_get_op_keys: u64, pub assembly_script_get_op_keys_prefix: u64, @@ -467,10 +467,10 @@ impl GasCosts { ), assembly_script_get_keys: get_cost!("assembly_script_get_keys"), assembly_script_get_keys_for: get_cost!("assembly_script_get_keys_for"), - assembly_script_get_keys_paginated: get_cost!("assembly_script_get_keys_paginated"), assembly_script_get_keys_for_paginated: get_cost!( "assembly_script_get_keys_for_paginated" ), + assembly_script_get_keys_paginated: get_cost!("assembly_script_get_keys_paginated"), assembly_script_get_op_data: get_cost!("assembly_script_get_op_data"), assembly_script_get_op_keys: get_cost!("assembly_script_get_op_keys"), assembly_script_get_op_keys_prefix: get_cost!("assembly_script_get_op_keys_prefix"), @@ -627,8 +627,13 @@ impl Default for GasCosts { assembly_script_get_deferred_call_quote: 1220, assembly_script_get_keys: 1000, assembly_script_get_keys_for: 1195, - assembly_script_get_keys_paginated: 1000, - assembly_script_get_keys_for_paginated: 1195, + // Flat price for the worst page the cap allows (500 keys): + // base + 500 × (per_key + per_key_byte × key length). + // 16-byte keys: 2_350 + 500 × 230 ≈ 117k. + // 255-byte keys: 117k + 500 × 255 × 0.55 ≈ 187k. + // 190k covers every such call, as far as keys go. + assembly_script_get_keys_for_paginated: 190000, + assembly_script_get_keys_paginated: 190000, assembly_script_get_op_data: 50000, assembly_script_get_op_keys: 1400, assembly_script_get_op_keys_prefix: 1400, @@ -740,6 +745,16 @@ impl Default for GasCosts { /// avoid a massa-versioning dependency. pub const WASMV1_RUNTIME_DISABLED_EXECUTION_VERSION: u32 = 2; +/// Execution component version from which the paginated datastore-key ABIs are +/// exposed to guest modules. Same massa MIP-0002 activation as +/// [`WASMV1_RUNTIME_DISABLED_EXECUTION_VERSION`]: before it, an updated node +/// does not resolve the new imports, so instantiation fails exactly as on a +/// non-updated node. A host that cannot report its version keeps them hidden. +pub const PAGINATED_DS_KEYS_EXECUTION_VERSION: u32 = WASMV1_RUNTIME_DISABLED_EXECUTION_VERSION; + +/// Maximum number of datastore keys one paginated call may return. +pub const MAX_DATASTORE_KEYS_PAGE: u32 = 500; + #[allow(unused_variables)] pub trait Interface: Send + Sync + InterfaceClone { fn increment_recursion_counter(&self) -> Result<()>; @@ -821,33 +836,44 @@ pub trait Interface: Send + Sync + InterfaceClone { /// Print function for examples fn print(&self, message: &str) -> Result<()>; - /// Return datastore keys - /// Will only return keys with a given prefix if provided in args + /// Return datastore keys. + /// Only keys with the given prefix are returned when one is provided. + /// + /// Superseded, from [`PAGINATED_DS_KEYS_EXECUTION_VERSION`], by + /// [`Interface::get_keys_paginated`]. fn get_keys(&self, prefix: Option<&[u8]>) -> Result>>; - /// Return datastore keys - /// Will only return keys with a given prefix if provided in args + /// Return datastore keys for an address. + /// Only keys with the given prefix are returned when one is provided. + /// + /// Superseded, from [`PAGINATED_DS_KEYS_EXECUTION_VERSION`], by + /// [`Interface::get_keys_for_paginated`]. fn get_keys_for(&self, address: &str, prefix: Option<&[u8]>) -> Result>>; - /// Return datastore keys, paginated. - /// Only keys with the given prefix (if any) are considered, ordered - /// lexicographically; only keys strictly after `start_after` (if any) are - /// returned, up to `count` keys. This is the bounded replacement for - /// `get_keys` (see massa #5284). + /// Return one page of datastore keys for the current address. + /// + /// Guest modules can call this only from [`PAGINATED_DS_KEYS_EXECUTION_VERSION`] + /// (massa MIP-0002). The AssemblyScript import is omitted before that version. + /// + /// * `prefix`: only keys with this prefix. `None` matches every key. + /// * `start_key`: exclusive resume cursor. `None` starts at the beginning of + /// the range. Pass the last key of the previous page to obtain the next one. + /// * `count`: page size, in `1..=`[`MAX_DATASTORE_KEYS_PAGE`]. The ABI rejects + /// any other value. The host must apply the same bound. fn get_keys_paginated( &self, prefix: Option<&[u8]>, - start_after: Option<&[u8]>, + start_key: Option<&[u8]>, count: u32, ) -> Result>>; - /// Return datastore keys for an address, paginated (same semantics as - /// `get_keys_paginated`, scoped to `address`). + /// Return one page of datastore keys for an address. + /// See [`Interface::get_keys_paginated`]. fn get_keys_for_paginated( &self, address: &str, prefix: Option<&[u8]>, - start_after: Option<&[u8]>, + start_key: Option<&[u8]>, count: u32, ) -> Result>>; From d2357de8ad71168432b955ee709b64f2a5e3cec3 Mon Sep 17 00:00:00 2001 From: Peterjah Date: Mon, 28 Sep 2026 13:10:55 +0200 Subject: [PATCH 3/3] refactor: use one MIP-0002 execution version constant for runtime gates --- src/as_execution/context.rs | 14 +++++--------- src/execution.rs | 4 ++-- src/lib.rs | 1 + src/settings.rs | 15 +++++++++++++++ src/tests/tests_runtime.rs | 11 ++++------- src/types.rs | 24 ++++-------------------- 6 files changed, 31 insertions(+), 38 deletions(-) diff --git a/src/as_execution/context.rs b/src/as_execution/context.rs index 83af8365..5a96f859 100644 --- a/src/as_execution/context.rs +++ b/src/as_execution/context.rs @@ -318,14 +318,14 @@ impl ASContext { /// Whether the paginated datastore-key ABIs are exposed for a host reporting /// this execution component version. fn paginated_ds_keys_enabled(interface_version: u32) -> bool { - interface_version >= crate::PAGINATED_DS_KEYS_EXECUTION_VERSION + interface_version >= crate::MIP_0002_EXECUTION_VERSION } #[cfg(test)] mod tests { use super::*; use crate::tests::{TestInterface, INTERFACE_VERSION}; - use crate::{CondomLimits, GasCosts, PAGINATED_DS_KEYS_EXECUTION_VERSION}; + use crate::{CondomLimits, GasCosts, MIP_0002_EXECUTION_VERSION}; use serial_test::serial; use std::sync::atomic::Ordering; use wasmer::{sys::EngineBuilder, wat2wasm, Module, Store}; @@ -334,12 +334,8 @@ mod tests { #[test] fn test_paginated_ds_keys_gate_boundaries() { assert!(!paginated_ds_keys_enabled(0)); - assert!(!paginated_ds_keys_enabled( - PAGINATED_DS_KEYS_EXECUTION_VERSION - 1 - )); - assert!(paginated_ds_keys_enabled( - PAGINATED_DS_KEYS_EXECUTION_VERSION - )); + assert!(!paginated_ds_keys_enabled(MIP_0002_EXECUTION_VERSION - 1)); + assert!(paginated_ds_keys_enabled(MIP_0002_EXECUTION_VERSION)); assert!(paginated_ds_keys_enabled(u32::MAX)); } @@ -366,7 +362,7 @@ mod tests { let before_paginated = before.exists("massa", "assembly_script_get_keys_paginated") || before.exists("massa", "assembly_script_get_keys_for_paginated"); - INTERFACE_VERSION.store(PAGINATED_DS_KEYS_EXECUTION_VERSION, Ordering::SeqCst); + INTERFACE_VERSION.store(MIP_0002_EXECUTION_VERSION, Ordering::SeqCst); let (after, _) = ctx.resolver(&mut store); let after_paginated = after.exists("massa", "assembly_script_get_keys_paginated") && after.exists("massa", "assembly_script_get_keys_for_paginated"); diff --git a/src/execution.rs b/src/execution.rs index ffe2a2e3..91c02354 100644 --- a/src/execution.rs +++ b/src/execution.rs @@ -4,7 +4,7 @@ use crate::middlewares::gas_calibration::GasCalibrationResult; use crate::types::{Interface, Response}; use crate::wasmv1_execution::{exec_wasmv1_module, WasmV1Module}; use crate::{settings, CondomLimits}; -use crate::{GasCosts, VMError, WASMV1_RUNTIME_DISABLED_EXECUTION_VERSION}; +use crate::{GasCosts, VMError, MIP_0002_EXECUTION_VERSION}; use anyhow::{anyhow, Result}; use num_enum::{IntoPrimitive, TryFromPrimitive}; @@ -157,7 +157,7 @@ pub(crate) fn exec( RuntimeModule::WasmV1Module(_) if interface .get_interface_version() - .is_ok_and(|v| v >= WASMV1_RUNTIME_DISABLED_EXECUTION_VERSION) => + .is_ok_and(|v| v >= MIP_0002_EXECUTION_VERSION) => { // Disabled from massa MIP-0002 on: no wasmv1 contract was ever deployed on mainnet, // and gating on the host's version keeps updated and non-updated nodes in agreement diff --git a/src/lib.rs b/src/lib.rs index 197392f7..90e794fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ mod wasmv1_execution; pub use error::VMError; pub use execution::{run_function, run_main}; pub use execution::{Compiler, RuntimeModule}; +pub use settings::{MAX_DATASTORE_KEYS_PAGE, MIP_0002_EXECUTION_VERSION}; pub use types::*; #[cfg(feature = "gas_calibration")] diff --git a/src/settings.rs b/src/settings.rs index 89d0ba58..10e124c8 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,3 +1,18 @@ +/// Execution component version introduced by massa MIP-0002 (`MipComponent::Execution` v2). +/// Kept as a literal here to avoid a massa-versioning dependency; it must stay equal to +/// massa's `MIP_0002_EXECUTION_VERSION`. +/// +/// From this version on, wasmv1 modules are no longer executed and the paginated +/// datastore-key imports are resolved. Before it, an updated node keeps the previous +/// behavior: wasmv1 modules still run, and the new imports are absent, so instantiation +/// fails exactly as on a non-updated node. A host that cannot report its version is +/// treated as pre-activation. +pub const MIP_0002_EXECUTION_VERSION: u32 = 2; + +/// Maximum number of datastore keys one paginated call may return. +/// The host must apply the same bound. +pub const MAX_DATASTORE_KEYS_PAGE: u32 = 500; + pub(crate) const MAIN: &str = "main"; pub(crate) fn max_number_of_pages() -> u32 { diff --git a/src/tests/tests_runtime.rs b/src/tests/tests_runtime.rs index 601e6e6a..94e0ca9f 100644 --- a/src/tests/tests_runtime.rs +++ b/src/tests/tests_runtime.rs @@ -388,10 +388,10 @@ fn test_get_current_period_and_thread_wasmv1_as() { #[test] #[serial] -/// wasmv1 modules run below WASMV1_RUNTIME_DISABLED_EXECUTION_VERSION and are refused from it on, +/// wasmv1 modules run below MIP_0002_EXECUTION_VERSION and are refused from it on, /// while AssemblyScript modules keep running. fn test_wasmv1_runtime_disabled_at_version() { - use crate::WASMV1_RUNTIME_DISABLED_EXECUTION_VERSION; + use crate::MIP_0002_EXECUTION_VERSION; use std::sync::atomic::Ordering; let gas_costs = GasCosts::default(); @@ -419,13 +419,10 @@ fn test_wasmv1_runtime_disabled_at_version() { ) }; - INTERFACE_VERSION.store( - WASMV1_RUNTIME_DISABLED_EXECUTION_VERSION - 1, - Ordering::SeqCst, - ); + INTERFACE_VERSION.store(MIP_0002_EXECUTION_VERSION - 1, Ordering::SeqCst); let before = run(wasmv1, 100_000_000); - INTERFACE_VERSION.store(WASMV1_RUNTIME_DISABLED_EXECUTION_VERSION, Ordering::SeqCst); + INTERFACE_VERSION.store(MIP_0002_EXECUTION_VERSION, Ordering::SeqCst); let after = run(wasmv1, 100_000_000); let as_after = run(as_module, 100_000); diff --git a/src/types.rs b/src/types.rs index 5a7323f6..ab323959 100644 --- a/src/types.rs +++ b/src/types.rs @@ -739,22 +739,6 @@ impl Default for GasCosts { } } -/// Execution component version from which wasmv1 modules (bytecode format byte `1`) are no -/// longer executed: every execution of one fails, as if the format were unsupported. Mirrors -/// massa's `MIP_0002_EXECUTION_VERSION` (`MipComponent::Execution` v2); kept as a literal here to -/// avoid a massa-versioning dependency. -pub const WASMV1_RUNTIME_DISABLED_EXECUTION_VERSION: u32 = 2; - -/// Execution component version from which the paginated datastore-key ABIs are -/// exposed to guest modules. Same massa MIP-0002 activation as -/// [`WASMV1_RUNTIME_DISABLED_EXECUTION_VERSION`]: before it, an updated node -/// does not resolve the new imports, so instantiation fails exactly as on a -/// non-updated node. A host that cannot report its version keeps them hidden. -pub const PAGINATED_DS_KEYS_EXECUTION_VERSION: u32 = WASMV1_RUNTIME_DISABLED_EXECUTION_VERSION; - -/// Maximum number of datastore keys one paginated call may return. -pub const MAX_DATASTORE_KEYS_PAGE: u32 = 500; - #[allow(unused_variables)] pub trait Interface: Send + Sync + InterfaceClone { fn increment_recursion_counter(&self) -> Result<()>; @@ -839,26 +823,26 @@ pub trait Interface: Send + Sync + InterfaceClone { /// Return datastore keys. /// Only keys with the given prefix are returned when one is provided. /// - /// Superseded, from [`PAGINATED_DS_KEYS_EXECUTION_VERSION`], by + /// Superseded, from [`crate::MIP_0002_EXECUTION_VERSION`], by /// [`Interface::get_keys_paginated`]. fn get_keys(&self, prefix: Option<&[u8]>) -> Result>>; /// Return datastore keys for an address. /// Only keys with the given prefix are returned when one is provided. /// - /// Superseded, from [`PAGINATED_DS_KEYS_EXECUTION_VERSION`], by + /// Superseded, from [`crate::MIP_0002_EXECUTION_VERSION`], by /// [`Interface::get_keys_for_paginated`]. fn get_keys_for(&self, address: &str, prefix: Option<&[u8]>) -> Result>>; /// Return one page of datastore keys for the current address. /// - /// Guest modules can call this only from [`PAGINATED_DS_KEYS_EXECUTION_VERSION`] + /// Guest modules can call this only from [`crate::MIP_0002_EXECUTION_VERSION`] /// (massa MIP-0002). The AssemblyScript import is omitted before that version. /// /// * `prefix`: only keys with this prefix. `None` matches every key. /// * `start_key`: exclusive resume cursor. `None` starts at the beginning of /// the range. Pass the last key of the previous page to obtain the next one. - /// * `count`: page size, in `1..=`[`MAX_DATASTORE_KEYS_PAGE`]. The ABI rejects + /// * `count`: page size, in `1..=`[`crate::MAX_DATASTORE_KEYS_PAGE`]. The ABI rejects /// any other value. The host must apply the same bound. fn get_keys_paginated( &self,