Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 128 additions & 1 deletion src/as_execution/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,106 @@ pub(crate) fn assembly_script_get_keys_for(
})
}

/// Empty buffer means the bound is unset.
fn opt_bound(buffer: &[u8]) -> Option<&[u8]> {
(!buffer.is_empty()).then_some(buffer)
}

/// Page size accepted by the paginated datastore-key ABIs: `1..=MAX_DATASTORE_KEYS_PAGE`.
fn checked_page_count(count: i32) -> ABIResult<u32> {
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 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<ASEnv>,
prefix: i32,
start_key: i32,
count: i32,
) -> ABIResult<i32> {
abi_with_memory!(ctx, assembly_script_get_keys_paginated, |memory| {
let prefix = read_buffer(&memory, &ctx, prefix)?;
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_key),
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_key),
into_trace_value!(count),
],
return_value: fmt_keys.into(),
sub_calls: None,
});
Ok(ptr as i32)
})
}

/// 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<ASEnv>,
address: i32,
prefix: i32,
start_key: i32,
count: i32,
) -> ABIResult<i32> {
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_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_key),
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_for_paginated".to_string(),
params: vec![
into_trace_value!(address),
into_trace_value!(prefix),
into_trace_value!(start_key),
into_trace_value!(count),
],
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(
Expand Down Expand Up @@ -1934,7 +2034,34 @@ pub(crate) fn assembly_script_hash_sha256(

#[cfg(test)]
mod tests {
use crate::as_execution::abi::ser_bytearray_vec;
use super::{checked_page_count, opt_bound, ser_bytearray_vec};
use crate::{GasCosts, MAX_DATASTORE_KEYS_PAGE};

#[test]
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_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_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]
fn test_ser() {
Expand Down
84 changes: 83 additions & 1 deletion src/as_execution/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ impl ASContext {
pub(crate) fn resolver(&self, store: &mut Store) -> (Imports, FunctionEnv<ASEnv>) {
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),
Expand Down Expand Up @@ -289,6 +289,88 @@ 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::MIP_0002_EXECUTION_VERSION
}

#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{TestInterface, INTERFACE_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};
use wasmer_compiler_singlepass::Singlepass;

#[test]
fn test_paginated_ds_keys_gate_boundaries() {
assert!(!paginated_ds_keys_enabled(0));
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));
}

/// 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(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");

INTERFACE_VERSION.store(0, Ordering::SeqCst);

assert!(before_legacy);
assert!(!before_paginated);
assert!(after_paginated);
}
}
4 changes: 2 additions & 2 deletions src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
15 changes: 15 additions & 0 deletions src/settings.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
21 changes: 20 additions & 1 deletion src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Interface> {
Expand Down Expand Up @@ -623,6 +623,25 @@ impl Interface for TestInterface {
todo!()
}

fn get_keys_paginated(
&self,
_prefix: Option<&[u8]>,
_start_key: Option<&[u8]>,
_count: u32,
) -> Result<BTreeSet<Vec<u8>>> {
todo!()
}

fn get_keys_for_paginated(
&self,
_address: &str,
_prefix: Option<&[u8]>,
_start_key: Option<&[u8]>,
_count: u32,
) -> Result<BTreeSet<Vec<u8>>> {
todo!()
}

fn raw_get_bytecode(&self) -> Result<Vec<u8>> {
todo!()
}
Expand Down
11 changes: 4 additions & 7 deletions src/tests/tests_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);

Expand Down
Loading
Loading