diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 87ee57237..6ed568f1a 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -22,6 +22,12 @@ jobs: run: | make test + - name: Cleanup + if: always() + run: | + go clean -testcache + git clean -ffdx + test-macos-arm64: runs-on: [self-hosted, macOS, ARM64] steps: @@ -35,3 +41,9 @@ jobs: - name: Test MacOS ARM64 run: | make test-w2 + + - name: Cleanup + if: always() + run: | + go clean -testcache + git clean -ffdx diff --git a/.gitignore b/.gitignore index 0222b9259..291fbe6b2 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ test/contracts/**/wasm/target **/Cargo.lock vmhost/**/__debug_bin .gocache/ + +# Temporary LLM artifacts +docs diff --git a/codecov.yml b/codecov.yml index 58e85b7e2..1afb38d34 100644 --- a/codecov.yml +++ b/codecov.yml @@ -8,3 +8,4 @@ ignore: - "**test/**" - "**testcommon/**" - "**testgen/**" + - "vmhost/vmhooks/generate/**" diff --git a/config/config.toml b/config/config.toml index b873a641a..73a99255f 100644 --- a/config/config.toml +++ b/config/config.toml @@ -706,7 +706,9 @@ MemoryAtomicWait32 = 1 MemoryAtomicWait64 = 1 MemoryCopy = 1 + MemoryCopyPerByte = 1 MemoryFill = 1 + MemoryFillPerByte = 1 MemoryGrow = 1 MemoryInit = 1 MemorySize = 1 diff --git a/config/gasSchedule.go b/config/gasSchedule.go index 3a14ce80a..13e459d67 100644 --- a/config/gasSchedule.go +++ b/config/gasSchedule.go @@ -1020,7 +1020,9 @@ func FillGasMapWASMOpcodeValues(value uint64) map[string]uint64 { gasMap["MemoryAtomicWait32"] = value gasMap["MemoryAtomicWait64"] = value gasMap["MemoryCopy"] = value + gasMap["MemoryCopyPerByte"] = value gasMap["MemoryFill"] = value + gasMap["MemoryFillPerByte"] = value gasMap["MemoryGrow"] = value gasMap["MemoryInit"] = value gasMap["MemorySize"] = value diff --git a/executor/executor.go b/executor/executor.go index f3c35390b..26b809183 100644 --- a/executor/executor.go +++ b/executor/executor.go @@ -21,8 +21,8 @@ type CompilationOptions struct { type Executor interface { check.NilInterfaceChecker - // SetOpcodeCosts sets gas costs globally inside an executor. - SetOpcodeCosts(opcodeCosts *WASMOpcodeCost) + // SetOpcodeConfig sets gas costs globally inside an executor. + SetOpcodeConfig(opcodeVersion OpcodeVersion, wasmOps *WASMOpcodeCost) error // FunctionNames return the low-level function names provided to contracts. FunctionNames() vmcommon.FunctionNames diff --git a/executor/executorError.go b/executor/executorError.go index 17c1b7b89..c826e860f 100644 --- a/executor/executorError.go +++ b/executor/executorError.go @@ -14,6 +14,13 @@ var ErrFunctionNonvoidSignature = fmt.Errorf("%w (nonvoid signature)", ErrInvali // ErrFuncNotFound signals that the the function does not exist var ErrFuncNotFound = fmt.Errorf("%w (not found)", ErrInvalidFunction) +// ErrNilOpcodeCosts signals that an executor was configured without a WASM opcode cost table. +// +// Rejected rather than defaulted, because an absent table is indistinguishable from one in +// which every opcode costs zero, and that is a contract running for free. OpcodeVersion needs +// no such check: its zero value is OpcodeVersionV1, a real choice. +var ErrNilOpcodeCosts = errors.New("nil WASM opcode costs") + // ErrMemoryBadBounds signals that a certain variable is out of bounds var ErrMemoryBadBounds = errors.New("bad bounds") diff --git a/executor/executorFactory.go b/executor/executorFactory.go index 28b4b13d6..368b4cc3d 100644 --- a/executor/executorFactory.go +++ b/executor/executorFactory.go @@ -5,6 +5,7 @@ import "github.com/multiversx/mx-chain-core-go/core/check" // ExecutorFactoryArgs define the Executor configurations that come from the VM, especially the hooks and the gas costs. type ExecutorFactoryArgs struct { VMHooks VMHooks + OpcodeVersion OpcodeVersion OpcodeCosts *WASMOpcodeCost RkyvSerializationEnabled bool WasmerSIGSEGVPassthrough bool diff --git a/executor/executorOpcodeVersion.go b/executor/executorOpcodeVersion.go new file mode 100644 index 000000000..ee6e72f2e --- /dev/null +++ b/executor/executorOpcodeVersion.go @@ -0,0 +1,38 @@ +package executor + +// OpcodeVersion specifies which set of opcodes should be used by the VM. +// +// [OpcodeVersionV1] is the legacy opcode set that does not include support +// for bulk memory operations. [OpcodeVersionV2] extends it by adding bulk +// memory instructions, such as MemoryCopy and MemoryFill, while dropping +// these categories from the whitelist, since no contract built with the +// current SDK toolchain can emit any of them: +// +// - legacy exception handling: Catch, CatchAll, Delegate, Rethrow, Throw, Try +// - reference types: RefFunc, RefIsNull, RefNull, TypedSelect +type OpcodeVersion uint32 + +const ( + // OpcodeVersionV1 is the 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. + OpcodeVersionV1 OpcodeVersion = iota + + // OpcodeVersionV2 is the 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. + // + // Relative to OpcodeVersionV1, OpcodeVersionV2 also drops these + // categories from the whitelist, since no contract built with today's + // tooling can produce them: + // + // - legacy exception handling: Catch, CatchAll, Delegate, Rethrow, Throw, Try + // - reference types: RefFunc, RefIsNull, RefNull, TypedSelect + // + // Note: OpcodeVersionV2 does not add support for memory.init and data.drop. + OpcodeVersionV2 +) diff --git a/executor/gasCostWASM.go b/executor/gasCostWASM.go index 9d3128282..23bf44651 100644 --- a/executor/gasCostWASM.go +++ b/executor/gasCostWASM.go @@ -503,7 +503,9 @@ type WASMOpcodeCost struct { MemoryAtomicWait32 uint32 MemoryAtomicWait64 uint32 MemoryCopy uint32 + MemoryCopyPerByte uint32 MemoryFill uint32 + MemoryFillPerByte uint32 MemoryGrow uint32 MemoryInit uint32 MemorySize uint32 diff --git a/executor/wrapper/wrapperExecutor.go b/executor/wrapper/wrapperExecutor.go index a8d146dde..ab6d48c10 100644 --- a/executor/wrapper/wrapperExecutor.go +++ b/executor/wrapper/wrapperExecutor.go @@ -15,9 +15,9 @@ type WrapperExecutor struct { WrappedInstances map[string][]executor.Instance } -// SetOpcodeCosts wraps the call to the underlying executor. -func (wexec *WrapperExecutor) SetOpcodeCosts(opcodeCosts *executor.WASMOpcodeCost) { - wexec.wrappedExecutor.SetOpcodeCosts(opcodeCosts) +// SetOpcodeConfig wraps the call to the underlying executor. +func (wexec *WrapperExecutor) SetOpcodeConfig(opcodeVersion executor.OpcodeVersion, wasmOps *executor.WASMOpcodeCost) error { + return wexec.wrappedExecutor.SetOpcodeConfig(opcodeVersion, wasmOps) } // FunctionNames wraps the call to the underlying executor. diff --git a/executor/wrapper/wrapperExecutorFactory.go b/executor/wrapper/wrapperExecutorFactory.go index 8e64990b7..7ba7c3053 100644 --- a/executor/wrapper/wrapperExecutorFactory.go +++ b/executor/wrapper/wrapperExecutorFactory.go @@ -1,9 +1,19 @@ package executorwrapper import ( + "errors" + + "github.com/multiversx/mx-chain-core-go/core/check" "github.com/multiversx/mx-chain-vm-go/executor" ) +// ErrNilWrappedExecutorFactory signals that the wrapper was built around no factory at all. +var ErrNilWrappedExecutorFactory = errors.New("nil wrapped executor factory") + +// ErrNilExecutorLogger signals that the wrapper was built without a logger. SimpleWrappedExecutorFactory +// supplies NoLogger for callers that want no logging; nil is not that. +var ErrNilExecutorLogger = errors.New("nil executor logger") + // WrapperExecutorFactory is the factory for the WrapperExecutor. type WrapperExecutorFactory struct { logger ExecutorLogger @@ -29,12 +39,24 @@ func SimpleWrappedExecutorFactory(wrappedFactory executor.ExecutorAbstractFactor } // CreateExecutor creates a new Executor instance. +// +// The dependencies are checked here rather than in NewWrappedExecutorFactory, which has no +// error to return: the factory struct is inert, and this is the first point at which a +// missing one would be used. func (factory *WrapperExecutorFactory) CreateExecutor(args executor.ExecutorFactoryArgs) (executor.Executor, error) { + if check.IfNil(factory.wrappedFactory) { + return nil, ErrNilWrappedExecutorFactory + } + if factory.logger == nil { + return nil, ErrNilExecutorLogger + } + wrappedExecutor, err := factory.wrappedFactory.CreateExecutor(executor.ExecutorFactoryArgs{ VMHooks: &WrapperVMHooks{ logger: factory.logger, wrappedVMHooks: args.VMHooks, }, + OpcodeVersion: args.OpcodeVersion, OpcodeCosts: args.OpcodeCosts, RkyvSerializationEnabled: args.RkyvSerializationEnabled, WasmerSIGSEGVPassthrough: args.WasmerSIGSEGVPassthrough, diff --git a/executor/wrapper/wrapperExecutorFactory_test.go b/executor/wrapper/wrapperExecutorFactory_test.go new file mode 100644 index 000000000..e2fdee11e --- /dev/null +++ b/executor/wrapper/wrapperExecutorFactory_test.go @@ -0,0 +1,26 @@ +package executorwrapper + +import ( + "testing" + + "github.com/multiversx/mx-chain-vm-go/executor" + "github.com/stretchr/testify/require" +) + +func TestWrapperExecutorFactory_CreateExecutor_MissingDependencies(t *testing.T) { + t.Run("nil wrapped factory", func(t *testing.T) { + factory := SimpleWrappedExecutorFactory(nil) + + exec, err := factory.CreateExecutor(executor.ExecutorFactoryArgs{}) + require.ErrorIs(t, err, ErrNilWrappedExecutorFactory) + require.Nil(t, exec) + }) + + t.Run("nil logger", func(t *testing.T) { + factory := NewWrappedExecutorFactory(nil, SimpleWrappedExecutorFactory(nil)) + + exec, err := factory.CreateExecutor(executor.ExecutorFactoryArgs{}) + require.ErrorIs(t, err, ErrNilExecutorLogger) + require.Nil(t, exec) + }) +} diff --git a/mock/context/executorMock.go b/mock/context/executorMock.go index 41d0bdcee..53f94166c 100644 --- a/mock/context/executorMock.go +++ b/mock/context/executorMock.go @@ -55,8 +55,9 @@ func NewExecutorMock(world *worldmock.MockWorld) *ExecutorMock { } } -// SetOpcodeCosts should set gas costs, but it does nothing in the case of this mock. -func (executorMock *ExecutorMock) SetOpcodeCosts(_ *executor.WASMOpcodeCost) { +// SetOpcodeConfig should set gas costs, but it does nothing in the case of this mock. +func (executorMock *ExecutorMock) SetOpcodeConfig(_ executor.OpcodeVersion, _ *executor.WASMOpcodeCost) error { + return nil } // FunctionNames mocked method diff --git a/scenario/gasSchedules/gasScheduleEmbedGenerated.go b/scenario/gasSchedules/gasScheduleEmbedGenerated.go index 816a12ebf..965016d54 100644 --- a/scenario/gasSchedules/gasScheduleEmbedGenerated.go +++ b/scenario/gasSchedules/gasScheduleEmbedGenerated.go @@ -123,10 +123,10 @@ const ( GetCodeHash = 100 IsBuiltinFunction = 100 IsReservedFunctionName = 100 - GetRoundTime = 100 - EpochStartBlockTimeStamp = 100 - EpochStartBlockNonce = 100 - EpochStartBlockRound = 100 + GetRoundTime = 10000 + EpochStartBlockTimeStamp = 10000 + EpochStartBlockNonce = 10000 + EpochStartBlockRound = 10000 [EthAPICost] UseGas = 100 @@ -702,7 +702,9 @@ const ( MaxMemoryGrow = 8 MaxMemoryGrowDelta = 10 MemoryCopy = 5 + MemoryCopyPerByte = 5 MemoryFill = 5 + MemoryFillPerByte = 5 MemoryGrow = 5 MemoryInit = 5 MemorySize = 5 @@ -971,10 +973,10 @@ const ( GetCodeHash = 100 IsBuiltinFunction = 100 IsReservedFunctionName = 100 - GetRoundTime = 100 - EpochStartBlockTimeStamp = 100 - EpochStartBlockNonce = 100 - EpochStartBlockRound = 100 + GetRoundTime = 10000 + EpochStartBlockTimeStamp = 10000 + EpochStartBlockNonce = 10000 + EpochStartBlockRound = 10000 [EthAPICost] UseGas = 100 @@ -1550,7 +1552,9 @@ const ( MaxMemoryGrow = 100 MaxMemoryGrowDelta = 1 MemoryCopy = 5 + MemoryCopyPerByte = 5 MemoryFill = 5 + MemoryFillPerByte = 5 MemoryGrow = 5 MemoryInit = 5 MemorySize = 5 diff --git a/scenario/gasSchedules/gasScheduleV3.toml b/scenario/gasSchedules/gasScheduleV3.toml index 12b246604..317c38f0d 100644 --- a/scenario/gasSchedules/gasScheduleV3.toml +++ b/scenario/gasSchedules/gasScheduleV3.toml @@ -690,7 +690,9 @@ MaxMemoryGrow = 8 MaxMemoryGrowDelta = 10 MemoryCopy = 5 + MemoryCopyPerByte = 5 MemoryFill = 5 + MemoryFillPerByte = 5 MemoryGrow = 5 MemoryInit = 5 MemorySize = 5 diff --git a/scenario/gasSchedules/gasScheduleV4.toml b/scenario/gasSchedules/gasScheduleV4.toml index 4d10c167a..4ae739434 100644 --- a/scenario/gasSchedules/gasScheduleV4.toml +++ b/scenario/gasSchedules/gasScheduleV4.toml @@ -692,7 +692,9 @@ MaxMemoryGrow = 100 MaxMemoryGrowDelta = 1 MemoryCopy = 5 + MemoryCopyPerByte = 5 MemoryFill = 5 + MemoryFillPerByte = 5 MemoryGrow = 5 MemoryInit = 5 MemorySize = 5 diff --git a/test/contracts/bulk-memory/README.md b/test/contracts/bulk-memory/README.md new file mode 100644 index 000000000..9a9205500 --- /dev/null +++ b/test/contracts/bulk-memory/README.md @@ -0,0 +1,14 @@ +# bulk-memory + +Contract used to measure the gas cost of the bulk memory opcodes, `memory.copy` and `memory.fill`. + +Every endpoint takes the number of bytes to copy/fill as argument 0. Since the rest of the +code path is identical no matter the argument, the gas difference between two calls is exactly +`(bytes1 - bytes2) * PerByte`, which is what the tests in +`vmhost/hosttest/bulkMemoryGas_test.go` assert. + +Built directly from WAT, without `wasm-opt`, so that the opcodes stay exactly as written: + +``` +./build.sh +``` diff --git a/test/contracts/bulk-memory/build.sh b/test/contracts/bulk-memory/build.sh new file mode 100755 index 000000000..0fe8736b6 --- /dev/null +++ b/test/contracts/bulk-memory/build.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Built directly from WAT, no wasm-opt, so that the opcodes stay exactly as written. + +set -e + +cd "$(dirname "$0")" +mkdir -p output +wat2wasm bulk-memory.wat -o output/bulk-memory.wasm diff --git a/test/contracts/bulk-memory/bulk-memory.wat b/test/contracts/bulk-memory/bulk-memory.wat new file mode 100644 index 000000000..55a211682 --- /dev/null +++ b/test/contracts/bulk-memory/bulk-memory.wat @@ -0,0 +1,118 @@ +;; Contract used to measure the gas cost of the bulk memory opcodes. +;; +;; Each endpoint takes the number of bytes to copy/fill as argument 0, so that the +;; same code path can be executed with different sizes. Everything apart from the +;; bulk memory opcode itself costs the same on every call, which means that the +;; difference in gas between two calls is exactly +;; (bytes1 - bytes2) * PerByte +;; +;; Memory is 2 pages, the second one is the source of the copy, so that source and +;; destination never overlap, for sizes up to 65536. +(module + (type $void (func)) + (type $getArgument (func (param i32) (result i64))) + + (type $finish (func (param i32 i32))) + + (import "env" "int64getArgument" (func $int64getArgument (type $getArgument))) + (import "env" "finish" (func $finish (type $finish))) + + (memory $mem 2) + + (func $init (type $void)) + + ;; number of bytes to copy comes from argument 0 + (func $memoryCopy (type $void) + (memory.copy + (i32.const 0) ;; destination address, first page + (i32.const 65536) ;; source address, second page + (i32.wrap_i64 (call $int64getArgument (i32.const 0))) + ) + ) + + ;; number of bytes to fill comes from argument 0 + (func $memoryFill (type $void) + (memory.fill + (i32.const 0) ;; destination address, first page + (i32.const 7) ;; value to fill with + (i32.wrap_i64 (call $int64getArgument (i32.const 0))) + ) + ) + + ;; same as $memoryCopy, but the size goes through a local first, to check that the + ;; metering injection puts the size operand back on the stack unchanged + (func $memoryCopyViaLocal (type $void) (local $size i32) + (local.set $size (i32.wrap_i64 (call $int64getArgument (i32.const 0)))) + (memory.copy + (i32.const 0) ;; destination address, first page + (i32.const 65536) ;; source address, second page + (local.get $size) + ) + ) + + ;; same as $memoryFill, but the size goes through a local first + (func $memoryFillViaLocal (type $void) (local $size i32) + (local.set $size (i32.wrap_i64 (call $int64getArgument (i32.const 0)))) + (memory.fill + (i32.const 0) ;; destination address, first page + (i32.const 7) ;; value to fill with + (local.get $size) + ) + ) + + ;; everything the *ViaLocal endpoints do, except the bulk memory opcode and its three + ;; operands, so that the base cost of the opcode can be isolated by subtraction. Also + ;; proves that decoding the argument costs the same regardless of its value. + (func $noBulkMemory (type $void) (local $size i32) + (local.set $size (i32.wrap_i64 (call $int64getArgument (i32.const 0)))) + ) + +;; Fills the destination region [0, 1100) with 0xAA and the source region starting at + ;; 65536 with the non-zero pattern (i % 251) + 1, using plain loops so that the setup does + ;; not depend on the opcodes under test. + (func $prepareMemory (local $i i32) + (loop $dst + (i32.store8 (local.get $i) (i32.const 170)) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if $dst (i32.lt_u (local.get $i) (i32.const 1100))) + ) + (local.set $i (i32.const 0)) + (loop $src + (i32.store8 + (i32.add (i32.const 65536) (local.get $i)) + (i32.add (i32.rem_u (local.get $i) (i32.const 251)) (i32.const 1))) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if $src (i32.lt_u (local.get $i) (i32.const 1100))) + ) + ) + + ;; prepares memory, copies argument 0 bytes, then returns the first 1100 bytes of memory + (func $memoryCopyCheck (type $void) + (call $prepareMemory) + (memory.copy + (i32.const 0) + (i32.const 65536) + (i32.wrap_i64 (call $int64getArgument (i32.const 0)))) + (call $finish (i32.const 0) (i32.const 1100)) + ) + + ;; prepares memory, fills argument 0 bytes with 7, then returns the first 1100 bytes + (func $memoryFillCheck (type $void) + (call $prepareMemory) + (memory.fill + (i32.const 0) + (i32.const 7) + (i32.wrap_i64 (call $int64getArgument (i32.const 0)))) + (call $finish (i32.const 0) (i32.const 1100)) + ) + + (export "memory" (memory $mem)) + (export "memoryCopyCheck" (func $memoryCopyCheck)) + (export "memoryFillCheck" (func $memoryFillCheck)) + (export "init" (func $init)) + (export "memoryCopy" (func $memoryCopy)) + (export "memoryFill" (func $memoryFill)) + (export "memoryCopyViaLocal" (func $memoryCopyViaLocal)) + (export "memoryFillViaLocal" (func $memoryFillViaLocal)) + (export "noBulkMemory" (func $noBulkMemory)) +) diff --git a/test/contracts/bulk-memory/output/bulk-memory.wasm b/test/contracts/bulk-memory/output/bulk-memory.wasm new file mode 100644 index 000000000..c3df0b875 Binary files /dev/null and b/test/contracts/bulk-memory/output/bulk-memory.wasm differ diff --git a/test/contracts/ref-types/build.sh b/test/contracts/ref-types/build.sh new file mode 100755 index 000000000..8ece56911 --- /dev/null +++ b/test/contracts/ref-types/build.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Built directly from WAT, no wasm-opt, so that the opcodes stay exactly as written. + +set -e + +cd "$(dirname "$0")" +mkdir -p output +wat2wasm ref-types.wat -o output/ref-types.wasm diff --git a/test/contracts/ref-types/output/ref-types.wasm b/test/contracts/ref-types/output/ref-types.wasm new file mode 100644 index 000000000..664ee21c7 Binary files /dev/null and b/test/contracts/ref-types/output/ref-types.wasm differ diff --git a/test/contracts/ref-types/ref-types.wat b/test/contracts/ref-types/ref-types.wat new file mode 100644 index 000000000..feef656bf --- /dev/null +++ b/test/contracts/ref-types/ref-types.wat @@ -0,0 +1,13 @@ +;; Uses reference types, which the V1 opcode whitelist accepts and the V2 one drops. +;; Used to check that code cached under V1 is not run under V2. +(module + (type $void (func)) + (memory $mem 2) + (func $init (type $void)) + (func $isNull (type $void) + (drop (ref.is_null (ref.null func))) + ) + (export "memory" (memory $mem)) + (export "init" (func $init)) + (export "isNull" (func $isNull)) +) diff --git a/vmhost/contexts/async_test.go b/vmhost/contexts/async_test.go index 49d9c696e..a3fc2f7ae 100644 --- a/vmhost/contexts/async_test.go +++ b/vmhost/contexts/async_test.go @@ -2,7 +2,6 @@ package contexts import ( "errors" - "github.com/multiversx/mx-chain-vm-go/wasmer2" "math/big" "testing" @@ -58,9 +57,6 @@ func initializeVMAndWasmerAsyncContextWithBuiltIn(tb testing.TB, isBuiltinFunc b gasSchedule := config.MakeGasMapForTests() gasCostConfig, err := config.CreateGasConfig(gasSchedule) require.Nil(tb, err) - wasmerExecutor, _ := wasmer2.CreateExecutor() - wasmerExecutor.SetOpcodeCosts(gasCostConfig.WASMOpcodeCost) - host := &contextmock.VMHostMock{ EnableEpochsHandlerField: &worldmock.EnableEpochsHandlerStub{}, } @@ -76,8 +72,9 @@ func initializeVMAndWasmerAsyncContextWithBuiltIn(tb testing.TB, isBuiltinFunc b mockWasmerInstance = contextmock.NewInstanceMock(nil) execFactory := testexecutor.NewDefaultTestExecutorFactory(tb) exec, err := execFactory.CreateExecutor(executor.ExecutorFactoryArgs{ - VMHooks: vmhooks.NewVMHooksImpl(host), - OpcodeCosts: gasCostConfig.WASMOpcodeCost, + VMHooks: vmhooks.NewVMHooksImpl(host), + OpcodeVersion: executor.OpcodeVersionV2, + OpcodeCosts: gasCostConfig.WASMOpcodeCost, }) require.Nil(tb, err) runtimeCtx, err := NewRuntimeContext( diff --git a/vmhost/contexts/runtime_test.go b/vmhost/contexts/runtime_test.go index 8099ba7dc..0f58045cb 100644 --- a/vmhost/contexts/runtime_test.go +++ b/vmhost/contexts/runtime_test.go @@ -32,9 +32,6 @@ var vmType = []byte("type") func InitializeVMAndWasmer() *contextmock.VMHostMock { gasSchedule := config.MakeGasMapForTests() - gasCostConfig, _ := config.CreateGasConfig(gasSchedule) - wasmerExecutor, _ := wasmer2.CreateExecutor() - wasmerExecutor.SetOpcodeCosts(gasCostConfig.WASMOpcodeCost) host := &contextmock.VMHostMock{} @@ -47,11 +44,23 @@ func InitializeVMAndWasmer() *contextmock.VMHostMock { return host } +// testExecutorFactoryArgs builds what hostCore.createExecutor builds in production: the VM +// hooks and the opcode configuration. The cost table is not optional - an executor without +// one meters every opcode at zero, so the contracts the test runs would be free. +func testExecutorFactoryArgs(tb testing.TB, host vmhost.VMHost) executor.ExecutorFactoryArgs { + gasCostConfig, err := config.CreateGasConfig(config.MakeGasMapForTests()) + require.Nil(tb, err) + + return executor.ExecutorFactoryArgs{ + VMHooks: vmhooks.NewVMHooksImpl(host), + OpcodeVersion: executor.OpcodeVersionV2, + OpcodeCosts: gasCostConfig.WASMOpcodeCost, + } +} + func makeDefaultRuntimeContext(t *testing.T, host vmhost.VMHost) *runtimeContext { execFactory := testexecutor.NewDefaultTestExecutorFactory(t) - exec, err := execFactory.CreateExecutor(executor.ExecutorFactoryArgs{ - VMHooks: vmhooks.NewVMHooksImpl(host), - }) + exec, err := execFactory.CreateExecutor(testExecutorFactoryArgs(t, host)) require.Nil(t, err) runtimeCtx, err := NewRuntimeContext( host, @@ -71,9 +80,7 @@ func TestNewRuntimeContextErrors(t *testing.T) { hasher := defaultHasher execFactory := testexecutor.NewDefaultTestExecutorFactory(t) - exec, err := execFactory.CreateExecutor(executor.ExecutorFactoryArgs{ - VMHooks: vmhooks.NewVMHooksImpl(host), - }) + exec, err := execFactory.CreateExecutor(testExecutorFactoryArgs(t, host)) require.Nil(t, err) t.Run("NilHost", func(t *testing.T) { @@ -362,9 +369,7 @@ func TestRuntimeContext_CountContractInstancesOnStack(t *testing.T) { testVMType := []byte("type") execFactory := testexecutor.NewDefaultTestExecutorFactory(t) - exec, err := execFactory.CreateExecutor(executor.ExecutorFactoryArgs{ - VMHooks: vmhooks.NewVMHooksImpl(host), - }) + exec, err := execFactory.CreateExecutor(testExecutorFactoryArgs(t, host)) require.Nil(t, err) runtime, _ := NewRuntimeContext( host, @@ -477,6 +482,25 @@ func TestRuntimeContext_Instance(t *testing.T) { require.Nil(t, runtimeCtx.iTracker.instance) } +// An executor built without a cost table does not fail - it prices every opcode at zero, and +// the contracts it runs cost nothing. That is what makes the omission worth a test of its +// own: nothing else here would notice. +func TestRuntimeContext_InstancesAreMetered(t *testing.T) { + host := InitializeVMAndWasmer() + runtimeCtx := makeDefaultRuntimeContext(t, host) + defer runtimeCtx.ClearWarmInstanceCache() + + runtimeCtx.SetMaxInstanceStackSize(1) + + contractCode := vmhost.GetSCCode(counterWasmCode) + err := runtimeCtx.StartWasmerInstance(contractCode, uint64(100000000), false) + require.Nil(t, err) + require.Zero(t, runtimeCtx.GetPointsUsed()) + + require.Nil(t, runtimeCtx.GetInstance().CallFunction("init")) + require.NotZero(t, runtimeCtx.GetPointsUsed(), "the instance was compiled unmetered") +} + func TestRuntimeContext_Breakpoints(t *testing.T) { host := InitializeVMAndWasmer() runtimeCtx := makeDefaultRuntimeContext(t, host) diff --git a/vmhost/flags.go b/vmhost/flags.go index 71d91ba15..f780e9c70 100644 --- a/vmhost/flags.go +++ b/vmhost/flags.go @@ -39,5 +39,9 @@ const ( // OutputRollbackOnDestContextFlag defines the flag that activates output state rollback on failed destination context calls OutputRollbackOnDestContextFlag core.EnableEpochFlag = "OutputRollbackOnDestContextFlag" + // OpcodeV2Flag defines the flag that switches the executor to the V2 opcode version, + // which whitelists and meters the bulk memory opcodes + OpcodeV2Flag core.EnableEpochFlag = "OpcodeV2Flag" + // all new flags must be added to allFlags slice from hostCore/host ) diff --git a/vmhost/hostCore/host.go b/vmhost/hostCore/host.go index abf9ba0bd..ec41c4921 100644 --- a/vmhost/hostCore/host.go +++ b/vmhost/hostCore/host.go @@ -51,6 +51,7 @@ var allFlags = []core.EnableEpochFlag{ vmhost.AttributeExtraGasUsageFlag, vmhost.FixCorrectSmallIntAndGasLimitsFlag, vmhost.OutputRollbackOnDestContextFlag, + vmhost.OpcodeV2Flag, } // vmHost implements HostContext interface. @@ -71,6 +72,7 @@ type vmHost struct { managedTypesContext vmhost.ManagedTypesContext gasSchedule config.GasScheduleMap + opcodeVersion executor.OpcodeVersion builtInFuncContainer vmcommon.BuiltInFunctionContainer esdtTransferParser vmcommon.ESDTTransferParser callArgsParser vmhost.CallArgsParser @@ -197,6 +199,12 @@ func NewVMHost( host.runtimeContext.SetMaxInstanceStackSize(MaximumRuntimeInstanceStackSize) + // Cached artifacts are keyed by code hash only, so the opcode version and gas costs they + // were compiled under are not recorded with them. The node already clears the cache on + // startup and before replacing a host; this guards embedders that create a host over a + // blockchain hook whose cache was not cleared. + host.blockchainContext.ClearCompiledCodes() + host.initContexts() hostParameters.EpochNotifier.RegisterNotifyHandler(host) @@ -226,8 +234,12 @@ func (host *vmHost) createExecutor(hostParameters *vmhost.VMHostParameters) (exe } else { vmExecutorFactory = wasmer2.ExecutorFactory() } + + host.opcodeVersion = host.getOpcodeVersionForCurrentEpoch() + vmExecutorFactoryArgs := executor.ExecutorFactoryArgs{ VMHooks: vmHooks, + OpcodeVersion: host.opcodeVersion, OpcodeCosts: gasCostConfig.WASMOpcodeCost, RkyvSerializationEnabled: true, WasmerSIGSEGVPassthrough: hostParameters.WasmerSIGSEGVPassthrough, @@ -354,22 +366,79 @@ func (host *vmHost) ClearContextStateStack() { host.blockchainContext.ClearStateStack() } +func (host *vmHost) getOpcodeVersionForCurrentEpoch() executor.OpcodeVersion { + if host.enableEpochsHandler.IsFlagEnabled(vmhost.OpcodeV2Flag) { + return executor.OpcodeVersionV2 + } + + return executor.OpcodeVersionV1 +} + +// applyExecutionConfig pushes the given gas schedule and opcode version to the executor and +// to the metering context. +// +// The opcode costs and the opcode version are equivalent as far as caching is concerned: +// both are baked into compiled code, the costs by the metering injected at compile time and +// the version by the opcode whitelist applied at compile time. Changing either one makes +// every artifact compiled so far stale, so both of the caches holding such artifacts - the +// warm instances and the precompiled code - are discarded here, before any subsequent +// compilation can pick a stale one up. That is why the two are applied together, by this +// single function, rather than by two paths that would have to be kept in agreement. +// +// Must be called under host.mutExecution. +func (host *vmHost) applyExecutionConfig( + opcodeVersion executor.OpcodeVersion, + gasSchedule config.GasScheduleMap, +) error { + gasCostConfig, err := config.CreateGasConfig(gasSchedule) + if err != nil { + return err + } + + err = host.runtimeContext.GetVMExecutor().SetOpcodeConfig(opcodeVersion, gasCostConfig.WASMOpcodeCost) + if err != nil { + return err + } + + host.gasSchedule = gasSchedule + host.opcodeVersion = opcodeVersion + + host.meteringContext.SetGasSchedule(gasSchedule) + + host.runtimeContext.ClearWarmInstanceCache() + host.blockchainContext.ClearCompiledCodes() + + return nil +} + +// reconfigureExecutorOpcodeVersion re-applies the execution config whenever the opcode +// version of the current epoch is no longer the one the executor was configured with. The +// executor only samples the version when the host creates it, so a host that outlives the +// activation epoch of a version flag would otherwise keep running the old version. +func (host *vmHost) reconfigureExecutorOpcodeVersion() { + host.mutExecution.Lock() + defer host.mutExecution.Unlock() + + opcodeVersion := host.getOpcodeVersionForCurrentEpoch() + if opcodeVersion == host.opcodeVersion { + return + } + + err := host.applyExecutionConfig(opcodeVersion, host.gasSchedule) + if err != nil { + log.Error("cannot apply new opcode version", "err", err) + } +} + // GasScheduleChange applies a new gas schedule to the host func (host *vmHost) GasScheduleChange(newGasSchedule config.GasScheduleMap) { host.mutExecution.Lock() defer host.mutExecution.Unlock() - host.gasSchedule = newGasSchedule - gasCostConfig, err := config.CreateGasConfig(newGasSchedule) + err := host.applyExecutionConfig(host.getOpcodeVersionForCurrentEpoch(), newGasSchedule) if err != nil { log.Error("cannot apply new gas config", "err", err) - return } - - host.runtimeContext.GetVMExecutor().SetOpcodeCosts(gasCostConfig.WASMOpcodeCost) - - host.meteringContext.SetGasSchedule(newGasSchedule) - host.runtimeContext.ClearWarmInstanceCache() } // GetGasScheduleMap returns the currently stored gas schedule @@ -650,6 +719,10 @@ func (host *vmHost) SetBuiltInFunctionsContainer(builtInFuncs vmcommon.BuiltInFu // EpochConfirmed is called whenever a new epoch is confirmed func (host *vmHost) EpochConfirmed(epoch uint32, _ uint64) { + // the executor samples the opcode version when the host creates it, so a host that + // outlives the activation epoch of OpcodeV2Flag must be told about the new version + host.reconfigureExecutorOpcodeVersion() + _, ok := host.activationEpochMap[epoch] if ok { host.Runtime().ClearWarmInstanceCache() diff --git a/vmhost/hostCore/host_test.go b/vmhost/hostCore/host_test.go index 21e9f4804..f11d6f03a 100644 --- a/vmhost/hostCore/host_test.go +++ b/vmhost/hostCore/host_test.go @@ -1,6 +1,7 @@ package hostCore import ( + "errors" "math" "testing" @@ -9,6 +10,8 @@ import ( vmcommon "github.com/multiversx/mx-chain-vm-common-go" "github.com/multiversx/mx-chain-vm-common-go/builtInFunctions" "github.com/multiversx/mx-chain-vm-common-go/parsers" + "github.com/multiversx/mx-chain-vm-go/config" + "github.com/multiversx/mx-chain-vm-go/executor" "github.com/multiversx/mx-chain-vm-go/vmhost" "github.com/multiversx/mx-chain-vm-go/vmhost/mock" "github.com/stretchr/testify/require" @@ -113,3 +116,160 @@ func TestValidateVMInput(t *testing.T) { err = validateVMInput(vmInput) require.Nil(t, err) } + +type opcodeConfigExecutorStub struct { + executor.Executor + setOpcodeConfigErr error + version executor.OpcodeVersion + calls int +} + +func (e *opcodeConfigExecutorStub) SetOpcodeConfig(version executor.OpcodeVersion, _ *executor.WASMOpcodeCost) error { + e.calls++ + if e.setOpcodeConfigErr != nil { + return e.setOpcodeConfigErr + } + e.version = version + return nil +} + +type opcodeConfigRuntimeStub struct { + vmhost.RuntimeContext + exec executor.Executor + warmCacheWipe int +} + +func (r *opcodeConfigRuntimeStub) GetVMExecutor() executor.Executor { return r.exec } +func (r *opcodeConfigRuntimeStub) ClearWarmInstanceCache() { r.warmCacheWipe++ } + +type opcodeConfigMeteringStub struct { + vmhost.MeteringContext + schedule config.GasScheduleMap +} + +func (m *opcodeConfigMeteringStub) SetGasSchedule(schedule config.GasScheduleMap) { + m.schedule = schedule +} + +type opcodeConfigBlockchainStub struct { + vmhost.BlockchainContext + codesCleared int +} + +func (b *opcodeConfigBlockchainStub) ClearCompiledCodes() { b.codesCleared++ } + +func newOpcodeConfigTestHost(v2Enabled *bool) (*vmHost, *opcodeConfigExecutorStub, *opcodeConfigRuntimeStub, *opcodeConfigMeteringStub, *opcodeConfigBlockchainStub) { + exec := &opcodeConfigExecutorStub{} + runtime := &opcodeConfigRuntimeStub{exec: exec} + metering := &opcodeConfigMeteringStub{} + blockchain := &opcodeConfigBlockchainStub{} + host := &vmHost{ + runtimeContext: runtime, + meteringContext: metering, + blockchainContext: blockchain, + enableEpochsHandler: &worldmock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + return flag == vmhost.OpcodeV2Flag && *v2Enabled + }, + }, + activationEpochMap: map[uint32]struct{}{}, + } + return host, exec, runtime, metering, blockchain +} + +func TestVMHost_GetOpcodeVersionForCurrentEpoch(t *testing.T) { + v2 := false + host, _, _, _, _ := newOpcodeConfigTestHost(&v2) + + require.Equal(t, executor.OpcodeVersionV1, host.getOpcodeVersionForCurrentEpoch()) + v2 = true + require.Equal(t, executor.OpcodeVersionV2, host.getOpcodeVersionForCurrentEpoch()) +} + +func TestVMHost_ApplyExecutionConfig(t *testing.T) { + v2 := false + + t.Run("success clears caches", func(t *testing.T) { + host, exec, runtime, metering, blockchain := newOpcodeConfigTestHost(&v2) + schedule := config.MakeGasMapForTests() + + err := host.applyExecutionConfig(executor.OpcodeVersionV2, schedule) + require.Nil(t, err) + require.Equal(t, executor.OpcodeVersionV2, exec.version) + require.Equal(t, executor.OpcodeVersionV2, host.opcodeVersion) + require.Equal(t, schedule, host.gasSchedule) + require.Equal(t, schedule, metering.schedule) + require.Equal(t, 1, runtime.warmCacheWipe) + require.Equal(t, 1, blockchain.codesCleared) + }) + t.Run("invalid gas schedule", func(t *testing.T) { + host, exec, runtime, _, blockchain := newOpcodeConfigTestHost(&v2) + + err := host.applyExecutionConfig(executor.OpcodeVersionV2, config.GasScheduleMap{}) + require.NotNil(t, err) + require.Equal(t, 0, exec.calls) + require.Equal(t, 0, runtime.warmCacheWipe) + require.Equal(t, 0, blockchain.codesCleared) + }) + t.Run("executor error leaves state untouched", func(t *testing.T) { + host, exec, runtime, metering, blockchain := newOpcodeConfigTestHost(&v2) + exec.setOpcodeConfigErr = errors.New("set opcode config failed") + + err := host.applyExecutionConfig(executor.OpcodeVersionV2, config.MakeGasMapForTests()) + require.Equal(t, exec.setOpcodeConfigErr, err) + require.Equal(t, executor.OpcodeVersionV1, host.opcodeVersion) + require.Nil(t, host.gasSchedule) + require.Nil(t, metering.schedule) + require.Equal(t, 0, runtime.warmCacheWipe) + require.Equal(t, 0, blockchain.codesCleared) + }) +} + +func TestVMHost_ReconfigureExecutorOpcodeVersion(t *testing.T) { + v2 := false + host, exec, runtime, _, blockchain := newOpcodeConfigTestHost(&v2) + host.gasSchedule = config.MakeGasMapForTests() + + // unchanged version: nothing happens + host.reconfigureExecutorOpcodeVersion() + require.Equal(t, 0, exec.calls) + + // flag flips: executor reconfigured and caches wiped + v2 = true + host.reconfigureExecutorOpcodeVersion() + require.Equal(t, 1, exec.calls) + require.Equal(t, executor.OpcodeVersionV2, host.opcodeVersion) + require.Equal(t, 1, runtime.warmCacheWipe) + require.Equal(t, 1, blockchain.codesCleared) + + // executor failure is only logged, the old version is kept + v2 = false + exec.setOpcodeConfigErr = errors.New("boom") + host.reconfigureExecutorOpcodeVersion() + require.Equal(t, executor.OpcodeVersionV2, host.opcodeVersion) +} + +func TestVMHost_GasScheduleChange(t *testing.T) { + v2 := true + host, exec, _, metering, _ := newOpcodeConfigTestHost(&v2) + schedule := config.MakeGasMapForTests() + + host.GasScheduleChange(schedule) + require.Equal(t, executor.OpcodeVersionV2, exec.version) + require.Equal(t, schedule, metering.schedule) + + // error path is only logged + exec.setOpcodeConfigErr = errors.New("boom") + host.GasScheduleChange(schedule) + require.Equal(t, 2, exec.calls) +} + +func TestVMHost_EpochConfirmedReconfiguresExecutor(t *testing.T) { + v2 := false + host, exec, _, _, _ := newOpcodeConfigTestHost(&v2) + host.gasSchedule = config.MakeGasMapForTests() + + v2 = true + host.EpochConfirmed(5, 0) + require.Equal(t, executor.OpcodeVersionV2, exec.version) +} diff --git a/vmhost/hosttest/bulkMemoryGas_test.go b/vmhost/hosttest/bulkMemoryGas_test.go new file mode 100644 index 000000000..eea38befd --- /dev/null +++ b/vmhost/hosttest/bulkMemoryGas_test.go @@ -0,0 +1,449 @@ +package hostCoretest + +import ( + "math/big" + "testing" + + "github.com/multiversx/mx-chain-core-go/core" + "github.com/multiversx/mx-chain-scenario-go/worldmock" + vmcommon "github.com/multiversx/mx-chain-vm-common-go" + "github.com/multiversx/mx-chain-vm-go/config" + executorwrapper "github.com/multiversx/mx-chain-vm-go/executor/wrapper" + contextmock "github.com/multiversx/mx-chain-vm-go/mock/context" + gasschedules "github.com/multiversx/mx-chain-vm-go/scenario/gasSchedules" + "github.com/multiversx/mx-chain-vm-go/testcommon" + "github.com/multiversx/mx-chain-vm-go/vmhost" + "github.com/multiversx/mx-chain-vm-go/wasmer2" + "github.com/stretchr/testify/require" +) + +// Deliberately not round numbers and not multiples of one another, so that a wrong +// per-byte cost cannot accidentally produce the expected gas delta. +const ( + memoryCopyCost = uint64(300) + memoryCopyPerByteCost = uint64(7) + memoryFillCost = uint64(400) + memoryFillPerByteCost = uint64(11) +) + +const bulkMemoryGasProvided = uint64(100_000_000) + +// bulkMemoryGasSchedule takes the default test gas schedule and gives the bulk memory +// opcodes distinctive costs, so that their contribution can be isolated. +func bulkMemoryGasSchedule() config.GasScheduleMap { + gasSchedule := config.MakeGasMapForTests() + wasmOpcodeCost := gasSchedule["WASMOpcodeCost"] + wasmOpcodeCost["MemoryCopy"] = memoryCopyCost + wasmOpcodeCost["MemoryCopyPerByte"] = memoryCopyPerByteCost + wasmOpcodeCost["MemoryFill"] = memoryFillCost + wasmOpcodeCost["MemoryFillPerByte"] = memoryFillPerByteCost + return gasSchedule +} + +// bulkMemoryGasUsed calls the given endpoint of the bulk-memory contract with the number +// of bytes as argument 0 and returns the gas consumed by the call. +func bulkMemoryGasUsed(tb testing.TB, gasSchedule config.GasScheduleMap, function string, numBytes uint64) uint64 { + var gasUsed uint64 + + testcommon.BuildInstanceCallTest(tb). + WithContracts( + testcommon.CreateInstanceContract(testcommon.ParentAddress). + WithCode(testcommon.GetTestSCCode("bulk-memory", "../../"))). + WithGasSchedule(gasSchedule). + WithInput(testcommon.CreateTestContractCallInputBuilder(). + WithGasProvided(bulkMemoryGasProvided). + WithFunction(function). + WithArguments(big.NewInt(0).SetUint64(numBytes).Bytes()). + Build()). + AndAssertResults(func(_ vmhost.VMHost, _ *contextmock.BlockchainHookStub, verify *testcommon.VMOutputVerifier) { + verify.Ok() + require.LessOrEqual(tb, verify.VmOutput.GasRemaining, bulkMemoryGasProvided) + gasUsed = bulkMemoryGasProvided - verify.VmOutput.GasRemaining + }) + + return gasUsed +} + +// TestBulkMemoryGas_PerByteCostsSurviveGasConfigDecoding checks that the per-byte costs make +// it from the gas schedule map into the WASMOpcodeCost struct, which is what the host hands +// to the executor via SetOpcodeConfig. +func TestBulkMemoryGas_PerByteCostsSurviveGasConfigDecoding(t *testing.T) { + gasSchedule := bulkMemoryGasSchedule() + + wasmOpcodeCost := gasSchedule["WASMOpcodeCost"] + require.Contains(t, wasmOpcodeCost, "MemoryCopyPerByte") + require.Contains(t, wasmOpcodeCost, "MemoryFillPerByte") + + gasCostConfig, err := config.CreateGasConfig(gasSchedule) + require.Nil(t, err) + + require.Equal(t, uint32(memoryCopyCost), gasCostConfig.WASMOpcodeCost.MemoryCopy) + require.Equal(t, uint32(memoryCopyPerByteCost), gasCostConfig.WASMOpcodeCost.MemoryCopyPerByte) + require.Equal(t, uint32(memoryFillCost), gasCostConfig.WASMOpcodeCost.MemoryFill) + require.Equal(t, uint32(memoryFillPerByteCost), gasCostConfig.WASMOpcodeCost.MemoryFillPerByte) +} + +// TestBulkMemoryGas_ShippedSchedulesDefinePerByteCosts guards against a gas schedule that +// the node would accept while leaving the new per-byte costs unset. +func TestBulkMemoryGas_ShippedSchedulesDefinePerByteCosts(t *testing.T) { + shippedSchedules := map[string]string{ + "V3": gasschedules.GetV3(), + "V4": gasschedules.GetV4(), + } + + for name, contents := range shippedSchedules { + gasSchedule, err := gasschedules.LoadGasScheduleConfig(contents) + require.Nil(t, err, name) + + require.Contains(t, gasSchedule["WASMOpcodeCost"], "MemoryCopyPerByte", name) + require.Contains(t, gasSchedule["WASMOpcodeCost"], "MemoryFillPerByte", name) + + // CreateGasConfig rejects any WASMOpcodeCost field left at zero + gasCostConfig, err := config.CreateGasConfig(gasSchedule) + require.Nil(t, err, name) + + require.NotZero(t, gasCostConfig.WASMOpcodeCost.MemoryCopyPerByte, name) + require.NotZero(t, gasCostConfig.WASMOpcodeCost.MemoryFillPerByte, name) + } +} + +// TestBulkMemoryGas_PerByteCostOfMemoryCopy checks that the gas difference between copying +// more and fewer bytes is exactly the number of extra bytes times MemoryCopyPerByte. +func TestBulkMemoryGas_PerByteCostOfMemoryCopy(t *testing.T) { + gasSchedule := bulkMemoryGasSchedule() + + gasForZero := bulkMemoryGasUsed(t, gasSchedule, "memoryCopy", 0) + gasForTen := bulkMemoryGasUsed(t, gasSchedule, "memoryCopy", 10) + gasForThousand := bulkMemoryGasUsed(t, gasSchedule, "memoryCopy", 1000) + + require.Equal(t, 10*memoryCopyPerByteCost, gasForTen-gasForZero) + require.Equal(t, 990*memoryCopyPerByteCost, gasForThousand-gasForTen) + require.Equal(t, 1000*memoryCopyPerByteCost, gasForThousand-gasForZero) +} + +// TestBulkMemoryGas_PerByteCostOfMemoryFill is TestBulkMemoryGas_PerByteCostOfMemoryCopy +// for memory.fill, which has its own, different per-byte cost. +func TestBulkMemoryGas_PerByteCostOfMemoryFill(t *testing.T) { + gasSchedule := bulkMemoryGasSchedule() + + gasForZero := bulkMemoryGasUsed(t, gasSchedule, "memoryFill", 0) + gasForTen := bulkMemoryGasUsed(t, gasSchedule, "memoryFill", 10) + gasForThousand := bulkMemoryGasUsed(t, gasSchedule, "memoryFill", 1000) + + require.Equal(t, 10*memoryFillPerByteCost, gasForTen-gasForZero) + require.Equal(t, 990*memoryFillPerByteCost, gasForThousand-gasForTen) + require.Equal(t, 1000*memoryFillPerByteCost, gasForThousand-gasForZero) +} + +// TestBulkMemoryGas_PerByteCostFollowsTheSchedule doubles the per-byte costs and checks that +// the gas delta doubles with them, which the base costs alone could not explain. +func TestBulkMemoryGas_PerByteCostFollowsTheSchedule(t *testing.T) { + doubledSchedule := bulkMemoryGasSchedule() + wasmOpcodeCost := doubledSchedule["WASMOpcodeCost"] + wasmOpcodeCost["MemoryCopyPerByte"] = 2 * memoryCopyPerByteCost + wasmOpcodeCost["MemoryFillPerByte"] = 2 * memoryFillPerByteCost + + copyDelta := bulkMemoryGasUsed(t, doubledSchedule, "memoryCopy", 1000) - + bulkMemoryGasUsed(t, doubledSchedule, "memoryCopy", 0) + require.Equal(t, 1000*2*memoryCopyPerByteCost, copyDelta) + + fillDelta := bulkMemoryGasUsed(t, doubledSchedule, "memoryFill", 1000) - + bulkMemoryGasUsed(t, doubledSchedule, "memoryFill", 0) + require.Equal(t, 1000*2*memoryFillPerByteCost, fillDelta) +} + +// TestBulkMemoryGas_BaseCostIsPaidForZeroBytes isolates the base cost of each opcode, by +// comparing against an endpoint that does everything except the bulk memory opcode itself. +func TestBulkMemoryGas_BaseCostIsPaidForZeroBytes(t *testing.T) { + gasSchedule := bulkMemoryGasSchedule() + + gasWithoutBulkMemory := bulkMemoryGasUsed(t, gasSchedule, "noBulkMemory", 0) + + // the *ViaLocal endpoints only add the opcode and the three operands it pops, which + // the default test schedule prices at config.GasValueForTests each + const operandsCost = 3 * uint64(config.GasValueForTests) + + copyGas := bulkMemoryGasUsed(t, gasSchedule, "memoryCopyViaLocal", 0) + require.Equal(t, memoryCopyCost+operandsCost, copyGas-gasWithoutBulkMemory) + + fillGas := bulkMemoryGasUsed(t, gasSchedule, "memoryFillViaLocal", 0) + require.Equal(t, memoryFillCost+operandsCost, fillGas-gasWithoutBulkMemory) +} + +// TestBulkMemoryGas_SizeOperandSurvivesTheInjection checks that the metering code injected +// before the opcode gives the size operand back unchanged, whether it comes straight off the +// stack or through a local. +func TestBulkMemoryGas_SizeOperandSurvivesTheInjection(t *testing.T) { + gasSchedule := bulkMemoryGasSchedule() + + // the *ViaLocal endpoints only add a local.set/local.get pair on top of their counterparts + const localCost = 2 * uint64(config.GasValueForTests) + + for _, numBytes := range []uint64{0, 10, 1000} { + copyDirect := bulkMemoryGasUsed(t, gasSchedule, "memoryCopy", numBytes) + copyViaLocal := bulkMemoryGasUsed(t, gasSchedule, "memoryCopyViaLocal", numBytes) + require.Equal(t, copyDirect+localCost, copyViaLocal, "memory.copy, numBytes = %d", numBytes) + + fillDirect := bulkMemoryGasUsed(t, gasSchedule, "memoryFill", numBytes) + fillViaLocal := bulkMemoryGasUsed(t, gasSchedule, "memoryFillViaLocal", numBytes) + require.Equal(t, fillDirect+localCost, fillViaLocal, "memory.fill, numBytes = %d", numBytes) + } +} + +// TestBulkMemoryGas_StillForbiddenOnOpcodeVersionV1 makes sure that metering the bulk memory +// opcodes did not accidentally allow them on the old opcode version, which is the one the +// host picks while the OpcodeV2 flag is off. +func TestBulkMemoryGas_StillForbiddenOnOpcodeVersionV1(t *testing.T) { + testcommon.BuildInstanceCallTest(t). + WithContracts( + testcommon.CreateInstanceContract(testcommon.ParentAddress). + WithCode(testcommon.GetTestSCCode("bulk-memory", "../../"))). + WithGasSchedule(bulkMemoryGasSchedule()). + WithEnableEpochsHandler(&worldmock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + return flag != vmhost.OpcodeV2Flag + }, + }). + WithInput(testcommon.CreateTestContractCallInputBuilder(). + WithGasProvided(bulkMemoryGasProvided). + WithFunction("memoryCopy"). + WithArguments(big.NewInt(10).Bytes()). + Build()). + AndAssertResults(func(_ vmhost.VMHost, _ *contextmock.BlockchainHookStub, verify *testcommon.VMOutputVerifier) { + verify.ContractInvalid() + }) +} + +// TestBulkMemoryGas_ExecutorReconfiguredOnOpcodeV2Activation makes sure that a host which +// outlives the activation of OpcodeV2Flag switches its executor to the new opcode version. +// The executor only samples the opcode version when the host creates it, so without an +// explicit reconfiguration at the epoch boundary it would keep rejecting the bulk memory +// opcodes for as long as the host lives. +func TestBulkMemoryGas_ExecutorReconfiguredOnOpcodeV2Activation(t *testing.T) { + opcodeV2Enabled := false + + host := testcommon.NewTestHostBuilder(t). + WithBlockchainHook(testcommon.BlockchainHookStubForCall( + testcommon.GetTestSCCode("bulk-memory", "../../"), + big.NewInt(0))). + WithGasSchedule(bulkMemoryGasSchedule()). + WithEnableEpochsHandler(&worldmock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + if flag == vmhost.OpcodeV2Flag { + return opcodeV2Enabled + } + return true + }, + }). + Build() + defer host.Reset() + + // the host is what the epoch notifier of the node notifies + epochSubscriber, ok := host.(vmcommon.EpochSubscriberHandler) + require.True(t, ok) + + input := testcommon.CreateTestContractCallInputBuilder(). + WithGasProvided(bulkMemoryGasProvided). + WithFunction("memoryCopy"). + WithArguments(big.NewInt(10).Bytes()). + Build() + + // before activation, memory.copy is not part of the opcode whitelist + vmOutput, err := host.RunSmartContractCall(input) + testcommon.NewVMOutputVerifier(t, vmOutput, err).ContractInvalid() + + // the flag activates while the host is alive + opcodeV2Enabled = true + epochSubscriber.EpochConfirmed(1, 0) + + vmOutput, err = host.RunSmartContractCall(input) + testcommon.NewVMOutputVerifier(t, vmOutput, err).Ok() +} + +// TestBulkMemoryGas_OpcodeV2ActivationClearsCachedCompiledCode makes sure that an opcode +// version change invalidates the caches exactly like a gas schedule change does. The opcode +// whitelist is applied at compile time, so artifacts compiled under the previous version +// must not survive the activation. +func TestBulkMemoryGas_OpcodeV2ActivationClearsCachedCompiledCode(t *testing.T) { + opcodeV2Enabled := false + + world := worldmock.NewMockWorld() + host := testcommon.NewTestHostBuilder(t). + WithBlockchainHook(world). + WithBuiltinFunctions(). + WithEnableEpochsHandler(&worldmock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + if flag == vmhost.OpcodeV2Flag { + return opcodeV2Enabled + } + return true + }, + }). + Build() + defer host.Reset() + + scAddress := testcommon.MakeTestSCAddress("counter") + world.AcctMap.CreateSmartContractAccount( + testcommon.ParentAddress, + scAddress, + testcommon.GetTestSCCode("counter", "../../"), + world) + + input := testcommon.CreateTestContractCallInputBuilder(). + WithRecipientAddr(scAddress). + WithGasProvided(100000). + WithFunction("increment"). + Build() + + vmOutput, err := host.RunSmartContractCall(input) + testcommon.NewVMOutputVerifier(t, vmOutput, err).Ok() + + // the contract was compiled under opcode version V1 and cached by the blockchain hook + require.NotEmpty(t, world.CompiledCode) + + epochSubscriber, ok := host.(vmcommon.EpochSubscriberHandler) + require.True(t, ok) + + opcodeV2Enabled = true + epochSubscriber.EpochConfirmed(1, 0) + + require.Empty(t, world.CompiledCode) +} + +// TestBulkMemoryGas_WrappedExecutorFactoryForwardsOpcodeVersion makes sure that the executor +// wrapper does not drop the opcode version chosen by the host, which would silently create a +// V1 executor and reject the bulk memory opcodes even though OpcodeV2Flag is active. +func TestBulkMemoryGas_WrappedExecutorFactoryForwardsOpcodeVersion(t *testing.T) { + host := testcommon.NewTestHostBuilder(t). + WithExecutorFactory(executorwrapper.SimpleWrappedExecutorFactory(wasmer2.ExecutorFactory())). + WithBlockchainHook(testcommon.BlockchainHookStubForCall( + testcommon.GetTestSCCode("bulk-memory", "../../"), + big.NewInt(0))). + WithGasSchedule(bulkMemoryGasSchedule()). + WithEnableEpochsHandler(&worldmock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(_ core.EnableEpochFlag) bool { + return true + }, + }). + Build() + defer host.Reset() + + input := testcommon.CreateTestContractCallInputBuilder(). + WithGasProvided(bulkMemoryGasProvided). + WithFunction("memoryCopy"). + WithArguments(big.NewInt(10).Bytes()). + Build() + + vmOutput, err := host.RunSmartContractCall(input) + testcommon.NewVMOutputVerifier(t, vmOutput, err).Ok() +} + +// bulkMemoryResult runs one of the *Check endpoints, which prepare a marked destination region +// and return the first bulkMemoryCheckedBytes bytes of memory after the bulk memory opcode. +func bulkMemoryResult(t *testing.T, function string, numBytes uint64) []byte { + var result []byte + + testcommon.BuildInstanceCallTest(t). + WithContracts( + testcommon.CreateInstanceContract(testcommon.ParentAddress). + WithCode(testcommon.GetTestSCCode("bulk-memory", "../../"))). + WithGasSchedule(bulkMemoryGasSchedule()). + WithInput(testcommon.CreateTestContractCallInputBuilder(). + WithGasProvided(bulkMemoryGasProvided). + WithFunction(function). + WithArguments(big.NewInt(0).SetUint64(numBytes).Bytes()). + Build()). + AndAssertResults(func(_ vmhost.VMHost, _ *contextmock.BlockchainHookStub, verify *testcommon.VMOutputVerifier) { + verify.Ok() + require.Len(t, verify.VmOutput.ReturnData, 1) + result = verify.VmOutput.ReturnData[0] + }) + + return result +} + +const bulkMemoryCheckedBytes = 1100 + +// TestBulkMemoryGas_MemoryCopyReceivesTheOriginalSize checks the memory after memory.copy: +// exactly numBytes bytes of the non-zero source pattern must land at the destination, and +// the bytes after them must keep their previous value. +func TestBulkMemoryGas_MemoryCopyReceivesTheOriginalSize(t *testing.T) { + for _, numBytes := range []uint64{0, 10, 1000} { + result := bulkMemoryResult(t, "memoryCopyCheck", numBytes) + require.Len(t, result, bulkMemoryCheckedBytes) + + for i := 0; i < bulkMemoryCheckedBytes; i++ { + expected := byte(0xAA) + if uint64(i) < numBytes { + expected = byte(i%251 + 1) + } + require.Equal(t, expected, result[i], "memory.copy, numBytes = %d, offset = %d", numBytes, i) + } + } +} + +// TestBulkMemoryGas_MemoryFillReceivesTheOriginalSize is the memory.fill counterpart. +func TestBulkMemoryGas_MemoryFillReceivesTheOriginalSize(t *testing.T) { + for _, numBytes := range []uint64{0, 10, 1000} { + result := bulkMemoryResult(t, "memoryFillCheck", numBytes) + require.Len(t, result, bulkMemoryCheckedBytes) + + for i := 0; i < bulkMemoryCheckedBytes; i++ { + expected := byte(0xAA) + if uint64(i) < numBytes { + expected = 7 + } + require.Equal(t, expected, result[i], "memory.fill, numBytes = %d, offset = %d", numBytes, i) + } + } +} + +// TestBulkMemoryGas_RecreatedHostDoesNotTrustRetainedCompiledCode makes sure that a host +// created with OpcodeV2 over a compiled-code cache retained from a V1 host does not run +// artifacts compiled under V1. The cache is keyed by code hash only, and deserializing an +// artifact skips the opcode whitelist, so a hit would accept what a miss rejects. +func TestBulkMemoryGas_RecreatedHostDoesNotTrustRetainedCompiledCode(t *testing.T) { + world := worldmock.NewMockWorld() + scAddress := testcommon.MakeTestSCAddress("ref-types") + world.AcctMap.CreateSmartContractAccount( + testcommon.ParentAddress, + scAddress, + testcommon.GetTestSCCode("ref-types", "../../"), + world) + + buildHost := func(opcodeV2Enabled bool) vmhost.VMHost { + return testcommon.NewTestHostBuilder(t). + WithBlockchainHook(world). + WithBuiltinFunctions(). + WithEnableEpochsHandler(&worldmock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + if flag == vmhost.OpcodeV2Flag { + return opcodeV2Enabled + } + return true + }, + }). + Build() + } + + input := testcommon.CreateTestContractCallInputBuilder(). + WithRecipientAddr(scAddress). + WithGasProvided(100000). + WithFunction("isNull"). + Build() + + hostV1 := buildHost(false) + vmOutput, err := hostV1.RunSmartContractCall(input) + testcommon.NewVMOutputVerifier(t, vmOutput, err).Ok() + hostV1.Reset() + require.NotEmpty(t, world.CompiledCode) + + // the world, and with it the compiled code cache, outlives the first host + hostV2 := buildHost(true) + defer hostV2.Reset() + + require.Empty(t, world.CompiledCode) + + vmOutput, err = hostV2.RunSmartContractCall(input) + testcommon.NewVMOutputVerifier(t, vmOutput, err).ContractInvalid() +} diff --git a/vmhost/hosttest/execution_test.go b/vmhost/hosttest/execution_test.go index 9e65ce7c3..9887fa861 100644 --- a/vmhost/hosttest/execution_test.go +++ b/vmhost/hosttest/execution_test.go @@ -625,6 +625,54 @@ func TestExecution_ChangeWasmerOpcodeCosts(t *testing.T) { require.NotEqual(t, gasRemainingBeforeChange, gasRemainingAfterChange) } +// TestExecution_ChangeWasmerOpcodeCosts_ClearsCachedCompiledCode makes sure that a gas +// schedule change drops the precompiled code cache as well as the warm instance cache. +// The opcode costs are injected into the code at compile time, so an instance built from a +// module that was compiled under the previous schedule would keep metering with the +// previous costs, silently ignoring the change. +func TestExecution_ChangeWasmerOpcodeCosts_ClearsCachedCompiledCode(t *testing.T) { + world := worldmock.NewMockWorld() + host := test.NewTestHostBuilder(t). + WithBlockchainHook(world). + WithBuiltinFunctions(). + Build() + defer func() { + host.Reset() + }() + + scAddress := test.MakeTestSCAddress("misc") + world.AcctMap.CreateSmartContractAccount( + test.ParentAddress, + scAddress, + test.GetTestSCCode("misc", "../../"), + world) + + input := test.CreateTestContractCallInputBuilder(). + WithRecipientAddr(scAddress). + WithGasProvided(10000). + WithFunction("iterate_over_byte_array"). + Build() + + vmOutput, err := host.RunSmartContractCall(input) + test.NewVMOutputVerifier(t, vmOutput, err).Ok() + gasRemainingBeforeChange := vmOutput.GasRemaining + + // the first call compiled the contract and the blockchain hook cached the module + require.NotEmpty(t, world.CompiledCode) + + gasSchedule := host.GetGasScheduleMap() + gasSchedule["WASMOpcodeCost"]["BrIf"] += 20 + host.GasScheduleChange(gasSchedule) + + require.Empty(t, world.CompiledCode) + + // the contract is recompiled with the new costs, instead of being restored from a + // module metered with the old ones + vmOutput, err = host.RunSmartContractCall(input) + test.NewVMOutputVerifier(t, vmOutput, err).Ok() + require.NotEqual(t, gasRemainingBeforeChange, vmOutput.GasRemaining) +} + func TestExecution_ChangeWasmerAPICosts(t *testing.T) { contractCode := test.GetTestSCCode("misc", "../../") diff --git a/vmhost/hosttest/forbidden_opcodes_test.go b/vmhost/hosttest/forbidden_opcodes_test.go index 2960d1452..7ed58663a 100644 --- a/vmhost/hosttest/forbidden_opcodes_test.go +++ b/vmhost/hosttest/forbidden_opcodes_test.go @@ -12,7 +12,7 @@ import ( func TestForbiddenOps_BulkAndSIMD(t *testing.T) { wasmModules := []string{ - "data-drop", "memory-init", "memory-fill", "memory-copy", "simd", + "data-drop", "memory-init", "simd", "table-get", "table-grow", "table-init", "table-set", "table-size", } @@ -64,3 +64,38 @@ func TestBarnardOpcodesActivation(t *testing.T) { ContractInvalid() }) } + +func TestBulkMemoryOpcodesActivation(t *testing.T) { + wasmModules := []string{"memory-copy", "memory-fill"} + + for _, moduleName := range wasmModules { + testcommon.BuildInstanceCreatorTest(t). + WithInput(testcommon.CreateTestContractCreateInputBuilder(). + WithGasProvided(100000000). + WithContractCode(testcommon.GetTestSCCodeModule("forbidden-opcodes/"+moduleName, moduleName, "../../")). + Build()). + WithEnableEpochsHandler(&worldmock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + return flag != vmhost.OpcodeV2Flag + }, + }). + AndAssertResults(func(stubBlockchainHook *contextmock.BlockchainHookStub, verify *testcommon.VMOutputVerifier) { + verify. + ContractInvalid() + }) + + testcommon.BuildInstanceCreatorTest(t). + WithInput(testcommon.CreateTestContractCreateInputBuilder(). + WithGasProvided(100000000). + WithContractCode(testcommon.GetTestSCCodeModule("forbidden-opcodes/"+moduleName, moduleName, "../../")). + Build()). + WithEnableEpochsHandler(&worldmock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + return true + }, + }). + AndAssertResults(func(stubBlockchainHook *contextmock.BlockchainHookStub, verify *testcommon.VMOutputVerifier) { + verify.FunctionNotFound() + }) + } +} diff --git a/vmhost/vmhooks/generate/cmd/.gitignore b/vmhost/vmhooks/generate/cmd/.gitignore index 6f9c86958..b0b305dec 100644 --- a/vmhost/vmhooks/generate/cmd/.gitignore +++ b/vmhost/vmhooks/generate/cmd/.gitignore @@ -1,5 +1,9 @@ # generated files that belong to other repos: output -# optional path where to copy rust files +# optional path where to copy rust files (deprecated) wasm-vm-executor-rs-path.txt + +# optional path where to copy rust files +path-to-mx-sdk-rs.txt +path-to-vm-executor-rs.txt diff --git a/vmhost/vmhooks/generate/cmd/README.md b/vmhost/vmhooks/generate/cmd/README.md index 00a5711a8..d9eb2991d 100644 --- a/vmhost/vmhooks/generate/cmd/README.md +++ b/vmhost/vmhooks/generate/cmd/README.md @@ -5,3 +5,25 @@ The code generator generates boilerplate code for both this Go VM, and for [the For it to automatically copy files there, create a file called `wasm-vm-executor-rs-path.txt` here, in the `cmd` folder, contianing your local path to that repository, on your disk. Finally, simply run `go generate` in `vmhost/vmhooks`. + +## Inputs + +Everything under `input/` is a plain list of names, one per line. These lists are the only +place where opcodes and costs are still declared by hand. + +### `wasm_all_opcodes.txt` + +The full list of Wasm opcode names, used to generate the opcode cost struct and its helpers. +It is a superset of every other list in this directory. + +## The derived cost list + +`OpcodeNames.AllCostNames()` concatenates the allowed opcodes of v1, then those of v2, then +the `*PerByte` costs, then the additional costs, skipping the names it has already seen. + +That list is the field layout of the `OpcodeCost` struct on both sides of the FFI boundary, +`wasmer2/opcodeCost.go` here and `vm-executor/src/opcode_cost.rs` in the executor, and the +Go struct is cast straight to the C one. Reordering an input file, or inserting a name +anywhere but at the end of one, therefore changes that layout silently. Whenever it does +change, the executor has to be rebuilt and `wasmer2/libvmexeccapi.*` refreshed, or the VM +will read every cost past the first moved field from the wrong place. diff --git a/vmhost/vmhooks/generate/cmd/eiGenMain.go b/vmhost/vmhooks/generate/cmd/eiGenMain.go index 7a8cbb682..b56ef0b98 100644 --- a/vmhost/vmhooks/generate/cmd/eiGenMain.go +++ b/vmhost/vmhooks/generate/cmd/eiGenMain.go @@ -14,7 +14,8 @@ import ( ) const pathToApiPackage = "./" -const pathToRustRepoConfigFile = "wasm-vm-executor-rs-path.txt" +const pathVmExecutorRsFile = "path-to-vm-executor-rs.txt" +const pathToMxSdkRsFile = "path-to-mx-sdk-rs.txt" func initEIMetadata() *eapigen.EIMetadata { return &eapigen.EIMetadata{ @@ -55,6 +56,7 @@ func main() { writeRustVMHooksTrait(eiMetadata) writeRustVMHooksLegacyTrait(eiMetadata) writeRustVMHooksLegacyAdapter(eiMetadata) + writeRustVMHooksSignatures(eiMetadata) writeRustCapiVMHooks(eiMetadata) writeRustCapiVMHooksPointers(eiMetadata) writeRustWasmerProdImports(eiMetadata) @@ -67,13 +69,16 @@ func main() { writeWasmer2OpcodeCost() writeWASMOpcodeCostFuncHelpers() writeWASMOpcodeCostConfigHelpers() - writeOpcodeCostFuncHelpers() writeRustOpcodeCost() - writeRustWasmerMeteringHelpers() + writeRustCapiOpcodeCost() + writeRustWasmerOpcodeCost() + writeOpcodeWhitelisted() + writeRustWasmerExperimentalOpcodeCost() fmt.Println("Generated code for opcodes and metering helpers.") - tryCopyFilesToRustExecutorRepo() + tryCopyFilesToMxExecutorRsRepo() + tryCopyFilesToMxSdkRsRepo() } func writeVMHooks(eiMetadata *eapigen.EIMetadata) { @@ -173,6 +178,12 @@ func writeRustVHDispatcherLegacy(eiMetadata *eapigen.EIMetadata) { eapigen.WriteRustVHDispatcherLegacy(out, eiMetadata) } +func writeRustVMHooksSignatures(eiMetadata *eapigen.EIMetadata) { + out := eapigen.NewEIGenWriter(pathToApiPackage, "generate/cmd/output/vm_hook_signature_list.rs") + defer out.Close() + eapigen.WriteRustVMHooksSignatures(out, eiMetadata) +} + func writeExecutorOpcodeCosts() { out := eapigen.NewEIGenWriter(pathToApiPackage, "../../executor/gasCostWASM.go") defer out.Close() @@ -197,30 +208,42 @@ func writeWasmer2OpcodeCost() { eapigen.WriteWasmer2OpcodeCost(out) } -func writeOpcodeCostFuncHelpers() { - out := eapigen.NewEIGenWriter(pathToApiPackage, "generate/cmd/output/extractOpcodeCost.txt") - defer out.Close() - eapigen.WriteOpcodeCostFuncHelpers(out) -} - func writeRustOpcodeCost() { out := eapigen.NewEIGenWriter(pathToApiPackage, "generate/cmd/output/opcode_cost.rs") defer out.Close() eapigen.WriteRustOpcodeCost(out) } -func writeRustWasmerMeteringHelpers() { - out := eapigen.NewEIGenWriter(pathToApiPackage, "generate/cmd/output/wasmer_metering_helpers.rs") +func writeRustCapiOpcodeCost() { + out := eapigen.NewEIGenWriter(pathToApiPackage, "generate/cmd/output/capi_opcode_cost.rs") + defer out.Close() + eapigen.WriteRustCapiOpcodeCost(out) +} + +func writeRustWasmerOpcodeCost() { + out := eapigen.NewEIGenWriter(pathToApiPackage, "generate/cmd/output/wasmer_opcode_cost.rs") + defer out.Close() + eapigen.WriteRustWasmerOpcodeCost(out) +} + +func writeOpcodeWhitelisted() { + out := eapigen.NewEIGenWriter(pathToApiPackage, "generate/cmd/output/opcode_whitelist.rs") defer out.Close() - eapigen.WriteRustWasmerMeteringHelpers(out) + eapigen.WriteOpcodeWhitelisted(out) } -func tryCopyFilesToRustExecutorRepo() { - fullPathToRustRepoConfigFile := filepath.Join(pathToApiPackage, "generate/cmd/", pathToRustRepoConfigFile) - contentBytes, err := os.ReadFile(fullPathToRustRepoConfigFile) +func writeRustWasmerExperimentalOpcodeCost() { + out := eapigen.NewEIGenWriter(pathToApiPackage, "generate/cmd/output/we_opcode_cost.rs") + defer out.Close() + eapigen.WriteRustWasmerExperimentalOpcodeCost(out) +} + +func tryCopyFilesToMxExecutorRsRepo() { + fullPathToConfigFile := filepath.Join(pathToApiPackage, "generate/cmd/", pathVmExecutorRsFile) + contentBytes, err := os.ReadFile(fullPathToConfigFile) if err != nil { // this feature is optional - fmt.Println("Rust files not copied to wasm-vm-executor-rs. Add a wasm-vm-executor-rs-path.txt with the path to enable feature.") + fmt.Printf("Rust files not copied to wasm-vm-executor-rs. Add a %s with the path to enable feature.\n", pathVmExecutorRsFile) return } rustExecutorPath := strings.Trim(string(contentBytes), " \n\t") @@ -242,6 +265,10 @@ func tryCopyFilesToRustExecutorRepo() { filepath.Join(pathToApiPackage, "generate/cmd/output/opcode_cost.rs"), filepath.Join(rustExecutorPath, "vm-executor/src/opcode_cost.rs"), ) + copyFile( + filepath.Join(pathToApiPackage, "generate/cmd/output/capi_opcode_cost.rs"), + filepath.Join(rustExecutorPath, "c-api/src/capi_opcode_cost.rs"), + ) copyFile( filepath.Join(pathToApiPackage, "generate/cmd/output/capi_vm_hook.rs"), filepath.Join(rustExecutorPath, "c-api/src/capi_vm_hooks.rs"), @@ -259,8 +286,33 @@ func tryCopyFilesToRustExecutorRepo() { filepath.Join(rustExecutorPath, "vm-executor-experimental/src/we_imports.rs"), ) copyFile( - filepath.Join(pathToApiPackage, "generate/cmd/output/wasmer_metering_helpers.rs"), - filepath.Join(rustExecutorPath, "vm-executor-wasmer/src/wasmer_metering_helpers.rs"), + filepath.Join(pathToApiPackage, "generate/cmd/output/wasmer_opcode_cost.rs"), + filepath.Join(rustExecutorPath, "vm-executor-wasmer/src/wasmer_opcode_cost.rs"), + ) + copyFile( + filepath.Join(pathToApiPackage, "generate/cmd/output/we_opcode_cost.rs"), + filepath.Join(rustExecutorPath, "vm-executor-experimental/src/middlewares/we_opcode_cost.rs"), + ) +} + +func tryCopyFilesToMxSdkRsRepo() { + fullPathToConfigFile := filepath.Join(pathToApiPackage, "generate/cmd/", pathToMxSdkRsFile) + contentBytes, err := os.ReadFile(fullPathToConfigFile) + if err != nil { + // this feature is optional + fmt.Printf("Rust files not copied to mx-sdk-rs. Add a %s with the path to enable feature.\n", pathToMxSdkRsFile) + return + } + rustExecutorPath := strings.Trim(string(contentBytes), " \n\t") + + fmt.Printf("Copying generated Rust files to '%s':\n", rustExecutorPath) + copyFile( + filepath.Join(pathToApiPackage, "generate/cmd/output/opcode_whitelist.rs"), + filepath.Join(rustExecutorPath, "framework/meta-lib/src/tools/wasm_extractor/opcode_whitelist.rs"), + ) + copyFile( + filepath.Join(pathToApiPackage, "generate/cmd/output/vm_hook_signature_list.rs"), + filepath.Join(rustExecutorPath, "framework/meta-lib/src/ei/vm_hook_signature_list.rs"), ) } @@ -272,14 +324,14 @@ func copyFile(from, to string) { if err != nil { log.Fatal(err) } - defer original.Close() + defer func() { _ = original.Close() }() // Create new file new, err := os.Create(to) if err != nil { log.Fatal(err) } - defer new.Close() + defer func() { _ = new.Close() }() //This will copy _, err = io.Copy(new, original) diff --git a/vmhost/vmhooks/generate/cmd/input/wasm_additional_costs.txt b/vmhost/vmhooks/generate/cmd/input/wasm_additional_costs.txt new file mode 100644 index 000000000..b085503f5 --- /dev/null +++ b/vmhost/vmhooks/generate/cmd/input/wasm_additional_costs.txt @@ -0,0 +1 @@ +LocalAllocate \ No newline at end of file diff --git a/vmhost/vmhooks/generate/cmd/input/wasm_opcodes.txt b/vmhost/vmhooks/generate/cmd/input/wasm_all_opcodes.txt similarity index 99% rename from vmhost/vmhooks/generate/cmd/input/wasm_opcodes.txt rename to vmhost/vmhooks/generate/cmd/input/wasm_all_opcodes.txt index 06cd09ab1..d72aa3f93 100644 --- a/vmhost/vmhooks/generate/cmd/input/wasm_opcodes.txt +++ b/vmhost/vmhooks/generate/cmd/input/wasm_all_opcodes.txt @@ -494,7 +494,9 @@ MemoryAtomicNotify MemoryAtomicWait32 MemoryAtomicWait64 MemoryCopy +MemoryCopyPerByte MemoryFill +MemoryFillPerByte MemoryGrow MemoryInit MemorySize diff --git a/vmhost/vmhooks/generate/cmd/input/wasmer2_opcodes_short.txt b/vmhost/vmhooks/generate/cmd/input/wasm_allowed_opcodes_v1.txt similarity index 98% rename from vmhost/vmhooks/generate/cmd/input/wasmer2_opcodes_short.txt rename to vmhost/vmhooks/generate/cmd/input/wasm_allowed_opcodes_v1.txt index 023ccb982..4a5a0b79d 100644 --- a/vmhost/vmhooks/generate/cmd/input/wasmer2_opcodes_short.txt +++ b/vmhost/vmhooks/generate/cmd/input/wasm_allowed_opcodes_v1.txt @@ -103,7 +103,6 @@ If LocalGet LocalSet LocalTee -LocalAllocate Loop MemoryGrow MemorySize diff --git a/vmhost/vmhooks/generate/cmd/input/wasm_allowed_opcodes_v2.txt b/vmhost/vmhooks/generate/cmd/input/wasm_allowed_opcodes_v2.txt new file mode 100644 index 000000000..c1db6a458 --- /dev/null +++ b/vmhost/vmhooks/generate/cmd/input/wasm_allowed_opcodes_v2.txt @@ -0,0 +1,113 @@ +Block +Br +BrIf +BrTable +Call +CallIndirect +Drop +Else +End +GlobalGet +GlobalSet +I32Add +I32And +I32Clz +I32Const +I32Ctz +I32DivS +I32DivU +I32Eq +I32Eqz +I32Extend16S +I32Extend8S +I32GeS +I32GeU +I32GtS +I32GtU +I32LeS +I32LeU +I32Load +I32Load16S +I32Load16U +I32Load8S +I32Load8U +I32LtS +I32LtU +I32Mul +I32Ne +I32Or +I32Popcnt +I32RemS +I32RemU +I32Rotl +I32Rotr +I32Shl +I32ShrS +I32ShrU +I32Store +I32Store16 +I32Store8 +I32Sub +I32WrapI64 +I32Xor +I64Add +I64And +I64Clz +I64Const +I64Ctz +I64DivS +I64DivU +I64Eq +I64Eqz +I64Extend16S +I64Extend32S +I64Extend8S +I64ExtendI32S +I64ExtendI32U +I64GeS +I64GeU +I64GtS +I64GtU +I64LeS +I64LeU +I64Load +I64Load16S +I64Load16U +I64Load32S +I64Load32U +I64Load8S +I64Load8U +I64LtS +I64LtU +I64Mul +I64Ne +I64Or +I64Popcnt +I64RemS +I64RemU +I64Rotl +I64Rotr +I64Shl +I64ShrS +I64ShrU +I64Store +I64Store16 +I64Store32 +I64Store8 +I64Sub +I64Xor +If +LocalGet +LocalSet +LocalTee +Loop +MemoryGrow +MemorySize +Nop +Return +ReturnCall +ReturnCallIndirect +Select +Unreachable +MemoryCopy +MemoryFill \ No newline at end of file diff --git a/vmhost/vmhooks/generate/cmd/input/wasm_per_byte_opcodes.txt b/vmhost/vmhooks/generate/cmd/input/wasm_per_byte_opcodes.txt new file mode 100644 index 000000000..b20e58cef --- /dev/null +++ b/vmhost/vmhooks/generate/cmd/input/wasm_per_byte_opcodes.txt @@ -0,0 +1,2 @@ +MemoryCopy +MemoryFill \ No newline at end of file diff --git a/vmhost/vmhooks/generate/cmd/input/wasmer2_opcodes.txt b/vmhost/vmhooks/generate/cmd/input/wasmer2_opcodes.txt deleted file mode 100644 index d8da3939e..000000000 --- a/vmhost/vmhooks/generate/cmd/input/wasmer2_opcodes.txt +++ /dev/null @@ -1,514 +0,0 @@ -AtomicFence -Block -Br -BrIf -BrTable -Call -CallIndirect -Catch -CatchAll -DataDrop -Delegate -Drop -ElemDrop -Else -End -F32Abs -F32Add -F32Ceil -F32Const -F32ConvertI32S -F32ConvertI32U -F32ConvertI64S -F32ConvertI64U -F32Copysign -F32DemoteF64 -F32Div -F32Eq -F32Floor -F32Ge -F32Gt -F32Le -F32Load -F32Lt -F32Max -F32Min -F32Mul -F32Ne -F32Nearest -F32Neg -F32ReinterpretI32 -F32Sqrt -F32Store -F32Sub -F32Trunc -F32x4Abs -F32x4Add -F32x4Ceil -F32x4ConvertI32x4S -F32x4ConvertI32x4U -F32x4DemoteF64x2Zero -F32x4Div -F32x4Eq -F32x4ExtractLane -F32x4Floor -F32x4Ge -F32x4Gt -F32x4Le -F32x4Lt -F32x4Max -F32x4Min -F32x4Mul -F32x4Ne -F32x4Nearest -F32x4Neg -F32x4PMax -F32x4PMin -F32x4ReplaceLane -F32x4Splat -F32x4Sqrt -F32x4Sub -F32x4Trunc -F64Abs -F64Add -F64Ceil -F64Const -F64ConvertI32S -F64ConvertI32U -F64ConvertI64S -F64ConvertI64U -F64Copysign -F64Div -F64Eq -F64Floor -F64Ge -F64Gt -F64Le -F64Load -F64Lt -F64Max -F64Min -F64Mul -F64Ne -F64Nearest -F64Neg -F64PromoteF32 -F64ReinterpretI64 -F64Sqrt -F64Store -F64Sub -F64Trunc -F64x2Abs -F64x2Add -F64x2Ceil -F64x2ConvertLowI32x4S -F64x2ConvertLowI32x4U -F64x2Div -F64x2Eq -F64x2ExtractLane -F64x2Floor -F64x2Ge -F64x2Gt -F64x2Le -F64x2Lt -F64x2Max -F64x2Min -F64x2Mul -F64x2Ne -F64x2Nearest -F64x2Neg -F64x2PMax -F64x2PMin -F64x2PromoteLowF32x4 -F64x2ReplaceLane -F64x2Splat -F64x2Sqrt -F64x2Sub -F64x2Trunc -GlobalGet -GlobalSet -I16x8Abs -I16x8Add -I16x8AddSatS -I16x8AddSatU -I16x8AllTrue -I16x8Bitmask -I16x8Eq -I16x8ExtAddPairwiseI8x16S -I16x8ExtAddPairwiseI8x16U -I16x8ExtMulHighI8x16S -I16x8ExtMulHighI8x16U -I16x8ExtMulLowI8x16S -I16x8ExtMulLowI8x16U -I16x8ExtendHighI8x16S -I16x8ExtendHighI8x16U -I16x8ExtendLowI8x16S -I16x8ExtendLowI8x16U -I16x8ExtractLaneS -I16x8ExtractLaneU -I16x8GeS -I16x8GeU -I16x8GtS -I16x8GtU -I16x8LeS -I16x8LeU -I16x8LtS -I16x8LtU -I16x8MaxS -I16x8MaxU -I16x8MinS -I16x8MinU -I16x8Mul -I16x8NarrowI32x4S -I16x8NarrowI32x4U -I16x8Ne -I16x8Neg -I16x8Q15MulrSatS -I16x8ReplaceLane -I16x8RoundingAverageU -I16x8Shl -I16x8ShrS -I16x8ShrU -I16x8Splat -I16x8Sub -I16x8SubSatS -I16x8SubSatU -I32Add -I32And -I32AtomicLoad -I32AtomicLoad16U -I32AtomicLoad8U -I32AtomicRmw16AddU -I32AtomicRmw16AndU -I32AtomicRmw16CmpxchgU -I32AtomicRmw16OrU -I32AtomicRmw16SubU -I32AtomicRmw16XchgU -I32AtomicRmw16XorU -I32AtomicRmw8AddU -I32AtomicRmw8AndU -I32AtomicRmw8CmpxchgU -I32AtomicRmw8OrU -I32AtomicRmw8SubU -I32AtomicRmw8XchgU -I32AtomicRmw8XorU -I32AtomicRmwAdd -I32AtomicRmwAnd -I32AtomicRmwCmpxchg -I32AtomicRmwOr -I32AtomicRmwSub -I32AtomicRmwXchg -I32AtomicRmwXor -I32AtomicStore -I32AtomicStore16 -I32AtomicStore8 -I32Clz -I32Const -I32Ctz -I32DivS -I32DivU -I32Eq -I32Eqz -I32Extend16S -I32Extend8S -I32GeS -I32GeU -I32GtS -I32GtU -I32LeS -I32LeU -I32Load -I32Load16S -I32Load16U -I32Load8S -I32Load8U -I32LtS -I32LtU -I32Mul -I32Ne -I32Or -I32Popcnt -I32ReinterpretF32 -I32RemS -I32RemU -I32Rotl -I32Rotr -I32Shl -I32ShrS -I32ShrU -I32Store -I32Store16 -I32Store8 -I32Sub -I32TruncF32S -I32TruncF32U -I32TruncF64S -I32TruncF64U -I32TruncSatF32S -I32TruncSatF32U -I32TruncSatF64S -I32TruncSatF64U -I32WrapI64 -I32Xor -I32x4Abs -I32x4Add -I32x4AllTrue -I32x4Bitmask -I32x4DotI16x8S -I32x4Eq -I32x4ExtAddPairwiseI16x8S -I32x4ExtAddPairwiseI16x8U -I32x4ExtMulHighI16x8S -I32x4ExtMulHighI16x8U -I32x4ExtMulLowI16x8S -I32x4ExtMulLowI16x8U -I32x4ExtendHighI16x8S -I32x4ExtendHighI16x8U -I32x4ExtendLowI16x8S -I32x4ExtendLowI16x8U -I32x4ExtractLane -I32x4GeS -I32x4GeU -I32x4GtS -I32x4GtU -I32x4LeS -I32x4LeU -I32x4LtS -I32x4LtU -I32x4MaxS -I32x4MaxU -I32x4MinS -I32x4MinU -I32x4Mul -I32x4Ne -I32x4Neg -I32x4ReplaceLane -I32x4Shl -I32x4ShrS -I32x4ShrU -I32x4Splat -I32x4Sub -I32x4TruncSatF32x4S -I32x4TruncSatF32x4U -I32x4TruncSatF64x2SZero -I32x4TruncSatF64x2UZero -I64Add -I64And -I64AtomicLoad -I64AtomicLoad16U -I64AtomicLoad32U -I64AtomicLoad8U -I64AtomicRmw16AddU -I64AtomicRmw16AndU -I64AtomicRmw16CmpxchgU -I64AtomicRmw16OrU -I64AtomicRmw16SubU -I64AtomicRmw16XchgU -I64AtomicRmw16XorU -I64AtomicRmw32AddU -I64AtomicRmw32AndU -I64AtomicRmw32CmpxchgU -I64AtomicRmw32OrU -I64AtomicRmw32SubU -I64AtomicRmw32XchgU -I64AtomicRmw32XorU -I64AtomicRmw8AddU -I64AtomicRmw8AndU -I64AtomicRmw8CmpxchgU -I64AtomicRmw8OrU -I64AtomicRmw8SubU -I64AtomicRmw8XchgU -I64AtomicRmw8XorU -I64AtomicRmwAdd -I64AtomicRmwAnd -I64AtomicRmwCmpxchg -I64AtomicRmwOr -I64AtomicRmwSub -I64AtomicRmwXchg -I64AtomicRmwXor -I64AtomicStore -I64AtomicStore16 -I64AtomicStore32 -I64AtomicStore8 -I64Clz -I64Const -I64Ctz -I64DivS -I64DivU -I64Eq -I64Eqz -I64Extend16S -I64Extend32S -I64Extend8S -I64ExtendI32S -I64ExtendI32U -I64GeS -I64GeU -I64GtS -I64GtU -I64LeS -I64LeU -I64Load -I64Load16S -I64Load16U -I64Load32S -I64Load32U -I64Load8S -I64Load8U -I64LtS -I64LtU -I64Mul -I64Ne -I64Or -I64Popcnt -I64ReinterpretF64 -I64RemS -I64RemU -I64Rotl -I64Rotr -I64Shl -I64ShrS -I64ShrU -I64Store -I64Store16 -I64Store32 -I64Store8 -I64Sub -I64TruncF32S -I64TruncF32U -I64TruncF64S -I64TruncF64U -I64TruncSatF32S -I64TruncSatF32U -I64TruncSatF64S -I64TruncSatF64U -I64Xor -I64x2Abs -I64x2Add -I64x2AllTrue -I64x2Bitmask -I64x2Eq -I64x2ExtMulHighI32x4S -I64x2ExtMulHighI32x4U -I64x2ExtMulLowI32x4S -I64x2ExtMulLowI32x4U -I64x2ExtendHighI32x4S -I64x2ExtendHighI32x4U -I64x2ExtendLowI32x4S -I64x2ExtendLowI32x4U -I64x2ExtractLane -I64x2GeS -I64x2GtS -I64x2LeS -I64x2LtS -I64x2Mul -I64x2Ne -I64x2Neg -I64x2ReplaceLane -I64x2Shl -I64x2ShrS -I64x2ShrU -I64x2Splat -I64x2Sub -I8x16Abs -I8x16Add -I8x16AddSatS -I8x16AddSatU -I8x16AllTrue -I8x16Bitmask -I8x16Eq -I8x16ExtractLaneS -I8x16ExtractLaneU -I8x16GeS -I8x16GeU -I8x16GtS -I8x16GtU -I8x16LeS -I8x16LeU -I8x16LtS -I8x16LtU -I8x16MaxS -I8x16MaxU -I8x16MinS -I8x16MinU -I8x16NarrowI16x8S -I8x16NarrowI16x8U -I8x16Ne -I8x16Neg -I8x16Popcnt -I8x16ReplaceLane -I8x16RoundingAverageU -I8x16Shl -I8x16ShrS -I8x16ShrU -I8x16Shuffle -I8x16Splat -I8x16Sub -I8x16SubSatS -I8x16SubSatU -I8x16Swizzle -If -LocalGet -LocalSet -LocalTee -LocalAllocate -Loop -MemoryAtomicNotify -MemoryAtomicWait32 -MemoryAtomicWait64 -MemoryCopy -MemoryFill -MemoryGrow -MemoryInit -MemorySize -Nop -RefFunc -RefIsNull -RefNull -Rethrow -Return -ReturnCall -ReturnCallIndirect -Select -TableCopy -TableFill -TableGet -TableGrow -TableInit -TableSet -TableSize -Throw -Try -TypedSelect -Unreachable -Unwind -V128And -V128AndNot -V128AnyTrue -V128Bitselect -V128Const -V128Load -V128Load16Lane -V128Load16Splat -V128Load16x4S -V128Load16x4U -V128Load32Lane -V128Load32Splat -V128Load32Zero -V128Load32x2S -V128Load32x2U -V128Load64Lane -V128Load64Splat -V128Load64Zero -V128Load8Lane -V128Load8Splat -V128Load8x8S -V128Load8x8U -V128Not -V128Or -V128Store -V128Store16Lane -V128Store32Lane -V128Store64Lane -V128Store8Lane -V128Xor \ No newline at end of file diff --git a/vmhost/vmhooks/generate/eiGenOpcodeNames.go b/vmhost/vmhooks/generate/eiGenOpcodeNames.go new file mode 100644 index 000000000..7e947562d --- /dev/null +++ b/vmhost/vmhooks/generate/eiGenOpcodeNames.go @@ -0,0 +1,111 @@ +package vmhooksgenerate + +import ( + "bufio" + "os" +) + +type OpcodeNames struct { + AllowedV1 []string + AllowedV2 []string + + // PerByteCodes are the opcodes that, on top of their flat cost, are also charged + // for each byte they process (the bulk memory operators). + PerByteCodes []string + + // AdditionalCosts are executor costs that are not charged per opcode, so they cannot + // be found in any of the allowed opcode lists. + AdditionalCosts []string +} + +func loadOpcodeNamesForVersion(filePath string) []string { + var names []string + + readFile, err := os.Open(filePath) + if err != nil { + panic(err) + } + defer func() { _ = readFile.Close() }() + + fileScanner := bufio.NewScanner(readFile) + fileScanner.Split(bufio.ScanLines) + + for fileScanner.Scan() { + line := fileScanner.Text() + names = append(names, line) + } + + return names +} + +func LoadOpcodeNames() *OpcodeNames { + return &OpcodeNames{ + AllowedV1: loadOpcodeNamesForVersion("generate/cmd/input/wasm_allowed_opcodes_v1.txt"), + AllowedV2: loadOpcodeNamesForVersion("generate/cmd/input/wasm_allowed_opcodes_v2.txt"), + PerByteCodes: loadOpcodeNamesForVersion("generate/cmd/input/wasm_per_byte_opcodes.txt"), + AdditionalCosts: loadOpcodeNamesForVersion("generate/cmd/input/wasm_additional_costs.txt"), + } +} + +// AllCostNames yields every cost that the VM hands over to the executor: the opcodes allowed +// in any version, the per-byte costs of the bulk memory operators, and the additional costs, +// which are not charged per opcode. +// +// The result is the exact layout of the OpcodeCost structs on both sides of the FFI boundary, +// so changing it requires rebuilding the executor. +func (on *OpcodeNames) AllCostNames() []string { + var costNames []string + alreadyAdded := make(map[string]bool) + addCost := func(costName string) { + if alreadyAdded[costName] { + return + } + alreadyAdded[costName] = true + costNames = append(costNames, costName) + } + + for _, opcodeName := range on.AllowedV1 { + addCost(opcodeName) + } + for _, opcodeName := range on.AllowedV2 { + addCost(opcodeName) + } + for _, opcodeName := range on.PerByteCodes { + addCost(PerByteCostName(opcodeName)) + } + for _, costName := range on.AdditionalCosts { + addCost(costName) + } + + return costNames +} + +// IsPerByteOpcode returns true for the opcodes that are charged per processed byte, +// on top of their flat cost. +func (on *OpcodeNames) IsPerByteOpcode(opcodeName string) bool { + for _, perByteOpcode := range on.PerByteCodes { + if perByteOpcode == opcodeName { + return true + } + } + return false +} + +// PerByteCostName is the name of the cost of a single byte processed by an opcode. +// +// It complements the cost of the opcode itself, which is charged flat, no matter how +// many bytes end up being processed. +func PerByteCostName(opcodeName string) string { + return opcodeName + "PerByte" +} + +// maxCostNameLength helps with aligning to the right +func maxCostNameLength(costNames []string) int { + var max int + for _, name := range costNames { + if len(name) > max { + max = len(name) + } + } + return max +} diff --git a/vmhost/vmhooks/generate/eiGenWriteOpcodeCost.go b/vmhost/vmhooks/generate/eiGenWriteOpcodeCost.go index be01aeef9..c40899128 100644 --- a/vmhost/vmhooks/generate/eiGenWriteOpcodeCost.go +++ b/vmhost/vmhooks/generate/eiGenWriteOpcodeCost.go @@ -1,35 +1,47 @@ package vmhooksgenerate -import ( - "bufio" - "fmt" - "os" -) - // WriteWasmer2OpcodeCost generates code for wasmer2/opcodeCost.go func WriteWasmer2OpcodeCost(out *eiGenWriter) { - out.WriteString(`// Code generated by vmhooks generator. DO NOT EDIT. + costNames := LoadOpcodeNames().AllCostNames() + maxNameLength := maxCostNameLength(costNames) + + autoGeneratedHeader(out) + + out.WriteString(` +package wasmer2 -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -// !!!!!!!!!!!!!!!!!!!!!! AUTO-GENERATED FILE !!!!!!!!!!!!!!!!!!!!!! -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +import "github.com/multiversx/mx-chain-vm-go/executor" -`) - out.WriteString("package wasmer2\n\n") - out.WriteString("type OpcodeCost struct {\n") +type OpcodeCost struct {`) - readFile, err := os.Open("generate/cmd/input/wasmer2_opcodes_short.txt") - if err != nil { - panic(err) + for _, opcode := range costNames { + out.WriteString("\n\t") + out.WriteString(opcode) + for i := len(opcode); i < maxNameLength; i++ { + out.WriteString(" ") + } + out.WriteString(" uint32") } - defer readFile.Close() - fileScanner := bufio.NewScanner(readFile) - fileScanner.Split(bufio.ScanLines) + out.WriteString(` +} + +func (wasmerExecutor *Wasmer2Executor) extractOpcodeCost(wasmOps *executor.WASMOpcodeCost) *OpcodeCost { + return &OpcodeCost{`) - for fileScanner.Scan() { - opcode := fileScanner.Text() - out.WriteString(fmt.Sprintf("\t%-30suint32\n", opcode)) + for _, opcode := range costNames { + out.WriteString("\n\t\t") + out.WriteString(opcode) + out.WriteString(":") + for i := len(opcode); i < maxNameLength; i++ { + out.WriteString(" ") + } + out.WriteString(" wasmOps.") + out.WriteString(opcode) + out.WriteString(",") + // out.WriteString(fmt.Sprintf("\t\t%s: wasmOps.%s,\n", opcode, opcode)) } + + out.WriteString("\n\t}\n") out.WriteString("}\n") } diff --git a/vmhost/vmhooks/generate/eiGenWriteOpcodeCostFuncHelpers.go b/vmhost/vmhooks/generate/eiGenWriteOpcodeCostFuncHelpers.go deleted file mode 100644 index 26206712f..000000000 --- a/vmhost/vmhooks/generate/eiGenWriteOpcodeCostFuncHelpers.go +++ /dev/null @@ -1,39 +0,0 @@ -package vmhooksgenerate - -import ( - "bufio" - "fmt" - "os" -) - -// WriteOpcodeCostFuncHelpers generates code for extractOpcodeCost.txt -// (to be copied manually in wasmer2/wasmer2Executor.go) -func WriteOpcodeCostFuncHelpers(out *eiGenWriter) { - out.WriteString(`// Code generated by vmhooks generator. DO NOT EDIT. - -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -// !!!!!!!!!!!!!!!!!!!!!! AUTO-GENERATED FILE !!!!!!!!!!!!!!!!!!!!!! -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - -// !!!!!!!!!!!!!!! USE IN wasmer2/wasmer2Executor.go !!!!!!!!!!!!!!! - -`) - out.WriteString("func (wasmerExecutor *Wasmer2Executor) extractOpcodeCost(wasmOps *executor.WASMOpcodeCost) *OpcodeCost {\n") - out.WriteString("\treturn &OpcodeCost {\n") - - readFile, err := os.Open("generate/cmd/input/wasmer2_opcodes_short.txt") - if err != nil { - panic(err) - } - defer readFile.Close() - - fileScanner := bufio.NewScanner(readFile) - fileScanner.Split(bufio.ScanLines) - - for fileScanner.Scan() { - opcode := fileScanner.Text() - out.WriteString(fmt.Sprintf("\t\t%s: wasmOps.%s,\n", opcode, opcode)) - } - out.WriteString("\t}\n") - out.WriteString("}\n") -} diff --git a/vmhost/vmhooks/generate/eiGenWriteRustCapiOpcodeCost.go b/vmhost/vmhooks/generate/eiGenWriteRustCapiOpcodeCost.go new file mode 100644 index 000000000..b9e3c9f4c --- /dev/null +++ b/vmhost/vmhooks/generate/eiGenWriteRustCapiOpcodeCost.go @@ -0,0 +1,40 @@ +package vmhooksgenerate + +import ( + "fmt" + "strings" +) + +// WriteRustCapiOpcodeCost generates code for capi_opcode_cost.rs +func WriteRustCapiOpcodeCost(out *eiGenWriter) { + autoGeneratedHeader(out) + out.WriteString("\nuse multiversx_chain_vm_executor::OpcodeCost;\n\n") + out.WriteString("/// FFI-safe mirror of `OpcodeCost`.\n") + out.WriteString("///\n") + out.WriteString("/// This is a distinct type rather than a cast of `OpcodeCost` itself, so that\n") + out.WriteString("/// the general-purpose struct is free to change shape without touching the C\n") + out.WriteString("/// ABI, and vice versa.\n") + out.WriteString("#[allow(non_camel_case_types)]\n") + out.WriteString("#[repr(C)]\n") + out.WriteString("pub struct vm_exec_opcode_cost_t {\n") + + costNames := LoadOpcodeNames().AllCostNames() + + for _, opcode := range costNames { + out.WriteString(fmt.Sprintf(" pub opcode_%s: u32,\n", strings.ToLower(opcode))) + } + + out.WriteString("}\n\n") + out.WriteString("impl vm_exec_opcode_cost_t {\n") + out.WriteString(" pub(crate) fn to_opcode_cost(&self) -> OpcodeCost {\n") + out.WriteString(" OpcodeCost {\n") + + for _, opcode := range costNames { + fieldName := strings.ToLower(opcode) + out.WriteString(fmt.Sprintf(" opcode_%s: self.opcode_%s,\n", fieldName, fieldName)) + } + + out.WriteString(" }\n") + out.WriteString(" }\n") + out.WriteString("}\n") +} diff --git a/vmhost/vmhooks/generate/eiGenWriteRustOpcodeCost.go b/vmhost/vmhooks/generate/eiGenWriteRustOpcodeCost.go index acfe54aba..6785c6ce4 100644 --- a/vmhost/vmhooks/generate/eiGenWriteRustOpcodeCost.go +++ b/vmhost/vmhooks/generate/eiGenWriteRustOpcodeCost.go @@ -1,9 +1,7 @@ package vmhooksgenerate import ( - "bufio" "fmt" - "os" "strings" ) @@ -25,19 +23,12 @@ func WriteRustOpcodeCost(out *eiGenWriter) { out.WriteString("#[serde(default)]\n") out.WriteString("pub struct OpcodeCost {\n") - readFile, err := os.Open("generate/cmd/input/wasmer2_opcodes_short.txt") - if err != nil { - panic(err) - } - defer readFile.Close() - - fileScanner := bufio.NewScanner(readFile) - fileScanner.Split(bufio.ScanLines) + costNames := LoadOpcodeNames().AllCostNames() - for fileScanner.Scan() { - line := fileScanner.Text() - out.WriteString(fmt.Sprintf(" #[serde(rename = \"%s\", default)]\n", line)) - out.WriteString(fmt.Sprintf(" pub opcode_%s: u32,\n", strings.ToLower(line))) + for _, opcode := range costNames { + out.WriteString(fmt.Sprintf(" #[serde(rename = \"%s\", default)]\n", opcode)) + out.WriteString(fmt.Sprintf(" pub opcode_%s: u32,\n", strings.ToLower(opcode))) } + out.WriteString("}\n") } diff --git a/vmhost/vmhooks/generate/eiGenWriteRustOpcodeWhitelisted.go b/vmhost/vmhooks/generate/eiGenWriteRustOpcodeWhitelisted.go new file mode 100644 index 000000000..545f119d4 --- /dev/null +++ b/vmhost/vmhooks/generate/eiGenWriteRustOpcodeWhitelisted.go @@ -0,0 +1,62 @@ +package vmhooksgenerate + +import ( + "fmt" +) + +// WriteOpcodeWhitelisted generates code for opcode_whitelisted.rs +func WriteOpcodeWhitelisted(out *eiGenWriter) { + autoGeneratedHeader(out) + allowUnwind := false + allowedOpcodes := LoadOpcodeNames() + + out.WriteString(` +use wasmparser::Operator; + +use super::OpcodeVersion; + +pub fn is_opcode_whitelisted(op: &Operator, opcode_version: OpcodeVersion) -> bool { + match opcode_version { + OpcodeVersion::V1 => is_opcode_whitelisted_v1(op), + OpcodeVersion::V2 => is_opcode_whitelisted_v2(op), + } +} + +fn is_opcode_whitelisted_v1(op: &Operator) -> bool { + matches!( + op, +`) + + for i, opcodeName := range allowedOpcodes.AllowedV1 { + writeOperatorWhitelistMatchArm(out, allowUnwind, i, opcodeName) + } + + out.WriteString(` ) +} + +fn is_opcode_whitelisted_v2(op: &Operator) -> bool { + matches!( + op, +`) + + for i, opcodeName := range allowedOpcodes.AllowedV2 { + writeOperatorWhitelistMatchArm(out, allowUnwind, i, opcodeName) + } + + out.WriteString(` ) +} +`) + +} + +func writeOperatorWhitelistMatchArm(out *eiGenWriter, allowUnwind bool, i int, opcodeName string) { + if !allowUnwind && opcodeName == "Unwind" { + return + } + + if i == 0 { + out.WriteString(fmt.Sprintf(" Operator::%s { .. }\n", opcodeName)) + return + } + out.WriteString(fmt.Sprintf(" | Operator::%s { .. }\n", opcodeName)) +} diff --git a/vmhost/vmhooks/generate/eiGenWriteRustUtil.go b/vmhost/vmhooks/generate/eiGenWriteRustUtil.go index 9721a7339..5ce772792 100644 --- a/vmhost/vmhooks/generate/eiGenWriteRustUtil.go +++ b/vmhost/vmhooks/generate/eiGenWriteRustUtil.go @@ -53,6 +53,21 @@ func rustWasmerType(eiType EIType) string { return rustCapiType(eiType) } +func rustWasmparserValType(eiType EIType) string { + switch eiType { + case EITypeMemPtr: + fallthrough + case EITypeMemLength: + fallthrough + case EITypeInt32: + return "ValType::I32" + case EITypeInt64: + return "ValType::I64" + default: + panic("invalid type") + } +} + func rustWasmerProdConvertArg(arg *EIFunctionArg) string { argRustName := snakeCase(arg.Name) switch arg.Type { diff --git a/vmhost/vmhooks/generate/eiGenWriteRustVMHooksSignatures.go b/vmhost/vmhooks/generate/eiGenWriteRustVMHooksSignatures.go new file mode 100644 index 000000000..c0a43fe2c --- /dev/null +++ b/vmhost/vmhooks/generate/eiGenWriteRustVMHooksSignatures.go @@ -0,0 +1,40 @@ +package vmhooksgenerate + +// WriteRustVMHooksSignatures autogenerate data in the provided file +func WriteRustVMHooksSignatures(out *eiGenWriter, eiMetadata *EIMetadata) { + autoGeneratedHeader(out) + out.WriteString(` +use wasmparser::ValType; + +use super::VmHookSignature; + +#[rustfmt::skip] +pub const VM_HOOK_SIGNATURES: &[VmHookSignature] = &[ +`) + + for _, funcMetadata := range eiMetadata.AllFunctions { + out.WriteString(` VmHookSignature::new("`) + out.WriteString(lowerInitial(funcMetadata.Name)) + out.WriteString(`", &[`) + for i, arg := range funcMetadata.Arguments { + if i > 0 { + out.WriteString(", ") + } + out.WriteString(rustWasmparserValType(arg.Type)) + } + out.WriteString("], ") + if funcMetadata.Result == nil { + out.WriteString("None") + } else { + + out.WriteString("Some(") + out.WriteString(rustWasmparserValType(funcMetadata.Result.Type)) + out.WriteString(")") + } + out.WriteString("),\n") + } + + out.WriteString(`]; +`) + +} diff --git a/vmhost/vmhooks/generate/eiGenWriteRustWasmerExperimentalImports.go b/vmhost/vmhooks/generate/eiGenWriteRustWasmerExperimentalImports.go index 4b360f8ac..ca035bc05 100644 --- a/vmhost/vmhooks/generate/eiGenWriteRustWasmerExperimentalImports.go +++ b/vmhost/vmhooks/generate/eiGenWriteRustWasmerExperimentalImports.go @@ -11,9 +11,9 @@ func WriteRustWasmer5Imports(out *eiGenWriter, eiMetadata *EIMetadata) { #![allow(clippy::too_many_arguments)] use multiversx_chain_vm_executor::VMHooksEarlyExit; -use wasmer::{imports, Function, FunctionEnv, FunctionEnvMut, Imports, Store}; +use wasmer::{Function, FunctionEnv, FunctionEnvMut, Imports, Store, imports}; -use crate::we_vm_hooks::{convert_mem_length, convert_mem_ptr, with_vm_hooks, VMHooksWrapper}; +use crate::we_vm_hooks::{VMHooksWrapper, convert_mem_length, convert_mem_ptr, with_vm_hooks}; `) diff --git a/vmhost/vmhooks/generate/eiGenWriteRustWasmerMeteringHelpers.go b/vmhost/vmhooks/generate/eiGenWriteRustWasmerMeteringHelpers.go deleted file mode 100644 index 118c04a51..000000000 --- a/vmhost/vmhooks/generate/eiGenWriteRustWasmerMeteringHelpers.go +++ /dev/null @@ -1,54 +0,0 @@ -package vmhooksgenerate - -import ( - "bufio" - "os" - "strings" -) - -// Local cost name from config/config.toml (might change in the future) -const localCostName = "localallocate" - -// WriteRustWasmerMeteringHelpers generates code for wasmer_metering_helpers.rs -func WriteRustWasmerMeteringHelpers(out *eiGenWriter) { - autoGeneratedHeader(out) - out.WriteString("\nuse crate::executor_interface::OpcodeCost;\n") - out.WriteString("use wasmer::wasmparser::Operator;\n\n") - - readFile, err := os.Open("generate/cmd/input/wasmer2_opcodes_short.txt") - if err != nil { - panic(err) - } - defer readFile.Close() - - fileScanner := bufio.NewScanner(readFile) - fileScanner.Split(bufio.ScanLines) - - var content string - for fileScanner.Scan() { - line := fileScanner.Text() - if strings.ToLower(line) == localCostName { - // This operator doesn't really exist, so we skip it - continue - } - content += " Operator::" + line + " { .. } => " + "Some(opcode_cost.opcode_" + strings.ToLower(line) + "),\n" - } - - writeFnGetLocalCost(out) - writeFnGetOpcodeCost(out, content) -} - -func writeFnGetLocalCost(out *eiGenWriter) { - out.WriteString("pub fn get_local_cost(opcode_cost: &OpcodeCost) -> u32 {\n") - out.WriteString(" opcode_cost.opcode_" + localCostName + "\n") - out.WriteString("}\n\n") -} - -func writeFnGetOpcodeCost(out *eiGenWriter, content string) { - out.WriteString("pub fn get_opcode_cost(op: &Operator, opcode_cost: &OpcodeCost) -> Option {\n") - out.WriteString(" match op {\n") - out.WriteString(content) - out.WriteString(" _ => None,\n") - out.WriteString(" }\n") - out.WriteString("}\n") -} diff --git a/vmhost/vmhooks/generate/eiGenWriteRustWasmerOpcodeCost.go b/vmhost/vmhooks/generate/eiGenWriteRustWasmerOpcodeCost.go new file mode 100644 index 000000000..800ddd6fb --- /dev/null +++ b/vmhost/vmhooks/generate/eiGenWriteRustWasmerOpcodeCost.go @@ -0,0 +1,89 @@ +package vmhooksgenerate + +import ( + "fmt" + "strings" +) + +// WriteRustWasmerOpcodeCost generates code for wasmer_opcode_cost.rs +func WriteRustWasmerOpcodeCost(out *eiGenWriter) { + autoGeneratedHeader(out) + out.WriteString("\nuse crate::Cost;") + out.WriteString("\nuse crate::executor_interface::{OpcodeConfig, OpcodeCost, OpcodeVersion};") + writeOpcodeCostBody(out, true) + +} + +// WriteRustWasmerExperimentalOpcodeCost generates code for we_opcode_cost.rs +func WriteRustWasmerExperimentalOpcodeCost(out *eiGenWriter) { + autoGeneratedHeader(out) + out.WriteString("\nuse super::Cost;") + out.WriteString("\nuse multiversx_chain_vm_executor::{OpcodeConfig, OpcodeCost, OpcodeVersion};") + writeOpcodeCostBody(out, false) +} + +func writeOpcodeCostBody(out *eiGenWriter, allowUnwind bool) { + allowedOpcodes := LoadOpcodeNames() + + out.WriteString(` +use wasmer::wasmparser::Operator; + +pub fn get_opcode_cost(op: &Operator, opcode_config: &OpcodeConfig) -> Cost { + match opcode_config.opcode_version { + OpcodeVersion::V1 => get_opcode_cost_v1(op, &opcode_config.opcode_cost), + OpcodeVersion::V2 => get_opcode_cost_v2(op, &opcode_config.opcode_cost), + } +} + +#[rustfmt::skip] +fn get_opcode_cost_v1(op: &Operator, opcode_cost: &OpcodeCost) -> Cost { + match op { +`) + + for _, opcodeName := range allowedOpcodes.AllowedV1 { + writeOperatorMatchArm(out, allowUnwind, allowedOpcodes, opcodeName) + } + + out.WriteString(` _ => Cost::Illegal, + } +} + +#[rustfmt::skip] +fn get_opcode_cost_v2(op: &Operator, opcode_cost: &OpcodeCost) -> Cost { + match op { +`) + + for _, opcodeName := range allowedOpcodes.AllowedV2 { + writeOperatorMatchArm(out, allowUnwind, allowedOpcodes, opcodeName) + } + + out.WriteString(` _ => Cost::Illegal, + } +} +`) + +} + +func writeOperatorMatchArm(out *eiGenWriter, allowUnwind bool, allowedOpcodes *OpcodeNames, opcodeName string) { + if !allowUnwind && opcodeName == "Unwind" { + out.WriteString(" // Operator::Unwind { .. } => Cost::Base(opcode_cost.opcode_unwind), // unwind no longer available in Wasmer 6\n") + return + } + + if allowedOpcodes.IsPerByteOpcode(opcodeName) { + writeBulkMemoryMatchArm(out, opcodeName) + return + } + + writeMatchArm(out, opcodeName, fmt.Sprintf("Cost::Base(opcode_cost.opcode_%s)", strings.ToLower(opcodeName))) +} + +func writeBulkMemoryMatchArm(out *eiGenWriter, opcodeName string) { + writeMatchArm(out, opcodeName, fmt.Sprintf("Cost::BulkMemory { base: opcode_cost.opcode_%s, per_byte: opcode_cost.opcode_%s }", + strings.ToLower(opcodeName), strings.ToLower(PerByteCostName(opcodeName)))) +} + +// writeMatchArm writes one line per operator, the match functions are rustfmt::skip-ed. +func writeMatchArm(out *eiGenWriter, opcodeName string, armBody string) { + out.WriteString(fmt.Sprintf(" Operator::%s { .. } => %s,\n", opcodeName, armBody)) +} diff --git a/vmhost/vmhooks/generate/eiGenWriteRustWasmerProdImports.go b/vmhost/vmhooks/generate/eiGenWriteRustWasmerProdImports.go index 3c7d0d554..ed1fdea19 100644 --- a/vmhost/vmhooks/generate/eiGenWriteRustWasmerProdImports.go +++ b/vmhost/vmhooks/generate/eiGenWriteRustWasmerProdImports.go @@ -10,7 +10,7 @@ func WriteRustWasmerImports(out *eiGenWriter, eiMetadata *EIMetadata) { out.WriteString(` #![allow(clippy::too_many_arguments)] -use wasmer::{imports, Function, ImportObject, Store}; +use wasmer::{Function, ImportObject, Store, imports}; use crate::wasmer_vm_hooks::VMHooksWrapper; diff --git a/vmhost/vmhooks/generate/eiGenWriteWASMOpcodeCost.go b/vmhost/vmhooks/generate/eiGenWriteWASMOpcodeCost.go index 02247b146..71f5119df 100644 --- a/vmhost/vmhooks/generate/eiGenWriteWASMOpcodeCost.go +++ b/vmhost/vmhooks/generate/eiGenWriteWASMOpcodeCost.go @@ -18,11 +18,11 @@ func WriteExecutorOpcodeCost(out *eiGenWriter) { out.WriteString("package executor\n\n") out.WriteString("type WASMOpcodeCost struct {\n") - readFile, err := os.Open("generate/cmd/input/wasm_opcodes.txt") + readFile, err := os.Open("generate/cmd/input/wasm_all_opcodes.txt") if err != nil { panic(err) } - defer readFile.Close() + defer func() { _ = readFile.Close() }() fileScanner := bufio.NewScanner(readFile) fileScanner.Split(bufio.ScanLines) diff --git a/vmhost/vmhooks/generate/eiGenWriteWASMOpcodeCostConfigHelpers.go b/vmhost/vmhooks/generate/eiGenWriteWASMOpcodeCostConfigHelpers.go index af9f9a94b..a51160760 100644 --- a/vmhost/vmhooks/generate/eiGenWriteWASMOpcodeCostConfigHelpers.go +++ b/vmhost/vmhooks/generate/eiGenWriteWASMOpcodeCostConfigHelpers.go @@ -19,11 +19,11 @@ func WriteWASMOpcodeCostConfigHelpers(out *eiGenWriter) { `) out.WriteString("[WASMOpcodeCost]\n") - readFile, err := os.Open("generate/cmd/input/wasm_opcodes.txt") + readFile, err := os.Open("generate/cmd/input/wasm_all_opcodes.txt") if err != nil { panic(err) } - defer readFile.Close() + defer func() { _ = readFile.Close() }() fileScanner := bufio.NewScanner(readFile) fileScanner.Split(bufio.ScanLines) diff --git a/vmhost/vmhooks/generate/eiGenWriteWASMOpcodeCostFuncHelpers.go b/vmhost/vmhooks/generate/eiGenWriteWASMOpcodeCostFuncHelpers.go index 8c4498fb8..4c0a0e415 100644 --- a/vmhost/vmhooks/generate/eiGenWriteWASMOpcodeCostFuncHelpers.go +++ b/vmhost/vmhooks/generate/eiGenWriteWASMOpcodeCostFuncHelpers.go @@ -21,11 +21,11 @@ func WriteWASMOpcodeCostFuncHelpers(out *eiGenWriter) { out.WriteString("func FillGasMap_WASMOpcodeCosts(value uint64) map[string]uint64 {\n") out.WriteString("\tgasMap := make(map[string]uint64)\n\n") - readFile, err := os.Open("generate/cmd/input/wasm_opcodes.txt") + readFile, err := os.Open("generate/cmd/input/wasm_all_opcodes.txt") if err != nil { panic(err) } - defer readFile.Close() + defer func() { _ = readFile.Close() }() fileScanner := bufio.NewScanner(readFile) fileScanner.Split(bufio.ScanLines) diff --git a/wasmer2/bridge2.go b/wasmer2/bridge2.go index c1c6204c3..16f56b207 100644 --- a/wasmer2/bridge2.go +++ b/wasmer2/bridge2.go @@ -10,6 +10,8 @@ package wasmer2 import "C" import ( "unsafe" + + "github.com/multiversx/mx-chain-vm-go/executor" ) type cBool C.bool @@ -29,6 +31,21 @@ type cWasmerVmHookPointers = C.vm_exec_vm_hook_c_func_pointers const cWasmerOk = C.VM_EXEC_OK +// toCWasmerCompilationOptions converts field-by-field instead of reinterpreting +// executor.CompilationOptions as cWasmerCompilationOptions via unsafe.Pointer, so a field +// renamed, retyped, or removed on either side is caught by the compiler. +func toCWasmerCompilationOptions(options executor.CompilationOptions) cWasmerCompilationOptions { + return cWasmerCompilationOptions{ + gas_limit: C.uint64_t(options.GasLimit), + unmetered_locals: C.uint64_t(options.UnmeteredLocals), + max_memory_grow: C.uint64_t(options.MaxMemoryGrow), + max_memory_grow_delta: C.uint64_t(options.MaxMemoryGrowDelta), + opcode_trace: C.bool(options.OpcodeTrace), + metering: C.bool(options.Metering), + runtime_breakpoints: C.bool(options.RuntimeBreakpoints), + } +} + func cWasmerSetLogLevel( value uint64, ) cWasmerResultT { @@ -41,12 +58,14 @@ func cWasmerForceInstallSighandlers() { C.vm_force_sighandler_reinstall() } -func cWasmerExecutorSetOpcodeCost( +func cWasmerExecutorSetOpcodeConfig( executor *cWasmerExecutorT, + opcodeVersion int32, opcodeCost *cWasmerOpcodeCostT, ) cWasmerResultT { - return (cWasmerResultT)(C.vm_exec_set_opcode_costs( + return (cWasmerResultT)(C.vm_exec_set_opcode_config( (*C.vm_exec_executor_t)(executor), + (C.int32_t)(opcodeVersion), (*C.vm_exec_opcode_cost_t)(opcodeCost), )) } @@ -195,6 +214,12 @@ func cWasmerExecutorContextDataSet(executor *cWasmerExecutorT, vmHooksPtr unsafe ) } +func cWasmerExecutorDestroy(executor *cWasmerExecutorT) { + C.vm_exec_executor_destroy( + (*C.vm_exec_executor_t)(executor), + ) +} + func cWasmerInstanceDestroy(instance *cWasmerInstanceT) { C.vm_exec_instance_destroy( (*C.vm_exec_instance_t)(instance), diff --git a/wasmer2/bridge2_test.go b/wasmer2/bridge2_test.go new file mode 100644 index 000000000..ba099f7c7 --- /dev/null +++ b/wasmer2/bridge2_test.go @@ -0,0 +1,95 @@ +package wasmer2 + +import ( + "reflect" + "strings" + "testing" + + "github.com/multiversx/mx-chain-vm-go/executor" + "github.com/stretchr/testify/require" +) + +// setAllFields sets every field of the struct pointed to by ptr to a distinct non-zero value. +func setAllFields(t *testing.T, ptr interface{}) { + value := reflect.ValueOf(ptr).Elem() + for i := 0; i < value.NumField(); i++ { + field := value.Field(i) + switch field.Kind() { + case reflect.Uint32, reflect.Uint64: + field.SetUint(uint64(i) + 1) + case reflect.Bool: + field.SetBool(true) + default: + t.Fatalf("unsupported field kind %s for %s", field.Kind(), value.Type().Field(i).Name) + } + } +} + +// namedFields returns the fields of a struct type, skipping the blank padding +// fields that cgo may add. +func namedFields(structType reflect.Type) []reflect.StructField { + fields := make([]reflect.StructField, 0, structType.NumField()) + for i := 0; i < structType.NumField(); i++ { + if structType.Field(i).Name != "_" { + fields = append(fields, structType.Field(i)) + } + } + return fields +} + +// requireAllFieldsSet checks that no (non-padding) field of the struct is left at its zero value. +func requireAllFieldsSet(t *testing.T, structValue reflect.Value, what string) { + for _, field := range namedFields(structValue.Type()) { + require.False(t, structValue.FieldByName(field.Name).IsZero(), "%s: field %s not set", what, field.Name) + } +} + +// Guards toCWasmerCompilationOptions against a field being added to either +// executor.CompilationOptions or vm_exec_compilation_options_t without the +// conversion being updated: every field is set to a non-zero value, and the +// converted struct must have the same number of fields, all non-zero. +func TestToCWasmerCompilationOptions_CoversAllFields(t *testing.T) { + goOptions := executor.CompilationOptions{} + setAllFields(t, &goOptions) + + cValue := reflect.ValueOf(toCWasmerCompilationOptions(goOptions)) + requireAllFieldsSet(t, cValue, "compilation options") + require.Equal(t, reflect.TypeOf(goOptions).NumField(), len(namedFields(cValue.Type())), + "field count mismatch between Go and C compilation options") +} + +// OpcodeCost is reinterpreted as vm_exec_opcode_cost_t via unsafe.Pointer, so +// the two must have identical layout: same fields, in the same order, at the same offsets. +func TestOpcodeCost_LayoutMatchesC(t *testing.T) { + goType := reflect.TypeOf(OpcodeCost{}) + cType := reflect.TypeOf(cWasmerOpcodeCostT{}) + cFields := namedFields(cType) + + require.Equal(t, goType.Size(), cType.Size(), "size mismatch between Go and C opcode costs") + require.Equal(t, goType.NumField(), len(cFields), "field count mismatch between Go and C opcode costs") + for i, cField := range cFields { + goField := goType.Field(i) + cName := strings.TrimPrefix(cField.Name, "opcode_") + require.Equal(t, strings.ToLower(goField.Name), strings.ReplaceAll(cName, "_", ""), + "field %d name mismatch", i) + require.Equal(t, goField.Type.Kind(), cField.Type.Kind(), "field %s kind mismatch", goField.Name) + require.Equal(t, goField.Offset, cField.Offset, "field %s offset mismatch", goField.Name) + } +} + +// Every OpcodeCost field must be populated from executor.WASMOpcodeCost. +func TestExtractOpcodeCost_CoversAllFields(t *testing.T) { + wasmOps := executor.WASMOpcodeCost{} + setAllFields(t, &wasmOps) + + opcodeCost := (&Wasmer2Executor{}).extractOpcodeCost(&wasmOps) + requireAllFieldsSet(t, reflect.ValueOf(*opcodeCost), "opcode cost") +} + +// Every hook in vm_exec_vm_hook_c_func_pointers must be given a function pointer. +func TestPopulateCgoFunctionPointers_CoversAllFields(t *testing.T) { + pointers := reflect.ValueOf(*populateCgoFunctionPointers()) + for _, field := range namedFields(pointers.Type()) { + require.False(t, pointers.FieldByName(field.Name).IsNil(), "hook %s not populated", field.Name) + } +} diff --git a/wasmer2/libvmexeccapi.dylib b/wasmer2/libvmexeccapi.dylib index b571dacea..20364b8f1 100755 Binary files a/wasmer2/libvmexeccapi.dylib and b/wasmer2/libvmexeccapi.dylib differ diff --git a/wasmer2/libvmexeccapi.h b/wasmer2/libvmexeccapi.h index f183140bb..67ba27549 100644 --- a/wasmer2/libvmexeccapi.h +++ b/wasmer2/libvmexeccapi.h @@ -313,12 +313,160 @@ typedef struct vm_exec_vm_hook_c_func_pointers { int32_t (*managed_verify_blsaggregated_signature_func_ptr)(void *context, int32_t key_handle, int32_t message_handle, int32_t sig_handle); } vm_exec_vm_hook_c_func_pointers; +/** + * FFI-safe mirror of `CompilationOptionsLegacy`. + * + * This is a distinct type rather than a cast of `CompilationOptionsLegacy` + * itself, so that the general-purpose struct is free to change shape + * without touching the C ABI, and vice versa. Fields that are `usize` on + * the Rust side are fixed to `u64` here, since `usize` has no guaranteed + * width across host platforms. + */ typedef struct vm_exec_compilation_options_t { - + uint64_t gas_limit; + uint64_t unmetered_locals; + uint64_t max_memory_grow; + uint64_t max_memory_grow_delta; + bool opcode_trace; + bool metering; + bool runtime_breakpoints; } vm_exec_compilation_options_t; +/** + * FFI-safe mirror of `OpcodeCost`. + * + * This is a distinct type rather than a cast of `OpcodeCost` itself, so that + * the general-purpose struct is free to change shape without touching the C + * ABI, and vice versa. + */ typedef struct vm_exec_opcode_cost_t { - + uint32_t opcode_block; + uint32_t opcode_br; + uint32_t opcode_brif; + uint32_t opcode_brtable; + uint32_t opcode_call; + uint32_t opcode_callindirect; + uint32_t opcode_catch; + uint32_t opcode_catchall; + uint32_t opcode_delegate; + uint32_t opcode_drop; + uint32_t opcode_else; + uint32_t opcode_end; + uint32_t opcode_globalget; + uint32_t opcode_globalset; + uint32_t opcode_i32add; + uint32_t opcode_i32and; + uint32_t opcode_i32clz; + uint32_t opcode_i32const; + uint32_t opcode_i32ctz; + uint32_t opcode_i32divs; + uint32_t opcode_i32divu; + uint32_t opcode_i32eq; + uint32_t opcode_i32eqz; + uint32_t opcode_i32extend16s; + uint32_t opcode_i32extend8s; + uint32_t opcode_i32ges; + uint32_t opcode_i32geu; + uint32_t opcode_i32gts; + uint32_t opcode_i32gtu; + uint32_t opcode_i32les; + uint32_t opcode_i32leu; + uint32_t opcode_i32load; + uint32_t opcode_i32load16s; + uint32_t opcode_i32load16u; + uint32_t opcode_i32load8s; + uint32_t opcode_i32load8u; + uint32_t opcode_i32lts; + uint32_t opcode_i32ltu; + uint32_t opcode_i32mul; + uint32_t opcode_i32ne; + uint32_t opcode_i32or; + uint32_t opcode_i32popcnt; + uint32_t opcode_i32rems; + uint32_t opcode_i32remu; + uint32_t opcode_i32rotl; + uint32_t opcode_i32rotr; + uint32_t opcode_i32shl; + uint32_t opcode_i32shrs; + uint32_t opcode_i32shru; + uint32_t opcode_i32store; + uint32_t opcode_i32store16; + uint32_t opcode_i32store8; + uint32_t opcode_i32sub; + uint32_t opcode_i32wrapi64; + uint32_t opcode_i32xor; + uint32_t opcode_i64add; + uint32_t opcode_i64and; + uint32_t opcode_i64clz; + uint32_t opcode_i64const; + uint32_t opcode_i64ctz; + uint32_t opcode_i64divs; + uint32_t opcode_i64divu; + uint32_t opcode_i64eq; + uint32_t opcode_i64eqz; + uint32_t opcode_i64extend16s; + uint32_t opcode_i64extend32s; + uint32_t opcode_i64extend8s; + uint32_t opcode_i64extendi32s; + uint32_t opcode_i64extendi32u; + uint32_t opcode_i64ges; + uint32_t opcode_i64geu; + uint32_t opcode_i64gts; + uint32_t opcode_i64gtu; + uint32_t opcode_i64les; + uint32_t opcode_i64leu; + uint32_t opcode_i64load; + uint32_t opcode_i64load16s; + uint32_t opcode_i64load16u; + uint32_t opcode_i64load32s; + uint32_t opcode_i64load32u; + uint32_t opcode_i64load8s; + uint32_t opcode_i64load8u; + uint32_t opcode_i64lts; + uint32_t opcode_i64ltu; + uint32_t opcode_i64mul; + uint32_t opcode_i64ne; + uint32_t opcode_i64or; + uint32_t opcode_i64popcnt; + uint32_t opcode_i64rems; + uint32_t opcode_i64remu; + uint32_t opcode_i64rotl; + uint32_t opcode_i64rotr; + uint32_t opcode_i64shl; + uint32_t opcode_i64shrs; + uint32_t opcode_i64shru; + uint32_t opcode_i64store; + uint32_t opcode_i64store16; + uint32_t opcode_i64store32; + uint32_t opcode_i64store8; + uint32_t opcode_i64sub; + uint32_t opcode_i64xor; + uint32_t opcode_if; + uint32_t opcode_localget; + uint32_t opcode_localset; + uint32_t opcode_localtee; + uint32_t opcode_loop; + uint32_t opcode_memorygrow; + uint32_t opcode_memorysize; + uint32_t opcode_nop; + uint32_t opcode_reffunc; + uint32_t opcode_refisnull; + uint32_t opcode_refnull; + uint32_t opcode_rethrow; + uint32_t opcode_return; + uint32_t opcode_returncall; + uint32_t opcode_returncallindirect; + uint32_t opcode_select; + uint32_t opcode_throw; + uint32_t opcode_try; + uint32_t opcode_typedselect; + uint32_t opcode_unreachable; + uint32_t opcode_unwind; + uint32_t opcode_memorycopy; + uint32_t opcode_memoryfill; + uint32_t opcode_memorycopyperbyte; + uint32_t opcode_memoryfillperbyte; + uint32_t opcode_localallocate; } vm_exec_opcode_cost_t; /** @@ -600,7 +748,7 @@ enum vm_exec_result_t vm_exec_instance_memory_grow(struct vm_exec_instance_t *in * * C API function, works with raw object pointers. */ -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_version_code, const struct vm_exec_opcode_cost_t *opcode_cost_ptr); /** * Sets the number of points(gas) limit for the given instance. diff --git a/wasmer2/libvmexeccapi.so b/wasmer2/libvmexeccapi.so old mode 100755 new mode 100644 index 05989230a..40d3a3e0c Binary files a/wasmer2/libvmexeccapi.so and b/wasmer2/libvmexeccapi.so differ diff --git a/wasmer2/libvmexeccapi_arm.dylib b/wasmer2/libvmexeccapi_arm.dylib index 060d40701..e867305f6 100755 Binary files a/wasmer2/libvmexeccapi_arm.dylib and b/wasmer2/libvmexeccapi_arm.dylib differ diff --git a/wasmer2/libvmexeccapi_arm.so b/wasmer2/libvmexeccapi_arm.so index d9243aa1b..1956ded65 100644 Binary files a/wasmer2/libvmexeccapi_arm.so and b/wasmer2/libvmexeccapi_arm.so differ diff --git a/wasmer2/opcodeConfig_test.go b/wasmer2/opcodeConfig_test.go new file mode 100644 index 000000000..d840bae50 --- /dev/null +++ b/wasmer2/opcodeConfig_test.go @@ -0,0 +1,56 @@ +package wasmer2 + +import ( + "testing" + + "github.com/multiversx/mx-chain-vm-go/config" + "github.com/multiversx/mx-chain-vm-go/executor" + "github.com/stretchr/testify/require" +) + +func testOpcodeCosts(t *testing.T) *executor.WASMOpcodeCost { + gasCostConfig, err := config.CreateGasConfig(config.MakeGasMapForTests()) + require.Nil(t, err) + return gasCostConfig.WASMOpcodeCost +} + +func TestWasmer2Executor_SetOpcodeConfig(t *testing.T) { + wasmerExecutor, err := CreateExecutor() + require.Nil(t, err) + + costs := testOpcodeCosts(t) + for _, version := range []executor.OpcodeVersion{executor.OpcodeVersionV1, executor.OpcodeVersionV2} { + require.Nil(t, wasmerExecutor.SetOpcodeConfig(version, costs)) + require.NotNil(t, wasmerExecutor.opcodeCost) + } +} + +func TestWasmer2ExecutorFactory_CreateExecutor_WithOpcodeCosts(t *testing.T) { + factory := ExecutorFactory() + + for _, version := range []executor.OpcodeVersion{executor.OpcodeVersionV1, executor.OpcodeVersionV2} { + exec, err := factory.CreateExecutor(executor.ExecutorFactoryArgs{ + OpcodeVersion: version, + OpcodeCosts: testOpcodeCosts(t), + }) + require.Nil(t, err) + require.NotNil(t, exec) + } +} + +// An executor with no cost table does not fail loudly - it meters every opcode at zero and +// runs contracts for free. Both ways of configuring one must refuse it. +func TestWasmer2Executor_NilOpcodeCostsRejected(t *testing.T) { + t.Run("SetOpcodeConfig", func(t *testing.T) { + wasmerExecutor, err := CreateExecutor() + require.Nil(t, err) + + require.ErrorIs(t, wasmerExecutor.SetOpcodeConfig(executor.OpcodeVersionV2, nil), executor.ErrNilOpcodeCosts) + }) + + t.Run("CreateExecutor", func(t *testing.T) { + exec, err := ExecutorFactory().CreateExecutor(executor.ExecutorFactoryArgs{}) + require.ErrorIs(t, err, executor.ErrNilOpcodeCosts) + require.Nil(t, exec) + }) +} diff --git a/wasmer2/opcodeCost.go b/wasmer2/opcodeCost.go index 07c5030b3..813241a2e 100644 --- a/wasmer2/opcodeCost.go +++ b/wasmer2/opcodeCost.go @@ -6,6 +6,8 @@ package wasmer2 +import "github.com/multiversx/mx-chain-vm-go/executor" + type OpcodeCost struct { Block uint32 Br uint32 @@ -112,7 +114,6 @@ type OpcodeCost struct { LocalGet uint32 LocalSet uint32 LocalTee uint32 - LocalAllocate uint32 Loop uint32 MemoryGrow uint32 MemorySize uint32 @@ -130,4 +131,141 @@ type OpcodeCost struct { TypedSelect uint32 Unreachable uint32 Unwind uint32 + MemoryCopy uint32 + MemoryFill uint32 + MemoryCopyPerByte uint32 + MemoryFillPerByte uint32 + LocalAllocate uint32 +} + +func (wasmerExecutor *Wasmer2Executor) extractOpcodeCost(wasmOps *executor.WASMOpcodeCost) *OpcodeCost { + return &OpcodeCost{ + Block: wasmOps.Block, + Br: wasmOps.Br, + BrIf: wasmOps.BrIf, + BrTable: wasmOps.BrTable, + Call: wasmOps.Call, + CallIndirect: wasmOps.CallIndirect, + Catch: wasmOps.Catch, + CatchAll: wasmOps.CatchAll, + Delegate: wasmOps.Delegate, + Drop: wasmOps.Drop, + Else: wasmOps.Else, + End: wasmOps.End, + GlobalGet: wasmOps.GlobalGet, + GlobalSet: wasmOps.GlobalSet, + I32Add: wasmOps.I32Add, + I32And: wasmOps.I32And, + I32Clz: wasmOps.I32Clz, + I32Const: wasmOps.I32Const, + I32Ctz: wasmOps.I32Ctz, + I32DivS: wasmOps.I32DivS, + I32DivU: wasmOps.I32DivU, + I32Eq: wasmOps.I32Eq, + I32Eqz: wasmOps.I32Eqz, + I32Extend16S: wasmOps.I32Extend16S, + I32Extend8S: wasmOps.I32Extend8S, + I32GeS: wasmOps.I32GeS, + I32GeU: wasmOps.I32GeU, + I32GtS: wasmOps.I32GtS, + I32GtU: wasmOps.I32GtU, + I32LeS: wasmOps.I32LeS, + I32LeU: wasmOps.I32LeU, + I32Load: wasmOps.I32Load, + I32Load16S: wasmOps.I32Load16S, + I32Load16U: wasmOps.I32Load16U, + I32Load8S: wasmOps.I32Load8S, + I32Load8U: wasmOps.I32Load8U, + I32LtS: wasmOps.I32LtS, + I32LtU: wasmOps.I32LtU, + I32Mul: wasmOps.I32Mul, + I32Ne: wasmOps.I32Ne, + I32Or: wasmOps.I32Or, + I32Popcnt: wasmOps.I32Popcnt, + I32RemS: wasmOps.I32RemS, + I32RemU: wasmOps.I32RemU, + I32Rotl: wasmOps.I32Rotl, + I32Rotr: wasmOps.I32Rotr, + I32Shl: wasmOps.I32Shl, + I32ShrS: wasmOps.I32ShrS, + I32ShrU: wasmOps.I32ShrU, + I32Store: wasmOps.I32Store, + I32Store16: wasmOps.I32Store16, + I32Store8: wasmOps.I32Store8, + I32Sub: wasmOps.I32Sub, + I32WrapI64: wasmOps.I32WrapI64, + I32Xor: wasmOps.I32Xor, + I64Add: wasmOps.I64Add, + I64And: wasmOps.I64And, + I64Clz: wasmOps.I64Clz, + I64Const: wasmOps.I64Const, + I64Ctz: wasmOps.I64Ctz, + I64DivS: wasmOps.I64DivS, + I64DivU: wasmOps.I64DivU, + I64Eq: wasmOps.I64Eq, + I64Eqz: wasmOps.I64Eqz, + I64Extend16S: wasmOps.I64Extend16S, + I64Extend32S: wasmOps.I64Extend32S, + I64Extend8S: wasmOps.I64Extend8S, + I64ExtendI32S: wasmOps.I64ExtendI32S, + I64ExtendI32U: wasmOps.I64ExtendI32U, + I64GeS: wasmOps.I64GeS, + I64GeU: wasmOps.I64GeU, + I64GtS: wasmOps.I64GtS, + I64GtU: wasmOps.I64GtU, + I64LeS: wasmOps.I64LeS, + I64LeU: wasmOps.I64LeU, + I64Load: wasmOps.I64Load, + I64Load16S: wasmOps.I64Load16S, + I64Load16U: wasmOps.I64Load16U, + I64Load32S: wasmOps.I64Load32S, + I64Load32U: wasmOps.I64Load32U, + I64Load8S: wasmOps.I64Load8S, + I64Load8U: wasmOps.I64Load8U, + I64LtS: wasmOps.I64LtS, + I64LtU: wasmOps.I64LtU, + I64Mul: wasmOps.I64Mul, + I64Ne: wasmOps.I64Ne, + I64Or: wasmOps.I64Or, + I64Popcnt: wasmOps.I64Popcnt, + I64RemS: wasmOps.I64RemS, + I64RemU: wasmOps.I64RemU, + I64Rotl: wasmOps.I64Rotl, + I64Rotr: wasmOps.I64Rotr, + I64Shl: wasmOps.I64Shl, + I64ShrS: wasmOps.I64ShrS, + I64ShrU: wasmOps.I64ShrU, + I64Store: wasmOps.I64Store, + I64Store16: wasmOps.I64Store16, + I64Store32: wasmOps.I64Store32, + I64Store8: wasmOps.I64Store8, + I64Sub: wasmOps.I64Sub, + I64Xor: wasmOps.I64Xor, + If: wasmOps.If, + LocalGet: wasmOps.LocalGet, + LocalSet: wasmOps.LocalSet, + LocalTee: wasmOps.LocalTee, + Loop: wasmOps.Loop, + MemoryGrow: wasmOps.MemoryGrow, + MemorySize: wasmOps.MemorySize, + Nop: wasmOps.Nop, + RefFunc: wasmOps.RefFunc, + RefIsNull: wasmOps.RefIsNull, + RefNull: wasmOps.RefNull, + Rethrow: wasmOps.Rethrow, + Return: wasmOps.Return, + ReturnCall: wasmOps.ReturnCall, + ReturnCallIndirect: wasmOps.ReturnCallIndirect, + Select: wasmOps.Select, + Throw: wasmOps.Throw, + Try: wasmOps.Try, + TypedSelect: wasmOps.TypedSelect, + Unreachable: wasmOps.Unreachable, + Unwind: wasmOps.Unwind, + MemoryCopy: wasmOps.MemoryCopy, + MemoryFill: wasmOps.MemoryFill, + MemoryCopyPerByte: wasmOps.MemoryCopyPerByte, + MemoryFillPerByte: wasmOps.MemoryFillPerByte, + LocalAllocate: wasmOps.LocalAllocate, + } } diff --git a/wasmer2/wasmer2Executor.go b/wasmer2/wasmer2Executor.go index 1e15659b5..6562c1815 100644 --- a/wasmer2/wasmer2Executor.go +++ b/wasmer2/wasmer2Executor.go @@ -52,14 +52,38 @@ func CreateExecutor() (*Wasmer2Executor, error) { return wasmerExecutor, nil } -// SetOpcodeCosts sets gas costs globally inside the Wasmer executor. -func (wasmerExecutor *Wasmer2Executor) SetOpcodeCosts(wasmOps *executor.WASMOpcodeCost) { +// destroy releases the native executor. It must not be used afterwards. +func (wasmerExecutor *Wasmer2Executor) destroy() { + if wasmerExecutor.cgoExecutor == nil { + return + } + cWasmerExecutorDestroy(wasmerExecutor.cgoExecutor) + wasmerExecutor.cgoExecutor = nil +} + +// SetOpcodeConfig sets the opcode version and opcode costs, based on the gas schedule. +func (wasmerExecutor *Wasmer2Executor) SetOpcodeConfig(opcodeVersion executor.OpcodeVersion, wasmOps *executor.WASMOpcodeCost) error { + if wasmOps == nil { + // Checked here rather than left to extractOpcodeCost, which reads every field and + // would panic inside cgo-locked code. + return executor.ErrNilOpcodeCosts + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + // extract only wasmer2 opcodes wasmerExecutor.opcodeCost = wasmerExecutor.extractOpcodeCost(wasmOps) - cWasmerExecutorSetOpcodeCost( + result := cWasmerExecutorSetOpcodeConfig( wasmerExecutor.cgoExecutor, + (int32)(opcodeVersion), (*cWasmerOpcodeCostT)(unsafe.Pointer(wasmerExecutor.opcodeCost)), ) + if result != cWasmerOk { + return newWrappedError(ErrFailedInstantiation) + } + + return nil } // SetRkyvSerializationEnabled controls a Wasmer flag. @@ -89,13 +113,13 @@ func (wasmerExecutor *Wasmer2Executor) NewInstanceWithOptions( return nil, newWrappedError(ErrInvalidBytecode) } - cOptions := unsafe.Pointer(&options) + cOptions := toCWasmerCompilationOptions(options) var compileResult = cWasmerInstantiateWithOptions( wasmerExecutor.cgoExecutor, &cInstance, (*cUchar)(unsafe.Pointer(&contractCode[0])), cUint(len(contractCode)), - (*cWasmerCompilationOptions)(cOptions), + &cOptions, ) if compileResult != cWasmerOk { @@ -120,13 +144,13 @@ func (wasmerExecutor *Wasmer2Executor) NewInstanceFromCompiledCodeWithOptions( return nil, newWrappedError(ErrInvalidBytecode) } - cOptions := unsafe.Pointer(&options) + cOptions := toCWasmerCompilationOptions(options) var compileResult = cWasmerInstanceFromCache( wasmerExecutor.cgoExecutor, &cInstance, (*cUchar)(unsafe.Pointer(&compiledCode[0])), cUint32T(len(compiledCode)), - (*cWasmerCompilationOptions)(cOptions), + &cOptions, ) if compileResult != cWasmerOk { @@ -149,131 +173,3 @@ func (wasmerExecutor *Wasmer2Executor) initVMHooks(vmHooks executor.VMHooks) { wasmerExecutor.vmHooksPtrPtr = unsafe.Pointer(&localPtr) cWasmerExecutorContextDataSet(wasmerExecutor.cgoExecutor, wasmerExecutor.vmHooksPtrPtr) } - -func (wasmerExecutor *Wasmer2Executor) extractOpcodeCost(wasmOps *executor.WASMOpcodeCost) *OpcodeCost { - return &OpcodeCost{ - Block: wasmOps.Block, - Br: wasmOps.Br, - BrIf: wasmOps.BrIf, - BrTable: wasmOps.BrTable, - Call: wasmOps.Call, - CallIndirect: wasmOps.CallIndirect, - Catch: wasmOps.Catch, - CatchAll: wasmOps.CatchAll, - Delegate: wasmOps.Delegate, - Drop: wasmOps.Drop, - Else: wasmOps.Else, - End: wasmOps.End, - GlobalGet: wasmOps.GlobalGet, - GlobalSet: wasmOps.GlobalSet, - I32Add: wasmOps.I32Add, - I32And: wasmOps.I32And, - I32Clz: wasmOps.I32Clz, - I32Const: wasmOps.I32Const, - I32Ctz: wasmOps.I32Ctz, - I32DivS: wasmOps.I32DivS, - I32DivU: wasmOps.I32DivU, - I32Eq: wasmOps.I32Eq, - I32Eqz: wasmOps.I32Eqz, - I32Extend16S: wasmOps.I32Extend16S, - I32Extend8S: wasmOps.I32Extend8S, - I32GeS: wasmOps.I32GeS, - I32GeU: wasmOps.I32GeU, - I32GtS: wasmOps.I32GtS, - I32GtU: wasmOps.I32GtU, - I32LeS: wasmOps.I32LeS, - I32LeU: wasmOps.I32LeU, - I32Load: wasmOps.I32Load, - I32Load16S: wasmOps.I32Load16S, - I32Load16U: wasmOps.I32Load16U, - I32Load8S: wasmOps.I32Load8S, - I32Load8U: wasmOps.I32Load8U, - I32LtS: wasmOps.I32LtS, - I32LtU: wasmOps.I32LtU, - I32Mul: wasmOps.I32Mul, - I32Ne: wasmOps.I32Ne, - I32Or: wasmOps.I32Or, - I32Popcnt: wasmOps.I32Popcnt, - I32RemS: wasmOps.I32RemS, - I32RemU: wasmOps.I32RemU, - I32Rotl: wasmOps.I32Rotl, - I32Rotr: wasmOps.I32Rotr, - I32Shl: wasmOps.I32Shl, - I32ShrS: wasmOps.I32ShrS, - I32ShrU: wasmOps.I32ShrU, - I32Store: wasmOps.I32Store, - I32Store16: wasmOps.I32Store16, - I32Store8: wasmOps.I32Store8, - I32Sub: wasmOps.I32Sub, - I32WrapI64: wasmOps.I32WrapI64, - I32Xor: wasmOps.I32Xor, - I64Add: wasmOps.I64Add, - I64And: wasmOps.I64And, - I64Clz: wasmOps.I64Clz, - I64Const: wasmOps.I64Const, - I64Ctz: wasmOps.I64Ctz, - I64DivS: wasmOps.I64DivS, - I64DivU: wasmOps.I64DivU, - I64Eq: wasmOps.I64Eq, - I64Eqz: wasmOps.I64Eqz, - I64Extend16S: wasmOps.I64Extend16S, - I64Extend32S: wasmOps.I64Extend32S, - I64Extend8S: wasmOps.I64Extend8S, - I64ExtendI32S: wasmOps.I64ExtendI32S, - I64ExtendI32U: wasmOps.I64ExtendI32U, - I64GeS: wasmOps.I64GeS, - I64GeU: wasmOps.I64GeU, - I64GtS: wasmOps.I64GtS, - I64GtU: wasmOps.I64GtU, - I64LeS: wasmOps.I64LeS, - I64LeU: wasmOps.I64LeU, - I64Load: wasmOps.I64Load, - I64Load16S: wasmOps.I64Load16S, - I64Load16U: wasmOps.I64Load16U, - I64Load32S: wasmOps.I64Load32S, - I64Load32U: wasmOps.I64Load32U, - I64Load8S: wasmOps.I64Load8S, - I64Load8U: wasmOps.I64Load8U, - I64LtS: wasmOps.I64LtS, - I64LtU: wasmOps.I64LtU, - I64Mul: wasmOps.I64Mul, - I64Ne: wasmOps.I64Ne, - I64Or: wasmOps.I64Or, - I64Popcnt: wasmOps.I64Popcnt, - I64RemS: wasmOps.I64RemS, - I64RemU: wasmOps.I64RemU, - I64Rotl: wasmOps.I64Rotl, - I64Rotr: wasmOps.I64Rotr, - I64Shl: wasmOps.I64Shl, - I64ShrS: wasmOps.I64ShrS, - I64ShrU: wasmOps.I64ShrU, - I64Store: wasmOps.I64Store, - I64Store16: wasmOps.I64Store16, - I64Store32: wasmOps.I64Store32, - I64Store8: wasmOps.I64Store8, - I64Sub: wasmOps.I64Sub, - I64Xor: wasmOps.I64Xor, - If: wasmOps.If, - LocalGet: wasmOps.LocalGet, - LocalSet: wasmOps.LocalSet, - LocalTee: wasmOps.LocalTee, - LocalAllocate: wasmOps.LocalAllocate, - Loop: wasmOps.Loop, - MemoryGrow: wasmOps.MemoryGrow, - MemorySize: wasmOps.MemorySize, - Nop: wasmOps.Nop, - RefFunc: wasmOps.RefFunc, - RefIsNull: wasmOps.RefIsNull, - RefNull: wasmOps.RefNull, - Rethrow: wasmOps.Rethrow, - Return: wasmOps.Return, - ReturnCall: wasmOps.ReturnCall, - ReturnCallIndirect: wasmOps.ReturnCallIndirect, - Select: wasmOps.Select, - Throw: wasmOps.Throw, - Try: wasmOps.Try, - TypedSelect: wasmOps.TypedSelect, - Unreachable: wasmOps.Unreachable, - Unwind: wasmOps.Unwind, - } -} diff --git a/wasmer2/wasmer2ExecutorFactory.go b/wasmer2/wasmer2ExecutorFactory.go index b3b28a549..ecef0f298 100644 --- a/wasmer2/wasmer2ExecutorFactory.go +++ b/wasmer2/wasmer2ExecutorFactory.go @@ -17,20 +17,31 @@ func ExecutorFactory() *Wasmer2ExecutorFactory { } // CreateExecutor creates a new Executor instance. +// +// The opcode cost table is required. It used to be applied only when present, so that tests +// which had not built one could still create an executor - but an executor without a cost +// table does not fail, it meters every opcode at zero, and the contracts it runs are free. +// A caller with no schedule of its own passes config.MakeGasMapForTests. func (wef *Wasmer2ExecutorFactory) CreateExecutor(args executor.ExecutorFactoryArgs) (executor.Executor, error) { + if args.OpcodeCosts == nil { + return nil, executor.ErrNilOpcodeCosts + } + signal.Reset() - executor, err := CreateExecutor() + wasmerExecutor, err := CreateExecutor() if err != nil { return nil, err } - executor.initVMHooks(args.VMHooks) - if args.OpcodeCosts != nil { - // opcode costs are sometimes not initialized at this point in certain tests - executor.SetOpcodeCosts(args.OpcodeCosts) + wasmerExecutor.initVMHooks(args.VMHooks) + err = wasmerExecutor.SetOpcodeConfig(args.OpcodeVersion, args.OpcodeCosts) + if err != nil { + // the native executor is not garbage collected, release it explicitly + wasmerExecutor.destroy() + return nil, err } - return executor, nil + return wasmerExecutor, nil } // IsInterfaceNil returns true if there is no value under the interface diff --git a/wasmer2/wasmer2MemoryReset_test.go b/wasmer2/wasmer2MemoryReset_test.go index a6dede05d..424b849e4 100644 --- a/wasmer2/wasmer2MemoryReset_test.go +++ b/wasmer2/wasmer2MemoryReset_test.go @@ -6,6 +6,7 @@ import ( "testing" "unsafe" + "github.com/multiversx/mx-chain-vm-go/config" "github.com/multiversx/mx-chain-vm-go/executor" "github.com/stretchr/testify/require" ) @@ -24,7 +25,13 @@ func newMemoryResetInstance(t *testing.T) executor.Instance { code, err := os.ReadFile(filepath.Join("..", "test", "contracts", "memory-reset", "output", "memory-reset.wasm")) require.Nil(t, err) - exec, err := ExecutorFactory().CreateExecutor(executor.ExecutorFactoryArgs{}) + gasCostConfig, err := config.CreateGasConfig(config.MakeGasMapForTests()) + require.Nil(t, err) + + exec, err := ExecutorFactory().CreateExecutor(executor.ExecutorFactoryArgs{ + OpcodeVersion: executor.OpcodeVersionV2, + OpcodeCosts: gasCostConfig.WASMOpcodeCost, + }) require.Nil(t, err) instance, err := exec.NewInstanceWithOptions(code, executor.CompilationOptions{