Skip to content

add metering for bulk memory opcodes#79

Open
laurci wants to merge 14 commits intorc/after-supernovafrom
mem-ops-wasm
Open

add metering for bulk memory opcodes#79
laurci wants to merge 14 commits intorc/after-supernovafrom
mem-ops-wasm

Conversation

@laurci
Copy link
Contributor

@laurci laurci commented Jul 21, 2025

The implementation is mostly complete, but it needs gas testing.

Also contains opcode versioning.

@andrei-marinica andrei-marinica marked this pull request as draft July 21, 2025 11:40
@laurci laurci changed the base branch from rc/v1.7.2 to rc/after-supernova August 20, 2025 07:27
@laurci laurci marked this pull request as ready for review August 20, 2025 07:27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need proper opcode check versioning.
I will be adding it soon.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think we need. Like we might add some opcodes once per year, there is no need for that. SpaceVM can treat activations, there is no need to complicated the executor.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think we need. Like we might add some opcodes once per year, there is no need for that. SpaceVM can treat activations, there is no need to complicated the executor.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds metering support for bulk memory operations (MemoryCopy and MemoryFill) in WebAssembly through an opcode versioning mechanism. The implementation introduces V2 opcodes that include these bulk memory operations, while V1 maintains backward compatibility.

Changes:

  • Introduced opcode versioning (V1/V2) to enable/disable bulk memory operations
  • Added metering for MemoryCopy and MemoryFill with per-byte cost calculation
  • Refactored API from set_opcode_cost to set_opcode_config to include version information

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
vm-executor/src/opcode_version.rs New enum defining opcode versions V1 and V2 with i32 conversion
vm-executor/src/opcode_config.rs New struct combining opcode version and cost configuration
vm-executor/src/opcode_cost.rs Added cost fields for MemoryCopy and MemoryFill operations
vm-executor/src/lib.rs Exported new OpcodeConfig and OpcodeVersion types
vm-executor/src/executor.rs Updated trait method from set_opcode_cost to set_opcode_config
vm-executor-wasmer/src/wasmer_opcode_cost.rs Split opcode cost logic into V1 and V2 functions, added bulk memory ops to V2
vm-executor-wasmer/src/wasmer_metering.rs Added bulk memory metering with size backup global and per-byte cost injection
vm-executor-wasmer/src/wasmer_helpers.rs Added helper to identify supported bulk memory operators
vm-executor-wasmer/src/wasmer_instance.rs Updated to use OpcodeConfig instead of OpcodeCost
vm-executor-wasmer/src/wasmer_executor.rs Updated executor to use OpcodeConfig with improved error handling
vm-executor-wasmer/src/lib.rs Renamed metering_helpers to opcode_cost module
vm-executor-wasmer/tests/opcode_version_test.rs Added test verifying V1 rejects and V2 accepts bulk memory ops
vm-executor-wasmer/tests/memory-copy.wat Test WAT file demonstrating memory.copy operation
vm-executor-wasmer/Cargo.toml Added std feature to log dependency
vm-executor-experimental/* Updated to use OpcodeConfig (but missing bulk memory metering)
c-api/src/capi_metering.rs Updated C API to accept opcode version parameter
c-api/libvmexeccapi.h Updated C header with new function signature

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

pub(crate) fn is_supported_bulk_memory_operator(operator: &Operator) -> bool {
matches!(
operator,
Operator::MemoryCopy { src: _, dst: _ } | Operator::MemoryFill { mem: _ }
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern matching uses named fields (src: _, dst: _) and (mem: _) which are unnecessary since the values are being ignored. This can be simplified to Operator::MemoryCopy { .. } and Operator::MemoryFill { .. } for better readability and consistency with other operator matches in the codebase.

Suggested change
Operator::MemoryCopy { src: _, dst: _ } | Operator::MemoryFill { mem: _ }
Operator::MemoryCopy { .. } | Operator::MemoryFill { .. }

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +7
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpcodeVersion {
V1,
V2,
}

impl OpcodeVersion {
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OpcodeVersion enum and its from_i32 method lack documentation explaining what each version means and when each should be used. Consider adding doc comments to explain that V1 is the legacy version without bulk memory support, and V2 adds support for bulk memory operations like MemoryCopy and MemoryFill.

Suggested change
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpcodeVersion {
V1,
V2,
}
impl OpcodeVersion {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// Specifies which set of opcodes should be used by the VM.
///
/// `V1` is the legacy opcode set that does **not** include support for bulk
/// memory operations. `V2` extends `V1` by adding bulk memory instructions,
/// such as `MemoryCopy` and `MemoryFill`.
pub enum OpcodeVersion {
/// Legacy opcode set without bulk memory support.
///
/// Use this for modules or environments that were compiled or designed
/// before bulk memory operations were introduced, or when compatibility
/// with older tooling is required.
V1,
/// Opcode set with bulk memory support.
///
/// Use this for modules that rely on bulk memory operations like
/// `MemoryCopy` and `MemoryFill`, or when targeting newer runtimes that
/// support these instructions.
V2,
}
impl OpcodeVersion {
/// Converts a numeric opcode version identifier into an `OpcodeVersion`.
///
/// * `0` maps to [`OpcodeVersion::V1`] (legacy, no bulk memory support).
/// * `1` maps to [`OpcodeVersion::V2`] (with bulk memory operations).
///
/// Any other value returns `None`. This is typically used when decoding
/// opcode version information from serialized formats or external
/// configuration that represent the version as an integer.

Copilot uses AI. Check for mistakes.
/// Sets the opcode costs for the given executor.
fn set_opcode_cost(&mut self, opcode_cost: &OpcodeCost) -> Result<(), ExecutorError>;
/// Sets the opcode version and costs for the given executor.
fn set_opcode_config(&mut self, opcode_config: OpcodeConfig) -> Result<(), ExecutorError>;
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking API change: the function signature changed from accepting a reference to OpcodeCost to accepting an owned OpcodeConfig. This will require all consumers of the ExecutorLegacy trait to update their code. Additionally, the function now takes ownership of the OpcodeConfig instead of borrowing it, which changes the calling patterns. Consider whether taking ownership is necessary or if a reference would be more ergonomic.

Suggested change
fn set_opcode_config(&mut self, opcode_config: OpcodeConfig) -> Result<(), ExecutorError>;
fn set_opcode_config(&mut self, opcode_config: &OpcodeConfig) -> Result<(), ExecutorError>;

Copilot uses AI. Check for mistakes.
pub unsafe extern "C" fn vm_exec_set_opcode_costs(
pub unsafe extern "C" fn vm_exec_set_opcode_config(
executor_ptr: *mut vm_exec_executor_t,
opcode_vesion_code: i32,
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter name has a typo: 'opcode_vesion_code' should be 'opcode_version_code'.

Copilot uses AI. Check for mistakes.
value: cost_per_byte as i64,
},
Operator::I64Mul,
// points user += memory size * price
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inject_bulk_memory_cost function assumes the size operand is on top of the stack. However, for MemoryCopy, the WASM stack order is [dst, src, size], and for MemoryFill it's [dst, val, size]. After backing up the size with GlobalSet, the function attempts to restore it with GlobalGet at the end. This is correct, but the comment on line 200 is misleading: it says 'points user +=' but the actual operation is 'points_used += memory_size * price', which should be reflected more clearly in the comment.

Copilot uses AI. Check for mistakes.
with_service(|service| {
service.update_last_error_str(format!(
"invalid opcode version code: {}",
opcode_vesion_code
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter name has a typo: 'opcode_vesion_code' should be 'opcode_version_code'. This is used in the error message formatting.

Copilot uses AI. Check for mistakes.
enum vm_exec_result_t vm_exec_set_opcode_costs(struct vm_exec_executor_t *executor_ptr,
const struct vm_exec_opcode_cost_t *opcode_cost_ptr);
enum vm_exec_result_t vm_exec_set_opcode_config(struct vm_exec_executor_t *executor_ptr,
int32_t opcode_vesion_code,
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter name has a typo: 'opcode_vesion_code' should be 'opcode_version_code'. This typo also affects the C header file.

Suggested change
int32_t opcode_vesion_code,
int32_t opcode_version_code,

Copilot uses AI. Check for mistakes.
match option {
Some(cost) if is_supported_bulk_memory_operator(&operator) => {
self.inject_bulk_memory_cost(state, cost);
// immediatly insert out of gas check as this operation might be expensive
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the comment: 'immediatly' should be 'immediately'.

Suggested change
// immediatly insert out of gas check as this operation might be expensive
// immediately insert out of gas check as this operation might be expensive

Copilot uses AI. Check for mistakes.
// This needs to be done before the metering logic, to prevent operators like `Call` from escaping metering in some
// corner cases.
let option = get_opcode_cost(&operator, &self.opcode_cost);
let option = get_opcode_cost(&operator, &self.opcode_config);
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The experimental executor's metering middleware does not include support for bulk memory operations (MemoryCopy and MemoryFill) that was added to the wasmer executor. This creates an inconsistency between the two implementations where bulk memory operations would not be properly metered in the experimental executor, even when using OpcodeVersion V2.

Suggested change
let option = get_opcode_cost(&operator, &self.opcode_config);
let option = match &operator {
// Explicit support for bulk memory operations to keep metering consistent
Operator::MemoryCopy { .. } => Some(self.opcode_config.opcode_memorycopy),
Operator::MemoryFill { .. } => Some(self.opcode_config.opcode_memoryfill),
_ => get_opcode_cost(&operator, &self.opcode_config),
};

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +34
#[test]
fn opcode_version_test() {
let wasm_bytes = wat2wasm(MEMORY_COPY_WAT).unwrap();

let mut executor = WasmerExecutor::new(Box::new(VMHooksLegacyDefault));
let result = executor.new_instance(&wasm_bytes, &DUMMY_COMPILATION_OPTIONS.to_legacy());
assert!(result.is_err());

executor
.set_opcode_config(OpcodeConfig {
opcode_version: OpcodeVersion::V2,
opcode_cost: OpcodeCost::default(),
})
.unwrap();

let result = executor.new_instance(&wasm_bytes, &DUMMY_COMPILATION_OPTIONS.to_legacy());
assert!(result.is_ok());
}
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test verifies that bulk memory operations are rejected with the default opcode version (V1) and accepted with V2, but it doesn't test that the metering for bulk memory operations actually works correctly. Consider adding a test that verifies the gas consumption for memory.copy and memory.fill operations to ensure the metering implementation is correct.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments