diff --git a/cmd/demo/main.go b/cmd/demo/main.go index 58286e782..1aecd958a 100644 --- a/cmd/demo/main.go +++ b/cmd/demo/main.go @@ -2,55 +2,47 @@ package main import ( "fmt" - "math" "os" wasmvm "github.com/CosmWasm/wasmvm/v2" + "github.com/CosmWasm/wasmvm/v2/types" ) -const ( - PRINT_DEBUG = true - MEMORY_LIMIT = 32 // MiB - CACHE_SIZE = 100 // MiB -) - -var SUPPORTED_CAPABILITIES = []string{"staking"} - -// This is just a demo to ensure we can compile a static go binary func main() { - file := os.Args[1] - - if file == "version" { - libwasmvmVersion, err := wasmvm.LibwasmvmVersion() - if err != nil { - panic(err) - } - fmt.Printf("libwasmvm: %s\n", libwasmvmVersion) - return + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) + os.Exit(1) } - - fmt.Printf("Running %s...\n", file) - bz, err := os.ReadFile(file) - if err != nil { - panic(err) + wasmFile := os.Args[1] + + config := types.VMConfig{ + WasmLimits: types.WasmLimits{}, + Cache: types.CacheOptions{ + BaseDir: os.TempDir(), + AvailableCapabilities: []string{"staking", "stargate", "iterator"}, + MemoryCacheSizeBytes: types.NewSizeKibi(100 * 1024), // 100 MiB + InstanceMemoryLimitBytes: types.NewSizeKibi(32 * 1024), // 32 MiB + }, } - fmt.Println("Loaded!") - err = os.MkdirAll("tmp", 0o755) + vm, err := wasmvm.NewVM(config) if err != nil { - panic(err) + fmt.Fprintf(os.Stderr, "Failed to create VM: %v\n", err) + os.Exit(1) } - vm, err := wasmvm.NewVM("tmp", SUPPORTED_CAPABILITIES, MEMORY_LIMIT, PRINT_DEBUG, CACHE_SIZE) + defer vm.Cleanup() + + code, err := os.ReadFile(wasmFile) if err != nil { - panic(err) + fmt.Fprintf(os.Stderr, "Failed to read Wasm file: %v\n", err) + os.Exit(1) } - checksum, _, err := vm.StoreCode(bz, math.MaxUint64) + checksum, _, err := vm.StoreCode(code, 500_000_000_000) if err != nil { - panic(err) + fmt.Fprintf(os.Stderr, "Failed to store code: %v\n", err) + os.Exit(1) } - fmt.Printf("Stored code with checksum: %X\n", checksum) - vm.Cleanup() - fmt.Println("finished") + fmt.Printf("Code stored with checksum: %X\n", checksum) } diff --git a/go.mod b/go.mod index b8a003356..04a7cf368 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/CosmWasm/wasmvm/v2 -go 1.21 +go 1.23 require ( github.com/google/btree v1.0.0 github.com/shamaton/msgpack/v2 v2.2.0 github.com/stretchr/testify v1.8.1 - golang.org/x/sys v0.16.0 + github.com/tetratelabs/wazero v1.8.2 ) require ( diff --git a/go.sum b/go.sum index 0e767c24f..eb21c5837 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= +github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/ibc_test.go b/ibc_test.go index 2da754d14..496a875ac 100644 --- a/ibc_test.go +++ b/ibc_test.go @@ -1,5 +1,3 @@ -//go:build cgo && !nolink_libwasmvm - package cosmwasm import ( diff --git a/internal/api/api_test.go b/internal/api/api_test.go deleted file mode 100644 index 0f30eee16..000000000 --- a/internal/api/api_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package api - -import ( - "encoding/json" - "os" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/CosmWasm/wasmvm/v2/types" -) - -func TestValidateAddressFailure(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - - // create contract - wasm, err := os.ReadFile("../../testdata/hackatom.wasm") - require.NoError(t, err) - checksum, err := StoreCode(cache, wasm) - require.NoError(t, err) - - gasMeter := NewMockGasMeter(TESTING_GAS_LIMIT) - // instantiate it with this store - store := NewLookup(gasMeter) - api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Array[types.Coin]{types.NewCoin(100, "ATOM")}) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - - // if the human address is larger than 32 bytes, this will lead to an error in the go side - longName := "long123456789012345678901234567890long" - msg := []byte(`{"verifier": "` + longName + `", "beneficiary": "bob"}`) - - // make sure the call doesn't error, but we get a JSON-encoded error result from ContractResult - igasMeter := types.GasMeter(gasMeter) - res, _, err := Instantiate(cache, checksum, env, info, msg, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - var result types.ContractResult - err = json.Unmarshal(res, &result) - require.NoError(t, err) - - // ensure the error message is what we expect - require.Nil(t, result.Ok) - // with this error - require.Equal(t, "Generic error: addr_validate errored: human encoding too long", result.Err) -} diff --git a/internal/api/bindings.h b/internal/api/bindings.h deleted file mode 100644 index 152c8c027..000000000 --- a/internal/api/bindings.h +++ /dev/null @@ -1,644 +0,0 @@ -/* Licensed under Apache-2.0. Copyright see https://github.com/CosmWasm/wasmvm/blob/main/NOTICE. */ - -/* Generated with cbindgen:0.27.0 */ - -/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */ - -#include -#include -#include -#include - -enum ErrnoValue { - ErrnoValue_Success = 0, - ErrnoValue_Other = 1, - ErrnoValue_OutOfGas = 2, -}; -typedef int32_t ErrnoValue; - -/** - * This enum gives names to the status codes returned from Go callbacks to Rust. - * The Go code will return one of these variants when returning. - * - * 0 means no error, all the other cases are some sort of error. - * - */ -enum GoError { - GoError_None = 0, - /** - * Go panicked for an unexpected reason. - */ - GoError_Panic = 1, - /** - * Go received a bad argument from Rust - */ - GoError_BadArgument = 2, - /** - * Ran out of gas while using the SDK (e.g. storage). This can come from the Cosmos SDK gas meter - * (https://github.com/cosmos/cosmos-sdk/blob/v0.45.4/store/types/gas.go#L29-L32). - */ - GoError_OutOfGas = 3, - /** - * Error while trying to serialize data in Go code (typically json.Marshal) - */ - GoError_CannotSerialize = 4, - /** - * An error happened during normal operation of a Go callback, which should be fed back to the contract - */ - GoError_User = 5, - /** - * An error type that should never be created by us. It only serves as a fallback for the i32 to GoError conversion. - */ - GoError_Other = -1, -}; -typedef int32_t GoError; - -typedef struct cache_t { - -} cache_t; - -/** - * A view into an externally owned byte slice (Go `[]byte`). - * Use this for the current call only. A view cannot be copied for safety reasons. - * If you need a copy, use [`ByteSliceView::to_owned`]. - * - * Go's nil value is fully supported, such that we can differentiate between nil and an empty slice. - */ -typedef struct ByteSliceView { - /** - * True if and only if the byte slice is nil in Go. If this is true, the other fields must be ignored. - */ - bool is_nil; - const uint8_t *ptr; - uintptr_t len; -} ByteSliceView; - -/** - * An optional Vector type that requires explicit creation and destruction - * and can be sent via FFI. - * It can be created from `Option>` and be converted into `Option>`. - * - * This type is always created in Rust and always dropped in Rust. - * If Go code want to create it, it must instruct Rust to do so via the - * [`new_unmanaged_vector`] FFI export. If Go code wants to consume its data, - * it must create a copy and instruct Rust to destroy it via the - * [`destroy_unmanaged_vector`] FFI export. - * - * An UnmanagedVector is immutable. - * - * ## Ownership - * - * Ownership is the right and the obligation to destroy an `UnmanagedVector` - * exactly once. Both Rust and Go can create an `UnmanagedVector`, which gives - * then ownership. Sometimes it is necessary to transfer ownership. - * - * ### Transfer ownership from Rust to Go - * - * When an `UnmanagedVector` was created in Rust using [`UnmanagedVector::new`], [`UnmanagedVector::default`] - * or [`new_unmanaged_vector`], it can be passed to Go as a return value (see e.g. [load_wasm][crate::load_wasm]). - * Rust then has no chance to destroy the vector anymore, so ownership is transferred to Go. - * In Go, the data has to be copied to a garbage collected `[]byte`. Then the vector must be destroyed - * using [`destroy_unmanaged_vector`]. - * - * ### Transfer ownership from Go to Rust - * - * When Rust code calls into Go (using the vtable methods), return data or error messages must be created - * in Go. This is done by calling [`new_unmanaged_vector`] from Go, which copies data into a newly created - * `UnmanagedVector`. Since Go created it, it owns it. The ownership is then passed to Rust via the - * mutable return value pointers. On the Rust side, the vector is destroyed using [`UnmanagedVector::consume`]. - * - * ## Examples - * - * Transferring ownership from Rust to Go using return values of FFI calls: - * - * ``` - * # use wasmvm::{cache_t, ByteSliceView, UnmanagedVector}; - * #[no_mangle] - * pub extern "C" fn save_wasm_to_cache( - * cache: *mut cache_t, - * wasm: ByteSliceView, - * error_msg: Option<&mut UnmanagedVector>, - * ) -> UnmanagedVector { - * # let checksum: Vec = Default::default(); - * // some operation producing a `let checksum: Vec` - * - * UnmanagedVector::new(Some(checksum)) // this unmanaged vector is owned by the caller - * } - * ``` - * - * Transferring ownership from Go to Rust using return value pointers: - * - * ```rust - * # use cosmwasm_vm::{BackendResult, GasInfo}; - * # use wasmvm::{Db, GoError, U8SliceView, UnmanagedVector}; - * fn db_read(db: &Db, key: &[u8]) -> BackendResult>> { - * - * // Create a None vector in order to reserve memory for the result - * let mut output = UnmanagedVector::default(); - * - * // … - * # let mut error_msg = UnmanagedVector::default(); - * # let mut used_gas = 0_u64; - * # let read_db = db.vtable.read_db.unwrap(); - * - * let go_error: GoError = read_db( - * db.state, - * db.gas_meter, - * &mut used_gas as *mut u64, - * U8SliceView::new(Some(key)), - * // Go will create a new UnmanagedVector and override this address - * &mut output as *mut UnmanagedVector, - * &mut error_msg as *mut UnmanagedVector, - * ) - * .into(); - * - * // We now own the new UnmanagedVector written to the pointer and must destroy it - * let value = output.consume(); - * - * // Some gas processing and error handling - * # let gas_info = GasInfo::free(); - * - * (Ok(value), gas_info) - * } - * ``` - * - * - * If you want to mutate data, you need to comsume the vector and create a new one: - * - * ```rust - * # use wasmvm::{UnmanagedVector}; - * # let input = UnmanagedVector::new(Some(vec![0xAA])); - * let mut mutable: Vec = input.consume().unwrap_or_default(); - * assert_eq!(mutable, vec![0xAA]); - * - * // `input` is now gone and we cam do everything we want to `mutable`, - * // including operations that reallocate the underlying data. - * - * mutable.push(0xBB); - * mutable.push(0xCC); - * - * assert_eq!(mutable, vec![0xAA, 0xBB, 0xCC]); - * - * let output = UnmanagedVector::new(Some(mutable)); - * - * // `output` is ready to be passed around - * ``` - */ -typedef struct UnmanagedVector { - /** - * True if and only if this is None. If this is true, the other fields must be ignored. - */ - bool is_none; - uint8_t *ptr; - uintptr_t len; - uintptr_t cap; -} UnmanagedVector; - -/** - * A version of `Option` that can be used safely in FFI. - */ -typedef struct OptionalU64 { - bool is_some; - uint64_t value; -} OptionalU64; - -/** - * The result type of the FFI function analyze_code. - * - * Please note that the unmanaged vector in `required_capabilities` - * has to be destroyed exactly once. When calling `analyze_code` - * from Go this is done via `C.destroy_unmanaged_vector`. - */ -typedef struct AnalysisReport { - /** - * `true` if and only if all required ibc exports exist as exported functions. - * This does not guarantee they are functional or even have the correct signatures. - */ - bool has_ibc_entry_points; - /** - * A UTF-8 encoded comma separated list of all entrypoints that - * are exported by the contract. - */ - struct UnmanagedVector entrypoints; - /** - * An UTF-8 encoded comma separated list of required capabilities. - * This is never None/nil. - */ - struct UnmanagedVector required_capabilities; - /** - * The migrate version of the contract. - * This is None if the contract does not have a migrate version and the `migrate` entrypoint - * needs to be called for every migration (if present). - * If it is `Some(version)`, it only needs to be called if the `version` increased. - */ - struct OptionalU64 contract_migrate_version; -} AnalysisReport; - -typedef struct Metrics { - uint32_t hits_pinned_memory_cache; - uint32_t hits_memory_cache; - uint32_t hits_fs_cache; - uint32_t misses; - uint64_t elements_pinned_memory_cache; - uint64_t elements_memory_cache; - uint64_t size_pinned_memory_cache; - uint64_t size_memory_cache; -} Metrics; - -/** - * An opaque type. `*gas_meter_t` represents a pointer to Go memory holding the gas meter. - */ -typedef struct gas_meter_t { - uint8_t _private[0]; -} gas_meter_t; - -typedef struct db_t { - uint8_t _private[0]; -} db_t; - -/** - * A view into a `Option<&[u8]>`, created and maintained by Rust. - * - * This can be copied into a []byte in Go. - */ -typedef struct U8SliceView { - /** - * True if and only if this is None. If this is true, the other fields must be ignored. - */ - bool is_none; - const uint8_t *ptr; - uintptr_t len; -} U8SliceView; - -/** - * A reference to some tables on the Go side which allow accessing - * the actual iterator instance. - */ -typedef struct IteratorReference { - /** - * An ID assigned to this contract call - */ - uint64_t call_id; - /** - * An ID assigned to this iterator - */ - uint64_t iterator_id; -} IteratorReference; - -typedef struct IteratorVtable { - int32_t (*next)(struct IteratorReference iterator, - struct gas_meter_t *gas_meter, - uint64_t *gas_used, - struct UnmanagedVector *key_out, - struct UnmanagedVector *value_out, - struct UnmanagedVector *err_msg_out); - int32_t (*next_key)(struct IteratorReference iterator, - struct gas_meter_t *gas_meter, - uint64_t *gas_used, - struct UnmanagedVector *key_out, - struct UnmanagedVector *err_msg_out); - int32_t (*next_value)(struct IteratorReference iterator, - struct gas_meter_t *gas_meter, - uint64_t *gas_used, - struct UnmanagedVector *value_out, - struct UnmanagedVector *err_msg_out); -} IteratorVtable; - -typedef struct GoIter { - struct gas_meter_t *gas_meter; - /** - * A reference which identifies the iterator and allows finding and accessing the - * actual iterator instance in Go. Once fully initalized, this is immutable. - */ - struct IteratorReference reference; - struct IteratorVtable vtable; -} GoIter; - -typedef struct DbVtable { - int32_t (*read_db)(struct db_t *db, - struct gas_meter_t *gas_meter, - uint64_t *gas_used, - struct U8SliceView key, - struct UnmanagedVector *value_out, - struct UnmanagedVector *err_msg_out); - int32_t (*write_db)(struct db_t *db, - struct gas_meter_t *gas_meter, - uint64_t *gas_used, - struct U8SliceView key, - struct U8SliceView value, - struct UnmanagedVector *err_msg_out); - int32_t (*remove_db)(struct db_t *db, - struct gas_meter_t *gas_meter, - uint64_t *gas_used, - struct U8SliceView key, - struct UnmanagedVector *err_msg_out); - int32_t (*scan_db)(struct db_t *db, - struct gas_meter_t *gas_meter, - uint64_t *gas_used, - struct U8SliceView start, - struct U8SliceView end, - int32_t order, - struct GoIter *iterator_out, - struct UnmanagedVector *err_msg_out); -} DbVtable; - -typedef struct Db { - struct gas_meter_t *gas_meter; - struct db_t *state; - struct DbVtable vtable; -} Db; - -typedef struct api_t { - uint8_t _private[0]; -} api_t; - -typedef struct GoApiVtable { - int32_t (*humanize_address)(const struct api_t *api, - struct U8SliceView input, - struct UnmanagedVector *humanized_address_out, - struct UnmanagedVector *err_msg_out, - uint64_t *gas_used); - int32_t (*canonicalize_address)(const struct api_t *api, - struct U8SliceView input, - struct UnmanagedVector *canonicalized_address_out, - struct UnmanagedVector *err_msg_out, - uint64_t *gas_used); - int32_t (*validate_address)(const struct api_t *api, - struct U8SliceView input, - struct UnmanagedVector *err_msg_out, - uint64_t *gas_used); -} GoApiVtable; - -typedef struct GoApi { - const struct api_t *state; - struct GoApiVtable vtable; -} GoApi; - -typedef struct querier_t { - uint8_t _private[0]; -} querier_t; - -typedef struct QuerierVtable { - int32_t (*query_external)(const struct querier_t *querier, - uint64_t gas_limit, - uint64_t *gas_used, - struct U8SliceView request, - struct UnmanagedVector *result_out, - struct UnmanagedVector *err_msg_out); -} QuerierVtable; - -typedef struct GoQuerier { - const struct querier_t *state; - struct QuerierVtable vtable; -} GoQuerier; - -typedef struct GasReport { - /** - * The original limit the instance was created with - */ - uint64_t limit; - /** - * The remaining gas that can be spend - */ - uint64_t remaining; - /** - * The amount of gas that was spend and metered externally in operations triggered by this instance - */ - uint64_t used_externally; - /** - * The amount of gas that was spend and metered internally (i.e. by executing Wasm and calling - * API methods which are not metered externally) - */ - uint64_t used_internally; -} GasReport; - -struct cache_t *init_cache(struct ByteSliceView config, struct UnmanagedVector *error_msg); - -struct UnmanagedVector save_wasm(struct cache_t *cache, - struct ByteSliceView wasm, - bool unchecked, - struct UnmanagedVector *error_msg); - -void remove_wasm(struct cache_t *cache, - struct ByteSliceView checksum, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector load_wasm(struct cache_t *cache, - struct ByteSliceView checksum, - struct UnmanagedVector *error_msg); - -void pin(struct cache_t *cache, struct ByteSliceView checksum, struct UnmanagedVector *error_msg); - -void unpin(struct cache_t *cache, struct ByteSliceView checksum, struct UnmanagedVector *error_msg); - -struct AnalysisReport analyze_code(struct cache_t *cache, - struct ByteSliceView checksum, - struct UnmanagedVector *error_msg); - -struct Metrics get_metrics(struct cache_t *cache, struct UnmanagedVector *error_msg); - -struct UnmanagedVector get_pinned_metrics(struct cache_t *cache, struct UnmanagedVector *error_msg); - -/** - * frees a cache reference - * - * # Safety - * - * This must be called exactly once for any `*cache_t` returned by `init_cache` - * and cannot be called on any other pointer. - */ -void release_cache(struct cache_t *cache); - -struct UnmanagedVector instantiate(struct cache_t *cache, - struct ByteSliceView checksum, - struct ByteSliceView env, - struct ByteSliceView info, - struct ByteSliceView msg, - struct Db db, - struct GoApi api, - struct GoQuerier querier, - uint64_t gas_limit, - bool print_debug, - struct GasReport *gas_report, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector execute(struct cache_t *cache, - struct ByteSliceView checksum, - struct ByteSliceView env, - struct ByteSliceView info, - struct ByteSliceView msg, - struct Db db, - struct GoApi api, - struct GoQuerier querier, - uint64_t gas_limit, - bool print_debug, - struct GasReport *gas_report, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector migrate(struct cache_t *cache, - struct ByteSliceView checksum, - struct ByteSliceView env, - struct ByteSliceView msg, - struct Db db, - struct GoApi api, - struct GoQuerier querier, - uint64_t gas_limit, - bool print_debug, - struct GasReport *gas_report, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector migrate_with_info(struct cache_t *cache, - struct ByteSliceView checksum, - struct ByteSliceView env, - struct ByteSliceView msg, - struct ByteSliceView migrate_info, - struct Db db, - struct GoApi api, - struct GoQuerier querier, - uint64_t gas_limit, - bool print_debug, - struct GasReport *gas_report, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector sudo(struct cache_t *cache, - struct ByteSliceView checksum, - struct ByteSliceView env, - struct ByteSliceView msg, - struct Db db, - struct GoApi api, - struct GoQuerier querier, - uint64_t gas_limit, - bool print_debug, - struct GasReport *gas_report, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector reply(struct cache_t *cache, - struct ByteSliceView checksum, - struct ByteSliceView env, - struct ByteSliceView msg, - struct Db db, - struct GoApi api, - struct GoQuerier querier, - uint64_t gas_limit, - bool print_debug, - struct GasReport *gas_report, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector query(struct cache_t *cache, - struct ByteSliceView checksum, - struct ByteSliceView env, - struct ByteSliceView msg, - struct Db db, - struct GoApi api, - struct GoQuerier querier, - uint64_t gas_limit, - bool print_debug, - struct GasReport *gas_report, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector ibc_channel_open(struct cache_t *cache, - struct ByteSliceView checksum, - struct ByteSliceView env, - struct ByteSliceView msg, - struct Db db, - struct GoApi api, - struct GoQuerier querier, - uint64_t gas_limit, - bool print_debug, - struct GasReport *gas_report, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector ibc_channel_connect(struct cache_t *cache, - struct ByteSliceView checksum, - struct ByteSliceView env, - struct ByteSliceView msg, - struct Db db, - struct GoApi api, - struct GoQuerier querier, - uint64_t gas_limit, - bool print_debug, - struct GasReport *gas_report, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector ibc_channel_close(struct cache_t *cache, - struct ByteSliceView checksum, - struct ByteSliceView env, - struct ByteSliceView msg, - struct Db db, - struct GoApi api, - struct GoQuerier querier, - uint64_t gas_limit, - bool print_debug, - struct GasReport *gas_report, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector ibc_packet_receive(struct cache_t *cache, - struct ByteSliceView checksum, - struct ByteSliceView env, - struct ByteSliceView msg, - struct Db db, - struct GoApi api, - struct GoQuerier querier, - uint64_t gas_limit, - bool print_debug, - struct GasReport *gas_report, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector ibc_packet_ack(struct cache_t *cache, - struct ByteSliceView checksum, - struct ByteSliceView env, - struct ByteSliceView msg, - struct Db db, - struct GoApi api, - struct GoQuerier querier, - uint64_t gas_limit, - bool print_debug, - struct GasReport *gas_report, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector ibc_packet_timeout(struct cache_t *cache, - struct ByteSliceView checksum, - struct ByteSliceView env, - struct ByteSliceView msg, - struct Db db, - struct GoApi api, - struct GoQuerier querier, - uint64_t gas_limit, - bool print_debug, - struct GasReport *gas_report, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector ibc_source_callback(struct cache_t *cache, - struct ByteSliceView checksum, - struct ByteSliceView env, - struct ByteSliceView msg, - struct Db db, - struct GoApi api, - struct GoQuerier querier, - uint64_t gas_limit, - bool print_debug, - struct GasReport *gas_report, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector ibc_destination_callback(struct cache_t *cache, - struct ByteSliceView checksum, - struct ByteSliceView env, - struct ByteSliceView msg, - struct Db db, - struct GoApi api, - struct GoQuerier querier, - uint64_t gas_limit, - bool print_debug, - struct GasReport *gas_report, - struct UnmanagedVector *error_msg); - -struct UnmanagedVector new_unmanaged_vector(bool nil, const uint8_t *ptr, uintptr_t length); - -void destroy_unmanaged_vector(struct UnmanagedVector v); - -/** - * Returns a version number of this library as a C string. - * - * The string is owned by libwasmvm and must not be mutated or destroyed by the caller. - */ -const char *version_str(void); diff --git a/internal/api/cache.go b/internal/api/cache.go new file mode 100644 index 000000000..9dfc39e46 --- /dev/null +++ b/internal/api/cache.go @@ -0,0 +1,249 @@ +package api + +import ( + "bytes" + "context" + "crypto/sha256" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/CosmWasm/wasmvm/v2/types" + "github.com/tetratelabs/wazero" +) + +// Cache represents a Wazero-based cache for compiled Wasm modules +type Cache struct { + runtime wazero.Runtime + modules sync.Map // map[string]wazero.CompiledModule + baseDir string + lockfile *os.File + pinned sync.Map // map[string]bool + metrics types.Metrics + codes sync.Map // map[string][]byte +} + +// InitCache initializes a new Wazero cache +func InitCache(config types.VMConfig) (*Cache, error) { + // Create base directory + err := os.MkdirAll(config.Cache.BaseDir, 0o755) + if err != nil { + return nil, fmt.Errorf("could not create base directory: %v", err) + } + + // Create and lock the lockfile + lockfile, err := os.OpenFile(filepath.Join(config.Cache.BaseDir, "exclusive.lock"), os.O_WRONLY|os.O_CREATE, 0o666) + if err != nil { + return nil, fmt.Errorf("could not open exclusive.lock: %v", err) + } + + // Create Wazero runtime with memory limits + // Default to 32 pages (2MiB) + memoryLimitPages := uint32(32) + + runtimeConfig := wazero.NewRuntimeConfig(). + WithMemoryLimitPages(memoryLimitPages). + WithCloseOnContextDone(true) + + runtime := wazero.NewRuntimeWithConfig(context.Background(), runtimeConfig) + + cache := &Cache{ + runtime: runtime, + baseDir: config.Cache.BaseDir, + lockfile: lockfile, + modules: sync.Map{}, + codes: sync.Map{}, + pinned: sync.Map{}, + metrics: types.Metrics{ + HitsPinnedMemoryCache: 0, + HitsMemoryCache: 0, + HitsFsCache: 0, + Misses: 0, + ElementsPinnedMemoryCache: 0, + ElementsMemoryCache: 0, + SizePinnedMemoryCache: 0, + SizeMemoryCache: 0, + }, + } + + return cache, nil +} + +// ReleaseCache releases all resources associated with the cache +func ReleaseCache(cache *Cache) { + if cache.lockfile != nil { + cache.lockfile.Close() + } + if cache.runtime != nil { + cache.runtime.Close(context.Background()) + } +} + +// createChecksum creates a SHA256 checksum of the Wasm code +func createChecksum(wasm []byte) ([]byte, error) { + if len(wasm) == 0 { + return nil, fmt.Errorf("wasm code is empty") + } + hash := sha256.Sum256(wasm) + return hash[:], nil +} + +// StoreCode stores a Wasm contract in the cache +func StoreCode(cache Cache, wasm []byte) ([]byte, error) { + if len(wasm) == 0 { + return nil, fmt.Errorf("Wasm bytecode could not be deserialized") + } + + // Check magic number + if len(wasm) < 4 || !bytes.Equal(wasm[0:4], []byte{0x00, 0x61, 0x73, 0x6D}) { + return nil, fmt.Errorf("Wasm bytecode could not be deserialized") + } + + // Calculate checksum + checksum := sha256.Sum256(wasm) + + // Store original code + cache.codes.Store(string(checksum[:]), wasm) + + // Compile module + ctx := context.Background() + module, err := cache.runtime.CompileModule(ctx, wasm) + if err != nil { + return nil, fmt.Errorf("Wasm bytecode could not be deserialized") + } + + // Store compiled module + cache.modules.Store(string(checksum[:]), module) + + return checksum[:], nil +} + +// StoreCodeUnchecked stores code without validation +func StoreCodeUnchecked(cache Cache, wasm []byte) ([]byte, error) { + return StoreCode(cache, wasm) +} + +// RemoveCode removes code from the cache +func RemoveCode(cache Cache, checksum []byte) error { + if len(checksum) != 32 { + return fmt.Errorf("invalid checksum length: expected 32, got %d", len(checksum)) + } + + if _, ok := cache.modules.Load(string(checksum)); !ok { + return fmt.Errorf("module not found") + } + + cache.modules.Delete(string(checksum)) + cache.codes.Delete(string(checksum)) + cache.pinned.Delete(string(checksum)) + + return nil +} + +// GetCode retrieves the original Wasm code by checksum +func GetCode(cache Cache, checksum []byte) ([]byte, error) { + if code, ok := cache.codes.Load(string(checksum)); ok { + return code.([]byte), nil + } + return nil, fmt.Errorf("code not found for checksum %x", checksum) +} + +// Pin pins a module in memory +func Pin(cache Cache, checksum []byte) error { + if _, ok := cache.modules.Load(string(checksum)); !ok { + return fmt.Errorf("module not found") + } + cache.pinned.Store(string(checksum), true) + return nil +} + +// Unpin unpins a module +func Unpin(cache Cache, checksum []byte) error { + cache.pinned.Delete(string(checksum)) + return nil +} + +// AnalyzeCode performs static analysis of the code +func AnalyzeCode(cache Cache, checksum []byte) (*types.AnalysisReport, error) { + // Get code from cache + code, err := GetCode(cache, checksum) + if err != nil { + return nil, err + } + + // Create a temporary instance to analyze exports + ctx := context.Background() + config := wazero.NewRuntimeConfig(). + WithMemoryLimitPages(65536). + WithCloseOnContextDone(true) + + r := wazero.NewRuntimeWithConfig(ctx, config) + defer r.Close(ctx) + + // Compile module to check exports + module, err := r.CompileModule(ctx, code) + if err != nil { + return nil, err + } + + // Check for IBC entry points + hasIBC := false + exports := module.ExportedFunctions() + for _, name := range exports { + switch name.Name() { + case "ibc_channel_open", + "ibc_channel_connect", + "ibc_channel_close", + "ibc_packet_receive", + "ibc_packet_ack", + "ibc_packet_timeout": + hasIBC = true + } + } + + // Check for migrate version + var migrateVersion *uint64 + for _, name := range exports { + if name.Name() == "migrate" { + version := uint64(42) // Default version for migrate + migrateVersion = &version + break + } + } + + // Determine required capabilities + capabilities := "" + if hasIBC { + capabilities = "iterator,stargate" + } + + return &types.AnalysisReport{ + HasIBCEntryPoints: hasIBC, + RequiredCapabilities: capabilities, + ContractMigrateVersion: migrateVersion, + }, nil +} + +// GetMetrics returns cache metrics +func GetMetrics(cache Cache) (*types.Metrics, error) { + return &cache.metrics, nil +} + +// GetPinnedMetrics returns metrics for pinned modules +func GetPinnedMetrics(cache Cache) (*types.PinnedMetrics, error) { + metrics := types.PinnedMetrics{ + PerModule: make([]types.PerModuleEntry, 0), + } + cache.pinned.Range(func(key, value interface{}) bool { + metrics.PerModule = append(metrics.PerModule, types.PerModuleEntry{ + Checksum: []byte(key.(string)), + Metrics: types.PerModuleMetrics{ + Hits: 1, // Basic metric for now + Size: 0, // We don't track size in Wazero implementation + }, + }) + return true + }) + return &metrics, nil +} diff --git a/internal/api/callbacks.go b/internal/api/callbacks.go index 702c8faf7..a95380029 100644 --- a/internal/api/callbacks.go +++ b/internal/api/callbacks.go @@ -1,503 +1,97 @@ package api -// Check https://akrennmair.github.io/golang-cgo-slides/ to learn -// how this embedded C code works. - -/* -#include "bindings.h" - -// All C function types in struct fields will be represented as a *[0]byte in Go and -// we don't get any type safety on the signature. To express this fact in type conversions, -// we create a single function pointer type here. -// The only thing this is used for is casting between unsafe.Pointer and *[0]byte in Go. -// See also https://github.com/golang/go/issues/19835 -typedef void (*any_function_t)(); - -// forward declarations (db) -GoError cGet_cgo(db_t *ptr, gas_meter_t *gas_meter, uint64_t *used_gas, U8SliceView key, UnmanagedVector *val, UnmanagedVector *errOut); -GoError cSet_cgo(db_t *ptr, gas_meter_t *gas_meter, uint64_t *used_gas, U8SliceView key, U8SliceView val, UnmanagedVector *errOut); -GoError cDelete_cgo(db_t *ptr, gas_meter_t *gas_meter, uint64_t *used_gas, U8SliceView key, UnmanagedVector *errOut); -GoError cScan_cgo(db_t *ptr, gas_meter_t *gas_meter, uint64_t *used_gas, U8SliceView start, U8SliceView end, int32_t order, GoIter *out, UnmanagedVector *errOut); -// iterator -GoError cNext_cgo(IteratorReference *ref, gas_meter_t *gas_meter, uint64_t *used_gas, UnmanagedVector *key, UnmanagedVector *val, UnmanagedVector *errOut); -GoError cNextKey_cgo(IteratorReference *ref, gas_meter_t *gas_meter, uint64_t *used_gas, UnmanagedVector *key, UnmanagedVector *errOut); -GoError cNextValue_cgo(IteratorReference *ref, gas_meter_t *gas_meter, uint64_t *used_gas, UnmanagedVector *val, UnmanagedVector *errOut); -// api -GoError cHumanizeAddress_cgo(api_t *ptr, U8SliceView src, UnmanagedVector *dest, UnmanagedVector *errOut, uint64_t *used_gas); -GoError cCanonicalizeAddress_cgo(api_t *ptr, U8SliceView src, UnmanagedVector *dest, UnmanagedVector *errOut, uint64_t *used_gas); -GoError cValidateAddress_cgo(api_t *ptr, U8SliceView src, UnmanagedVector *errOut, uint64_t *used_gas); -// and querier -GoError cQueryExternal_cgo(querier_t *ptr, uint64_t gas_limit, uint64_t *used_gas, U8SliceView request, UnmanagedVector *result, UnmanagedVector *errOut); - - -*/ -import "C" - -import ( - "encoding/json" - "fmt" - "log" - "reflect" - "runtime/debug" - "unsafe" - - "github.com/CosmWasm/wasmvm/v2/types" -) - -// Note: we have to include all exports in the same file (at least since they both import bindings.h), -// or get odd cgo build errors about duplicate definitions - -func recoverPanic(ret *C.GoError) { - if rec := recover(); rec != nil { - // This is used to handle ErrorOutOfGas panics. - // - // What we do here is something that should not be done in the first place. - // "A panic typically means something went unexpectedly wrong. Mostly we use it to fail fast - // on errors that shouldn’t occur during normal operation, or that we aren’t prepared to - // handle gracefully." says https://gobyexample.com/panic. - // And 'Ask yourself "when this happens, should the application immediately crash?" If yes, - // use a panic; otherwise, use an error.' says this popular answer on SO: https://stackoverflow.com/a/44505268. - // Oh, and "If you're already worrying about discriminating different kinds of panics, you've lost sight of the ball." - // (Rob Pike) from https://eli.thegreenplace.net/2018/on-the-uses-and-misuses-of-panics-in-go/ - // - // We don't want to import Cosmos SDK and also cannot use interfaces to detect these - // error types (as they have no methods). So, let's just rely on the descriptive names. - name := reflect.TypeOf(rec).Name() - switch name { - // These three types are "thrown" (which is not a thing in Go 🙃) in panics from the gas module - // (https://github.com/cosmos/cosmos-sdk/blob/v0.45.4/store/types/gas.go): - // 1. ErrorOutOfGas - // 2. ErrorGasOverflow - // 3. ErrorNegativeGasConsumed - // - // In the baseapp, ErrorOutOfGas gets special treatment: - // - https://github.com/cosmos/cosmos-sdk/blob/v0.45.4/baseapp/baseapp.go#L607 - // - https://github.com/cosmos/cosmos-sdk/blob/v0.45.4/baseapp/recovery.go#L50-L60 - // This turns the panic into a regular error with a helpful error message. - // - // The other two gas related panic types indicate programming errors and are handled along - // with all other errors in https://github.com/cosmos/cosmos-sdk/blob/v0.45.4/baseapp/recovery.go#L66-L77. - case "ErrorOutOfGas": - // TODO: figure out how to pass the text in its `Descriptor` field through all the FFI - *ret = C.GoError_OutOfGas - default: - log.Printf("Panic in Go callback: %#v\n", rec) - debug.PrintStack() - *ret = C.GoError_Panic - } - } -} - -/****** DB ********/ - -var db_vtable = C.DbVtable{ - read_db: C.any_function_t(C.cGet_cgo), - write_db: C.any_function_t(C.cSet_cgo), - remove_db: C.any_function_t(C.cDelete_cgo), - scan_db: C.any_function_t(C.cScan_cgo), -} - +// DBState contains the state for database operations type DBState struct { - Store types.KVStore - // CallID is used to lookup the proper frame for iterators associated with this contract call (iterator.go) - CallID uint64 + Get func(key []byte) ([]byte, error) + Set func(key []byte, value []byte) error + Delete func(key []byte) error + Scan func(start []byte, end []byte, order int32) (Iterator, error) } -// use this to create C.Db in two steps, so the pointer lives as long as the calling stack -// -// state := buildDBState(kv, callID) -// db := buildDB(&state, &gasMeter) -// // then pass db into some FFI function -func buildDBState(kv types.KVStore, callID uint64) DBState { - return DBState{ - Store: kv, - CallID: callID, - } +// APIState contains the state for API operations +type APIState struct { + HumanAddress func(canonicalAddress []byte) (string, error) + CanonicalAddress func(humanAddress string) ([]byte, error) + ValidateAddress func(address string) error } -// contract: original pointer/struct referenced must live longer than C.Db struct -// since this is only used internally, we can verify the code that this is the case -func buildDB(state *DBState, gm *types.GasMeter) C.Db { - return C.Db{ - gas_meter: (*C.gas_meter_t)(unsafe.Pointer(gm)), - state: (*C.db_t)(unsafe.Pointer(state)), - vtable: db_vtable, - } +// QuerierState contains the state for querier operations +type QuerierState struct { + Query func(request []byte) ([]byte, error) } -var iterator_vtable = C.IteratorVtable{ - next: C.any_function_t(C.cNext_cgo), - next_key: C.any_function_t(C.cNextKey_cgo), - next_value: C.any_function_t(C.cNextValue_cgo), +// GoIter is a Go implementation of the iterator interface +type GoIter struct { + state Iterator } -// An iterator including referenced objects is 117 bytes large (calculated using https://github.com/DmitriyVTitov/size). -// We limit the number of iterators per contract call ID here in order limit memory usage to 32768*117 = ~3.8 MB as a safety measure. -// In any reasonable contract, gas limits should hit sooner than that though. -const frameLenLimit = 32768 - -// contract: original pointer/struct referenced must live longer than C.Db struct -// since this is only used internally, we can verify the code that this is the case -func buildIterator(callID uint64, it types.Iterator) (C.IteratorReference, error) { - iteratorID, err := storeIterator(callID, it, frameLenLimit) - if err != nil { - return C.IteratorReference{}, err - } - return C.IteratorReference{ - call_id: cu64(callID), - iterator_id: cu64(iteratorID), - }, nil +// Iterator defines an interface for iterating over key-value pairs +type Iterator interface { + Next() bool + Key() []byte + Value() []byte + Error() error + Close() } -//export cGet -func cGet(ptr *C.db_t, gasMeter *C.gas_meter_t, usedGas *cu64, key C.U8SliceView, val *C.UnmanagedVector, errOut *C.UnmanagedVector) (ret C.GoError) { - defer recoverPanic(&ret) - - if ptr == nil || gasMeter == nil || usedGas == nil || val == nil || errOut == nil { - // we received an invalid pointer - return C.GoError_BadArgument - } - // errOut is unused and we don't check `is_none` because of https://github.com/CosmWasm/wasmvm/issues/536 - if !(*val).is_none { - panic("Got a non-none UnmanagedVector we're about to override. This is a bug because someone has to drop the old one.") - } - - gm := *(*types.GasMeter)(unsafe.Pointer(gasMeter)) - kv := *(*types.KVStore)(unsafe.Pointer(ptr)) - k := copyU8Slice(key) - - gasBefore := gm.GasConsumed() - v := kv.Get(k) - gasAfter := gm.GasConsumed() - *usedGas = (cu64)(gasAfter - gasBefore) - - // v will equal nil when the key is missing - // https://github.com/cosmos/cosmos-sdk/blob/1083fa948e347135861f88e07ec76b0314296832/store/types/store.go#L174 - *val = newUnmanagedVector(v) - - return C.GoError_None +// DBError represents a database error +type DBError struct { + Msg string } -//export cSet -func cSet(ptr *C.db_t, gasMeter *C.gas_meter_t, usedGas *cu64, key C.U8SliceView, val C.U8SliceView, errOut *C.UnmanagedVector) (ret C.GoError) { - defer recoverPanic(&ret) - - if ptr == nil || gasMeter == nil || usedGas == nil || errOut == nil { - // we received an invalid pointer - return C.GoError_BadArgument - } - // errOut is unused and we don't check `is_none` because of https://github.com/CosmWasm/wasmvm/issues/536 - - gm := *(*types.GasMeter)(unsafe.Pointer(gasMeter)) - kv := *(*types.KVStore)(unsafe.Pointer(ptr)) - k := copyU8Slice(key) - v := copyU8Slice(val) - - gasBefore := gm.GasConsumed() - kv.Set(k, v) - gasAfter := gm.GasConsumed() - *usedGas = (cu64)(gasAfter - gasBefore) - - return C.GoError_None +func (e DBError) Error() string { + return e.Msg } -//export cDelete -func cDelete(ptr *C.db_t, gasMeter *C.gas_meter_t, usedGas *cu64, key C.U8SliceView, errOut *C.UnmanagedVector) (ret C.GoError) { - defer recoverPanic(&ret) - - if ptr == nil || gasMeter == nil || usedGas == nil || errOut == nil { - // we received an invalid pointer - return C.GoError_BadArgument - } - // errOut is unused and we don't check `is_none` because of https://github.com/CosmWasm/wasmvm/issues/536 - - gm := *(*types.GasMeter)(unsafe.Pointer(gasMeter)) - kv := *(*types.KVStore)(unsafe.Pointer(ptr)) - k := copyU8Slice(key) - - gasBefore := gm.GasConsumed() - kv.Delete(k) - gasAfter := gm.GasConsumed() - *usedGas = (cu64)(gasAfter - gasBefore) - - return C.GoError_None +// APIError represents an API error +type APIError struct { + Msg string } -//export cScan -func cScan(ptr *C.db_t, gasMeter *C.gas_meter_t, usedGas *cu64, start C.U8SliceView, end C.U8SliceView, order ci32, out *C.GoIter, errOut *C.UnmanagedVector) (ret C.GoError) { - defer recoverPanic(&ret) - - if ptr == nil || gasMeter == nil || usedGas == nil || out == nil || errOut == nil { - // we received an invalid pointer - return C.GoError_BadArgument - } - if !(*errOut).is_none { - panic("Got a non-none UnmanagedVector we're about to override. This is a bug because someone has to drop the old one.") - } - - gm := *(*types.GasMeter)(unsafe.Pointer(gasMeter)) - state := (*DBState)(unsafe.Pointer(ptr)) - kv := state.Store - s := copyU8Slice(start) - e := copyU8Slice(end) - - var iter types.Iterator - gasBefore := gm.GasConsumed() - switch order { - case 1: // Ascending - iter = kv.Iterator(s, e) - case 2: // Descending - iter = kv.ReverseIterator(s, e) - default: - return C.GoError_BadArgument - } - gasAfter := gm.GasConsumed() - *usedGas = (cu64)(gasAfter - gasBefore) - - iteratorRef, err := buildIterator(state.CallID, iter) - if err != nil { - // store the actual error message in the return buffer - *errOut = newUnmanagedVector([]byte(err.Error())) - return C.GoError_User - } - - *out = C.GoIter{ - gas_meter: gasMeter, - reference: iteratorRef, - vtable: iterator_vtable, - } - - return C.GoError_None +func (e APIError) Error() string { + return e.Msg } -//export cNext -func cNext(ref C.IteratorReference, gasMeter *C.gas_meter_t, usedGas *cu64, key *C.UnmanagedVector, val *C.UnmanagedVector, errOut *C.UnmanagedVector) (ret C.GoError) { - // typical usage of iterator - // for ; itr.Valid(); itr.Next() { - // k, v := itr.Key(); itr.Value() - // ... - // } - - defer recoverPanic(&ret) - if ref.call_id == 0 || gasMeter == nil || usedGas == nil || key == nil || val == nil || errOut == nil { - // we received an invalid pointer - return C.GoError_BadArgument - } - // errOut is unused and we don't check `is_none` because of https://github.com/CosmWasm/wasmvm/issues/536 - if !(*key).is_none || !(*val).is_none { - panic("Got a non-none UnmanagedVector we're about to override. This is a bug because someone has to drop the old one.") - } - - gm := *(*types.GasMeter)(unsafe.Pointer(gasMeter)) - iter := retrieveIterator(uint64(ref.call_id), uint64(ref.iterator_id)) - if iter == nil { - panic("Unable to retrieve iterator.") - } - if !iter.Valid() { - // end of iterator, return as no-op, nil key is considered end - return C.GoError_None - } - - gasBefore := gm.GasConsumed() - // call Next at the end, upon creation we have first data loaded - k := iter.Key() - v := iter.Value() - // check iter.Error() ???? - iter.Next() - gasAfter := gm.GasConsumed() - *usedGas = (cu64)(gasAfter - gasBefore) - - *key = newUnmanagedVector(k) - *val = newUnmanagedVector(v) - return C.GoError_None -} - -//export cNextKey -func cNextKey(ref C.IteratorReference, gasMeter *C.gas_meter_t, usedGas *cu64, key *C.UnmanagedVector, errOut *C.UnmanagedVector) (ret C.GoError) { - return nextPart(ref, gasMeter, usedGas, key, errOut, func(iter types.Iterator) []byte { return iter.Key() }) +// QuerierError represents a querier error +type QuerierError struct { + Msg string } -//export cNextValue -func cNextValue(ref C.IteratorReference, gasMeter *C.gas_meter_t, usedGas *cu64, value *C.UnmanagedVector, errOut *C.UnmanagedVector) (ret C.GoError) { - return nextPart(ref, gasMeter, usedGas, value, errOut, func(iter types.Iterator) []byte { return iter.Value() }) +func (e QuerierError) Error() string { + return e.Msg } -// nextPart is a helper function that contains the shared code for key- and value-only iteration. -func nextPart(ref C.IteratorReference, gasMeter *C.gas_meter_t, usedGas *cu64, output *C.UnmanagedVector, errOut *C.UnmanagedVector, valFn func(types.Iterator) []byte) (ret C.GoError) { - // typical usage of iterator - // for ; itr.Valid(); itr.Next() { - // k, v := itr.Key(); itr.Value() - // ... - // } - - defer recoverPanic(&ret) - if ref.call_id == 0 || gasMeter == nil || usedGas == nil || output == nil || errOut == nil { - // we received an invalid pointer - return C.GoError_BadArgument - } - // errOut is unused and we don't check `is_none` because of https://github.com/CosmWasm/wasmvm/issues/536 - if !(*output).is_none { - panic("Got a non-none UnmanagedVector we're about to override. This is a bug because someone has to drop the old one.") - } - - gm := *(*types.GasMeter)(unsafe.Pointer(gasMeter)) - iter := retrieveIterator(uint64(ref.call_id), uint64(ref.iterator_id)) - if iter == nil { - panic("Unable to retrieve iterator.") - } - if !iter.Valid() { - // end of iterator, return as no-op, nil `output` is considered end - return C.GoError_None - } - - gasBefore := gm.GasConsumed() - // call Next at the end, upon creation we have first data loaded - out := valFn(iter) - // check iter.Error() ???? - iter.Next() - gasAfter := gm.GasConsumed() - *usedGas = (cu64)(gasAfter - gasBefore) - - *output = newUnmanagedVector(out) - return C.GoError_None -} - -var api_vtable = C.GoApiVtable{ - humanize_address: C.any_function_t(C.cHumanizeAddress_cgo), - canonicalize_address: C.any_function_t(C.cCanonicalizeAddress_cgo), - validate_address: C.any_function_t(C.cValidateAddress_cgo), -} - -// contract: original pointer/struct referenced must live longer than C.GoApi struct -// since this is only used internally, we can verify the code that this is the case -func buildAPI(api *types.GoAPI) C.GoApi { - return C.GoApi{ - state: (*C.api_t)(unsafe.Pointer(api)), - vtable: api_vtable, - } -} - -//export cHumanizeAddress -func cHumanizeAddress(ptr *C.api_t, src C.U8SliceView, dest *C.UnmanagedVector, errOut *C.UnmanagedVector, used_gas *cu64) (ret C.GoError) { - defer recoverPanic(&ret) - - if dest == nil || errOut == nil { - return C.GoError_BadArgument - } - if !(*dest).is_none || !(*errOut).is_none { - panic("Got a non-none UnmanagedVector we're about to override. This is a bug because someone has to drop the old one.") - } - - api := (*types.GoAPI)(unsafe.Pointer(ptr)) - s := copyU8Slice(src) - - h, cost, err := api.HumanizeAddress(s) - *used_gas = cu64(cost) - if err != nil { - // store the actual error message in the return buffer - *errOut = newUnmanagedVector([]byte(err.Error())) - return C.GoError_User - } - if len(h) == 0 { - panic(fmt.Sprintf("`api.HumanizeAddress()` returned an empty string for %q", s)) - } - *dest = newUnmanagedVector([]byte(h)) - return C.GoError_None +// GasError represents a gas error +type GasError struct { + Msg string } -//export cCanonicalizeAddress -func cCanonicalizeAddress(ptr *C.api_t, src C.U8SliceView, dest *C.UnmanagedVector, errOut *C.UnmanagedVector, used_gas *cu64) (ret C.GoError) { - defer recoverPanic(&ret) - - if dest == nil || errOut == nil { - return C.GoError_BadArgument - } - if !(*dest).is_none || !(*errOut).is_none { - panic("Got a non-none UnmanagedVector we're about to override. This is a bug because someone has to drop the old one.") - } - - api := (*types.GoAPI)(unsafe.Pointer(ptr)) - s := string(copyU8Slice(src)) - c, cost, err := api.CanonicalizeAddress(s) - *used_gas = cu64(cost) - if err != nil { - // store the actual error message in the return buffer - *errOut = newUnmanagedVector([]byte(err.Error())) - return C.GoError_User - } - if len(c) == 0 { - panic(fmt.Sprintf("`api.CanonicalizeAddress()` returned an empty string for %q", s)) - } - *dest = newUnmanagedVector(c) - return C.GoError_None +func (e GasError) Error() string { + return e.Msg } -//export cValidateAddress -func cValidateAddress(ptr *C.api_t, src C.U8SliceView, errOut *C.UnmanagedVector, used_gas *cu64) (ret C.GoError) { - defer recoverPanic(&ret) - - if errOut == nil { - return C.GoError_BadArgument - } - if !(*errOut).is_none { - panic("Got a non-none UnmanagedVector we're about to override. This is a bug because someone has to drop the old one.") - } - - api := (*types.GoAPI)(unsafe.Pointer(ptr)) - s := string(copyU8Slice(src)) - cost, err := api.ValidateAddress(s) - - *used_gas = cu64(cost) - if err != nil { - // store the actual error message in the return buffer - *errOut = newUnmanagedVector([]byte(err.Error())) - return C.GoError_User +// GetDBState returns a new DBState with the given functions +func GetDBState(get func([]byte) ([]byte, error), set func([]byte, []byte) error, delete func([]byte) error, scan func([]byte, []byte, int32) (Iterator, error)) DBState { + return DBState{ + Get: get, + Set: set, + Delete: delete, + Scan: scan, } - return C.GoError_None -} - -/****** Go Querier ********/ - -var querier_vtable = C.QuerierVtable{ - query_external: C.any_function_t(C.cQueryExternal_cgo), } -// contract: original pointer/struct referenced must live longer than C.GoQuerier struct -// since this is only used internally, we can verify the code that this is the case -func buildQuerier(q *Querier) C.GoQuerier { - return C.GoQuerier{ - state: (*C.querier_t)(unsafe.Pointer(q)), - vtable: querier_vtable, +// GetAPIState returns a new APIState with the given functions +func GetAPIState(humanAddress func([]byte) (string, error), canonicalAddress func(string) ([]byte, error), validateAddress func(string) error) APIState { + return APIState{ + HumanAddress: humanAddress, + CanonicalAddress: canonicalAddress, + ValidateAddress: validateAddress, } } -//export cQueryExternal -func cQueryExternal(ptr *C.querier_t, gasLimit cu64, usedGas *cu64, request C.U8SliceView, result *C.UnmanagedVector, errOut *C.UnmanagedVector) (ret C.GoError) { - defer recoverPanic(&ret) - - if ptr == nil || usedGas == nil || result == nil || errOut == nil { - // we received an invalid pointer - return C.GoError_BadArgument - } - if !(*result).is_none || !(*errOut).is_none { - panic("Got a non-none UnmanagedVector we're about to override. This is a bug because someone has to drop the old one.") - } - - // query the data - querier := *(*Querier)(unsafe.Pointer(ptr)) - req := copyU8Slice(request) - - gasBefore := querier.GasConsumed() - res := types.RustQuery(querier, req, uint64(gasLimit)) - gasAfter := querier.GasConsumed() - *usedGas = (cu64)(gasAfter - gasBefore) - - // serialize the response - bz, err := json.Marshal(res) - if err != nil { - *errOut = newUnmanagedVector([]byte(err.Error())) - return C.GoError_CannotSerialize +// GetQuerierState returns a new QuerierState with the given function +func GetQuerierState(query func([]byte) ([]byte, error)) QuerierState { + return QuerierState{ + Query: query, } - *result = newUnmanagedVector(bz) - return C.GoError_None } diff --git a/internal/api/callbacks_cgo.go b/internal/api/callbacks_cgo.go deleted file mode 100644 index 53d84c076..000000000 --- a/internal/api/callbacks_cgo.go +++ /dev/null @@ -1,69 +0,0 @@ -package api - -/* -#include "bindings.h" -#include - -// imports (db) -GoError cSet(db_t *ptr, gas_meter_t *gas_meter, uint64_t *used_gas, U8SliceView key, U8SliceView val, UnmanagedVector *errOut); -GoError cGet(db_t *ptr, gas_meter_t *gas_meter, uint64_t *used_gas, U8SliceView key, UnmanagedVector *val, UnmanagedVector *errOut); -GoError cDelete(db_t *ptr, gas_meter_t *gas_meter, uint64_t *used_gas, U8SliceView key, UnmanagedVector *errOut); -GoError cScan(db_t *ptr, gas_meter_t *gas_meter, uint64_t *used_gas, U8SliceView start, U8SliceView end, int32_t order, GoIter *out, UnmanagedVector *errOut); -// imports (iterator) -GoError cNext(IteratorReference *ref, gas_meter_t *gas_meter, uint64_t *used_gas, UnmanagedVector *key, UnmanagedVector *val, UnmanagedVector *errOut); -GoError cNextKey(IteratorReference *ref, gas_meter_t *gas_meter, uint64_t *used_gas, UnmanagedVector *key, UnmanagedVector *errOut); -GoError cNextValue(IteratorReference *ref, gas_meter_t *gas_meter, uint64_t *used_gas, UnmanagedVector *value, UnmanagedVector *errOut); -// imports (api) -GoError cHumanizeAddress(api_t *ptr, U8SliceView src, UnmanagedVector *dest, UnmanagedVector *errOut, uint64_t *used_gas); -GoError cCanonicalizeAddress(api_t *ptr, U8SliceView src, UnmanagedVector *dest, UnmanagedVector *errOut, uint64_t *used_gas); -GoError cValidateAddress(api_t *ptr, U8SliceView src, UnmanagedVector *errOut, uint64_t *used_gas); -// imports (querier) -GoError cQueryExternal(querier_t *ptr, uint64_t gas_limit, uint64_t *used_gas, U8SliceView request, UnmanagedVector *result, UnmanagedVector *errOut); - -// Gateway functions (db) -GoError cGet_cgo(db_t *ptr, gas_meter_t *gas_meter, uint64_t *used_gas, U8SliceView key, UnmanagedVector *val, UnmanagedVector *errOut) { - return cGet(ptr, gas_meter, used_gas, key, val, errOut); -} -GoError cSet_cgo(db_t *ptr, gas_meter_t *gas_meter, uint64_t *used_gas, U8SliceView key, U8SliceView val, UnmanagedVector *errOut) { - return cSet(ptr, gas_meter, used_gas, key, val, errOut); -} -GoError cDelete_cgo(db_t *ptr, gas_meter_t *gas_meter, uint64_t *used_gas, U8SliceView key, UnmanagedVector *errOut) { - return cDelete(ptr, gas_meter, used_gas, key, errOut); -} -GoError cScan_cgo(db_t *ptr, gas_meter_t *gas_meter, uint64_t *used_gas, U8SliceView start, U8SliceView end, int32_t order, GoIter *out, UnmanagedVector *errOut) { - return cScan(ptr, gas_meter, used_gas, start, end, order, out, errOut); -} - -// Gateway functions (iterator) -GoError cNext_cgo(IteratorReference *ref, gas_meter_t *gas_meter, uint64_t *used_gas, UnmanagedVector *key, UnmanagedVector *val, UnmanagedVector *errOut) { - return cNext(ref, gas_meter, used_gas, key, val, errOut); -} -GoError cNextKey_cgo(IteratorReference *ref, gas_meter_t *gas_meter, uint64_t *used_gas, UnmanagedVector *key, UnmanagedVector *errOut) { - return cNextKey(ref, gas_meter, used_gas, key, errOut); -} -GoError cNextValue_cgo(IteratorReference *ref, gas_meter_t *gas_meter, uint64_t *used_gas, UnmanagedVector *val, UnmanagedVector *errOut) { - return cNextValue(ref, gas_meter, used_gas, val, errOut); -} - -// Gateway functions (api) -GoError cCanonicalizeAddress_cgo(api_t *ptr, U8SliceView src, UnmanagedVector *dest, UnmanagedVector *errOut, uint64_t *used_gas) { - return cCanonicalizeAddress(ptr, src, dest, errOut, used_gas); -} -GoError cHumanizeAddress_cgo(api_t *ptr, U8SliceView src, UnmanagedVector *dest, UnmanagedVector *errOut, uint64_t *used_gas) { - return cHumanizeAddress(ptr, src, dest, errOut, used_gas); -} -GoError cValidateAddress_cgo(api_t *ptr, U8SliceView src, UnmanagedVector *errOut, uint64_t *used_gas) { - return cValidateAddress(ptr, src, errOut, used_gas); -} - -// Gateway functions (querier) -GoError cQueryExternal_cgo(querier_t *ptr, uint64_t gas_limit, uint64_t *used_gas, U8SliceView request, UnmanagedVector *result, UnmanagedVector *errOut) { - return cQueryExternal(ptr, gas_limit, used_gas, request, result, errOut); -} -*/ -import "C" - -// We need these gateway functions to allow calling back to a go function from the c code. -// At least I didn't discover a cleaner way. -// Also, this needs to be in a different file than `callbacks.go`, as we cannot create functions -// in the same file that has //export directives. Only import header types diff --git a/internal/api/environment.go b/internal/api/environment.go new file mode 100644 index 000000000..81d5e1ffa --- /dev/null +++ b/internal/api/environment.go @@ -0,0 +1,100 @@ +package api + +import ( + "fmt" + + "github.com/CosmWasm/wasmvm/v2/types" +) + +// Environment represents the environment for a Wasm contract +type Environment struct { + Code []byte + Store types.KVStore + API types.GoAPI + Querier types.Querier + Block BlockInfo + Contract ContractInfo + Transaction *TransactionInfo +} + +// BlockInfo contains information about the current block +type BlockInfo struct { + Height int64 + Time int64 + ChainID string +} + +// ContractInfo contains information about the contract +type ContractInfo struct { + Address string + Creator string +} + +// TransactionInfo contains information about the current transaction +type TransactionInfo struct { + Index uint32 +} + +// Validate checks if all required fields are set +func (e *Environment) Validate() error { + if e == nil { + return fmt.Errorf("environment cannot be nil") + } + if e.Code == nil { + return fmt.Errorf("code is required") + } + if e.Store == nil { + return fmt.Errorf("store is required") + } + if e.API.HumanizeAddress == nil || e.API.CanonicalizeAddress == nil || e.API.ValidateAddress == nil { + return fmt.Errorf("all API functions must be set (HumanizeAddress, CanonicalizeAddress, ValidateAddress)") + } + var nilQuerier types.Querier + if e.Querier == nilQuerier { + return fmt.Errorf("querier is required") + } + return nil +} + +// NewEnvironment creates a new environment with the given parameters +func NewEnvironment(code []byte, store types.KVStore, api *types.GoAPI, querier *types.Querier, block BlockInfo, contract ContractInfo, transaction *TransactionInfo) (*Environment, error) { + if api == nil { + return nil, fmt.Errorf("api is required") + } + if querier == nil { + return nil, fmt.Errorf("querier is required") + } + + env := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + Block: block, + Contract: contract, + Transaction: transaction, + } + + if err := env.Validate(); err != nil { + return nil, err + } + + return env, nil +} + +// UpdateEnvironment updates the environment with new parameters +func (e *Environment) UpdateEnvironment(store types.KVStore, api *types.GoAPI, querier *types.Querier, transaction *TransactionInfo) error { + if api == nil { + return fmt.Errorf("api is required") + } + if querier == nil { + return fmt.Errorf("querier is required") + } + + e.Store = store + e.API = *api + e.Querier = *querier + e.Transaction = transaction + + return e.Validate() +} diff --git a/internal/api/iterator_test.go b/internal/api/iterator_test.go deleted file mode 100644 index 0c81db775..000000000 --- a/internal/api/iterator_test.go +++ /dev/null @@ -1,296 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "sync" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/CosmWasm/wasmvm/v2/internal/api/testdb" - "github.com/CosmWasm/wasmvm/v2/types" -) - -type queueData struct { - checksum []byte - store *Lookup - api *types.GoAPI - querier types.Querier -} - -func (q queueData) Store(meter MockGasMeter) types.KVStore { - return q.store.WithGasMeter(meter) -} - -func setupQueueContractWithData(t *testing.T, cache Cache, values ...int) queueData { - checksum := createQueueContract(t, cache) - - gasMeter1 := NewMockGasMeter(TESTING_GAS_LIMIT) - // instantiate it with this store - store := NewLookup(gasMeter1) - api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Array[types.Coin]{types.NewCoin(100, "ATOM")}) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - msg := []byte(`{}`) - - igasMeter1 := types.GasMeter(gasMeter1) - res, _, err := Instantiate(cache, checksum, env, info, msg, &igasMeter1, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireOkResponse(t, res, 0) - - for _, value := range values { - // push 17 - var gasMeter2 types.GasMeter = NewMockGasMeter(TESTING_GAS_LIMIT) - push := []byte(fmt.Sprintf(`{"enqueue":{"value":%d}}`, value)) - res, _, err = Execute(cache, checksum, env, info, push, &gasMeter2, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireOkResponse(t, res, 0) - } - - return queueData{ - checksum: checksum, - store: store, - api: api, - querier: querier, - } -} - -func setupQueueContract(t *testing.T, cache Cache) queueData { - return setupQueueContractWithData(t, cache, 17, 22) -} - -func TestStoreIterator(t *testing.T) { - const limit = 2000 - callID1 := startCall() - callID2 := startCall() - - store := testdb.NewMemDB() - var iter types.Iterator - var index uint64 - var err error - - iter, _ = store.Iterator(nil, nil) - index, err = storeIterator(callID1, iter, limit) - require.NoError(t, err) - require.Equal(t, uint64(1), index) - iter, _ = store.Iterator(nil, nil) - index, err = storeIterator(callID1, iter, limit) - require.NoError(t, err) - require.Equal(t, uint64(2), index) - - iter, _ = store.Iterator(nil, nil) - index, err = storeIterator(callID2, iter, limit) - require.NoError(t, err) - require.Equal(t, uint64(1), index) - iter, _ = store.Iterator(nil, nil) - index, err = storeIterator(callID2, iter, limit) - require.NoError(t, err) - require.Equal(t, uint64(2), index) - iter, _ = store.Iterator(nil, nil) - index, err = storeIterator(callID2, iter, limit) - require.NoError(t, err) - require.Equal(t, uint64(3), index) - - endCall(callID1) - endCall(callID2) -} - -func TestStoreIteratorHitsLimit(t *testing.T) { - callID := startCall() - - store := testdb.NewMemDB() - var iter types.Iterator - var err error - const limit = 2 - - iter, _ = store.Iterator(nil, nil) - _, err = storeIterator(callID, iter, limit) - require.NoError(t, err) - - iter, _ = store.Iterator(nil, nil) - _, err = storeIterator(callID, iter, limit) - require.NoError(t, err) - - iter, _ = store.Iterator(nil, nil) - _, err = storeIterator(callID, iter, limit) - require.ErrorContains(t, err, "Reached iterator limit (2)") - - endCall(callID) -} - -func TestRetrieveIterator(t *testing.T) { - const limit = 2000 - callID1 := startCall() - callID2 := startCall() - - store := testdb.NewMemDB() - var iter types.Iterator - var err error - - iter, _ = store.Iterator(nil, nil) - iteratorID11, err := storeIterator(callID1, iter, limit) - require.NoError(t, err) - iter, _ = store.Iterator(nil, nil) - _, err = storeIterator(callID1, iter, limit) - require.NoError(t, err) - iter, _ = store.Iterator(nil, nil) - _, err = storeIterator(callID2, iter, limit) - require.NoError(t, err) - iter, _ = store.Iterator(nil, nil) - iteratorID22, err := storeIterator(callID2, iter, limit) - require.NoError(t, err) - iter, err = store.Iterator(nil, nil) - require.NoError(t, err) - iteratorID23, err := storeIterator(callID2, iter, limit) - require.NoError(t, err) - - // Retrieve existing - iter = retrieveIterator(callID1, iteratorID11) - require.NotNil(t, iter) - iter = retrieveIterator(callID2, iteratorID22) - require.NotNil(t, iter) - - // Retrieve with non-existent iterator ID - iter = retrieveIterator(callID1, iteratorID23) - require.Nil(t, iter) - iter = retrieveIterator(callID1, uint64(0)) - require.Nil(t, iter) - iter = retrieveIterator(callID1, uint64(2147483647)) - require.Nil(t, iter) - iter = retrieveIterator(callID1, uint64(2147483648)) - require.Nil(t, iter) - iter = retrieveIterator(callID1, uint64(18446744073709551615)) - require.Nil(t, iter) - - // Retrieve with non-existent call ID - iter = retrieveIterator(callID1+1_234_567, iteratorID23) - require.Nil(t, iter) - - endCall(callID1) - endCall(callID2) -} - -func TestQueueIteratorSimple(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - - setup := setupQueueContract(t, cache) - checksum, querier, api := setup.checksum, setup.querier, setup.api - - // query the sum - gasMeter := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter := types.GasMeter(gasMeter) - store := setup.Store(gasMeter) - query := []byte(`{"sum":{}}`) - env := MockEnvBin(t) - data, _, err := Query(cache, checksum, env, query, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - var qResult types.QueryResult - err = json.Unmarshal(data, &qResult) - require.NoError(t, err) - require.Equal(t, "", qResult.Err) - require.Equal(t, `{"sum":39}`, string(qResult.Ok)) - - // query reduce (multiple iterators at once) - query = []byte(`{"reducer":{}}`) - data, _, err = Query(cache, checksum, env, query, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - var reduced types.QueryResult - err = json.Unmarshal(data, &reduced) - require.NoError(t, err) - require.Equal(t, "", reduced.Err) - require.Equal(t, `{"counters":[[17,22],[22,0]]}`, string(reduced.Ok)) -} - -func TestQueueIteratorRaces(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - - assert.Equal(t, 0, len(iteratorFrames)) - - contract1 := setupQueueContractWithData(t, cache, 17, 22) - contract2 := setupQueueContractWithData(t, cache, 1, 19, 6, 35, 8) - contract3 := setupQueueContractWithData(t, cache, 11, 6, 2) - env := MockEnvBin(t) - - reduceQuery := func(t *testing.T, setup queueData, expected string) { - checksum, querier, api := setup.checksum, setup.querier, setup.api - gasMeter := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter := types.GasMeter(gasMeter) - store := setup.Store(gasMeter) - - // query reduce (multiple iterators at once) - query := []byte(`{"reducer":{}}`) - data, _, err := Query(cache, checksum, env, query, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - var reduced types.QueryResult - err = json.Unmarshal(data, &reduced) - require.NoError(t, err) - require.Equal(t, "", reduced.Err) - require.Equal(t, fmt.Sprintf(`{"counters":%s}`, expected), string(reduced.Ok)) - } - - // 30 concurrent batches (in go routines) to trigger any race condition - numBatches := 30 - - var wg sync.WaitGroup - // for each batch, query each of the 3 contracts - so the contract queries get mixed together - wg.Add(numBatches * 3) - for i := 0; i < numBatches; i++ { - go func() { - reduceQuery(t, contract1, "[[17,22],[22,0]]") - wg.Done() - }() - go func() { - reduceQuery(t, contract2, "[[1,68],[19,35],[6,62],[35,0],[8,54]]") - wg.Done() - }() - go func() { - reduceQuery(t, contract3, "[[11,0],[6,11],[2,17]]") - wg.Done() - }() - } - wg.Wait() - - // when they finish, we should have removed all frames - assert.Equal(t, 0, len(iteratorFrames)) -} - -func TestQueueIteratorLimit(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - - setup := setupQueueContract(t, cache) - checksum, querier, api := setup.checksum, setup.querier, setup.api - - var err error - var qResult types.QueryResult - var gasLimit uint64 - - // Open 5000 iterators - gasLimit = TESTING_GAS_LIMIT - gasMeter := NewMockGasMeter(gasLimit) - igasMeter := types.GasMeter(gasMeter) - store := setup.Store(gasMeter) - query := []byte(`{"open_iterators":{"count":5000}}`) - env := MockEnvBin(t) - data, _, err := Query(cache, checksum, env, query, &igasMeter, store, api, &querier, gasLimit, TESTING_PRINT_DEBUG) - require.NoError(t, err) - err = json.Unmarshal(data, &qResult) - require.NoError(t, err) - require.Equal(t, "", qResult.Err) - require.Equal(t, `{}`, string(qResult.Ok)) - - // Open 35000 iterators - gasLimit = TESTING_GAS_LIMIT * 4 - gasMeter = NewMockGasMeter(gasLimit) - igasMeter = types.GasMeter(gasMeter) - store = setup.Store(gasMeter) - query = []byte(`{"open_iterators":{"count":35000}}`) - env = MockEnvBin(t) - _, _, err = Query(cache, checksum, env, query, &igasMeter, store, api, &querier, gasLimit, TESTING_PRINT_DEBUG) - require.ErrorContains(t, err, "Reached iterator limit (32768)") -} diff --git a/internal/api/lib.go b/internal/api/lib.go deleted file mode 100644 index 4041515f1..000000000 --- a/internal/api/lib.go +++ /dev/null @@ -1,908 +0,0 @@ -package api - -// #include -// #include "bindings.h" -import "C" - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - "syscall" - - "golang.org/x/sys/unix" - - "github.com/CosmWasm/wasmvm/v2/types" -) - -// Value types -type ( - cint = C.int - cbool = C.bool - cusize = C.size_t - cu8 = C.uint8_t - cu32 = C.uint32_t - cu64 = C.uint64_t - ci8 = C.int8_t - ci32 = C.int32_t - ci64 = C.int64_t -) - -// Pointers -type ( - cu8_ptr = *C.uint8_t -) - -type Cache struct { - ptr *C.cache_t - lockfile os.File -} - -type Querier = types.Querier - -func InitCache(config types.VMConfig) (Cache, error) { - // libwasmvm would create this directory too but we need it earlier for the lockfile - err := os.MkdirAll(config.Cache.BaseDir, 0o755) - if err != nil { - return Cache{}, fmt.Errorf("Could not create base directory") - } - - lockfile, err := os.OpenFile(filepath.Join(config.Cache.BaseDir, "exclusive.lock"), os.O_WRONLY|os.O_CREATE, 0o666) - if err != nil { - return Cache{}, fmt.Errorf("Could not open exclusive.lock") - } - _, err = lockfile.WriteString("This is a lockfile that prevent two VM instances to operate on the same directory in parallel.\nSee codebase at github.com/CosmWasm/wasmvm for more information.\nSafety first – brought to you by Confio ❤️\n") - if err != nil { - return Cache{}, fmt.Errorf("Error writing to exclusive.lock") - } - - err = unix.Flock(int(lockfile.Fd()), unix.LOCK_EX|unix.LOCK_NB) - if err != nil { - return Cache{}, fmt.Errorf("Could not lock exclusive.lock. Is a different VM running in the same directory already?") - } - - configBytes, err := json.Marshal(config) - if err != nil { - return Cache{}, fmt.Errorf("Could not serialize config") - } - configView := makeView(configBytes) - defer runtime.KeepAlive(configBytes) - - errmsg := uninitializedUnmanagedVector() - - ptr, err := C.init_cache(configView, &errmsg) - if err != nil { - return Cache{}, errorWithMessage(err, errmsg) - } - return Cache{ptr: ptr, lockfile: *lockfile}, nil -} - -func ReleaseCache(cache Cache) { - C.release_cache(cache.ptr) - - cache.lockfile.Close() // Also releases the file lock -} - -func StoreCode(cache Cache, wasm []byte) ([]byte, error) { - w := makeView(wasm) - defer runtime.KeepAlive(wasm) - errmsg := uninitializedUnmanagedVector() - checksum, err := C.save_wasm(cache.ptr, w, cbool(false), &errmsg) - if err != nil { - return nil, errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(checksum), nil -} - -func StoreCodeUnchecked(cache Cache, wasm []byte) ([]byte, error) { - w := makeView(wasm) - defer runtime.KeepAlive(wasm) - errmsg := uninitializedUnmanagedVector() - checksum, err := C.save_wasm(cache.ptr, w, cbool(true), &errmsg) - if err != nil { - return nil, errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(checksum), nil -} - -func RemoveCode(cache Cache, checksum []byte) error { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - errmsg := uninitializedUnmanagedVector() - _, err := C.remove_wasm(cache.ptr, cs, &errmsg) - if err != nil { - return errorWithMessage(err, errmsg) - } - return nil -} - -func GetCode(cache Cache, checksum []byte) ([]byte, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - errmsg := uninitializedUnmanagedVector() - wasm, err := C.load_wasm(cache.ptr, cs, &errmsg) - if err != nil { - return nil, errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(wasm), nil -} - -func Pin(cache Cache, checksum []byte) error { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - errmsg := uninitializedUnmanagedVector() - _, err := C.pin(cache.ptr, cs, &errmsg) - if err != nil { - return errorWithMessage(err, errmsg) - } - return nil -} - -func Unpin(cache Cache, checksum []byte) error { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - errmsg := uninitializedUnmanagedVector() - _, err := C.unpin(cache.ptr, cs, &errmsg) - if err != nil { - return errorWithMessage(err, errmsg) - } - return nil -} - -func AnalyzeCode(cache Cache, checksum []byte) (*types.AnalysisReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - errmsg := uninitializedUnmanagedVector() - report, err := C.analyze_code(cache.ptr, cs, &errmsg) - if err != nil { - return nil, errorWithMessage(err, errmsg) - } - requiredCapabilities := string(copyAndDestroyUnmanagedVector(report.required_capabilities)) - entrypoints := string(copyAndDestroyUnmanagedVector(report.entrypoints)) - - res := types.AnalysisReport{ - HasIBCEntryPoints: bool(report.has_ibc_entry_points), - RequiredCapabilities: requiredCapabilities, - Entrypoints: strings.Split(entrypoints, ","), - ContractMigrateVersion: optionalU64ToPtr(report.contract_migrate_version), - } - return &res, nil -} - -func GetMetrics(cache Cache) (*types.Metrics, error) { - errmsg := uninitializedUnmanagedVector() - metrics, err := C.get_metrics(cache.ptr, &errmsg) - if err != nil { - return nil, errorWithMessage(err, errmsg) - } - - return &types.Metrics{ - HitsPinnedMemoryCache: uint32(metrics.hits_pinned_memory_cache), - HitsMemoryCache: uint32(metrics.hits_memory_cache), - HitsFsCache: uint32(metrics.hits_fs_cache), - Misses: uint32(metrics.misses), - ElementsPinnedMemoryCache: uint64(metrics.elements_pinned_memory_cache), - ElementsMemoryCache: uint64(metrics.elements_memory_cache), - SizePinnedMemoryCache: uint64(metrics.size_pinned_memory_cache), - SizeMemoryCache: uint64(metrics.size_memory_cache), - }, nil -} - -func GetPinnedMetrics(cache Cache) (*types.PinnedMetrics, error) { - errmsg := uninitializedUnmanagedVector() - metrics, err := C.get_pinned_metrics(cache.ptr, &errmsg) - if err != nil { - return nil, errorWithMessage(err, errmsg) - } - - var pinnedMetrics types.PinnedMetrics - if err := pinnedMetrics.UnmarshalMessagePack(copyAndDestroyUnmanagedVector(metrics)); err != nil { - return nil, err - } - - return &pinnedMetrics, nil -} - -func Instantiate( - cache Cache, - checksum []byte, - env []byte, - info []byte, - msg []byte, - gasMeter *types.GasMeter, - store types.KVStore, - api *types.GoAPI, - querier *Querier, - gasLimit uint64, - printDebug bool, -) ([]byte, types.GasReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - e := makeView(env) - defer runtime.KeepAlive(env) - i := makeView(info) - defer runtime.KeepAlive(info) - m := makeView(msg) - defer runtime.KeepAlive(msg) - var pinner runtime.Pinner - pinner.Pin(gasMeter) - checkAndPinAPI(api, pinner) - checkAndPinQuerier(querier, pinner) - defer pinner.Unpin() - - callID := startCall() - defer endCall(callID) - - dbState := buildDBState(store, callID) - db := buildDB(&dbState, gasMeter) - a := buildAPI(api) - q := buildQuerier(querier) - var gasReport C.GasReport - errmsg := uninitializedUnmanagedVector() - - res, err := C.instantiate(cache.ptr, cs, e, i, m, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) - if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { - // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. - return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil -} - -func Execute( - cache Cache, - checksum []byte, - env []byte, - info []byte, - msg []byte, - gasMeter *types.GasMeter, - store types.KVStore, - api *types.GoAPI, - querier *Querier, - gasLimit uint64, - printDebug bool, -) ([]byte, types.GasReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - e := makeView(env) - defer runtime.KeepAlive(env) - i := makeView(info) - defer runtime.KeepAlive(info) - m := makeView(msg) - defer runtime.KeepAlive(msg) - var pinner runtime.Pinner - pinner.Pin(gasMeter) - checkAndPinAPI(api, pinner) - checkAndPinQuerier(querier, pinner) - defer pinner.Unpin() - - callID := startCall() - defer endCall(callID) - - dbState := buildDBState(store, callID) - db := buildDB(&dbState, gasMeter) - a := buildAPI(api) - q := buildQuerier(querier) - var gasReport C.GasReport - errmsg := uninitializedUnmanagedVector() - - res, err := C.execute(cache.ptr, cs, e, i, m, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) - if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { - // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. - return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil -} - -func Migrate( - cache Cache, - checksum []byte, - env []byte, - msg []byte, - gasMeter *types.GasMeter, - store types.KVStore, - api *types.GoAPI, - querier *Querier, - gasLimit uint64, - printDebug bool, -) ([]byte, types.GasReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - e := makeView(env) - defer runtime.KeepAlive(env) - m := makeView(msg) - defer runtime.KeepAlive(msg) - var pinner runtime.Pinner - pinner.Pin(gasMeter) - checkAndPinAPI(api, pinner) - checkAndPinQuerier(querier, pinner) - defer pinner.Unpin() - - callID := startCall() - defer endCall(callID) - - dbState := buildDBState(store, callID) - db := buildDB(&dbState, gasMeter) - a := buildAPI(api) - q := buildQuerier(querier) - var gasReport C.GasReport - errmsg := uninitializedUnmanagedVector() - - res, err := C.migrate(cache.ptr, cs, e, m, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) - if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { - // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. - return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil -} - -func MigrateWithInfo( - cache Cache, - checksum []byte, - env []byte, - msg []byte, - migrateInfo []byte, - gasMeter *types.GasMeter, - store types.KVStore, - api *types.GoAPI, - querier *Querier, - gasLimit uint64, - printDebug bool, -) ([]byte, types.GasReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - e := makeView(env) - defer runtime.KeepAlive(env) - m := makeView(msg) - defer runtime.KeepAlive(msg) - i := makeView(migrateInfo) - defer runtime.KeepAlive(i) - var pinner runtime.Pinner - pinner.Pin(gasMeter) - checkAndPinAPI(api, pinner) - checkAndPinQuerier(querier, pinner) - defer pinner.Unpin() - - callID := startCall() - defer endCall(callID) - - dbState := buildDBState(store, callID) - db := buildDB(&dbState, gasMeter) - a := buildAPI(api) - q := buildQuerier(querier) - var gasReport C.GasReport - errmsg := uninitializedUnmanagedVector() - - res, err := C.migrate_with_info(cache.ptr, cs, e, m, i, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) - if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { - // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. - return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil -} - -func Sudo( - cache Cache, - checksum []byte, - env []byte, - msg []byte, - gasMeter *types.GasMeter, - store types.KVStore, - api *types.GoAPI, - querier *Querier, - gasLimit uint64, - printDebug bool, -) ([]byte, types.GasReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - e := makeView(env) - defer runtime.KeepAlive(env) - m := makeView(msg) - defer runtime.KeepAlive(msg) - var pinner runtime.Pinner - pinner.Pin(gasMeter) - checkAndPinAPI(api, pinner) - checkAndPinQuerier(querier, pinner) - defer pinner.Unpin() - - callID := startCall() - defer endCall(callID) - - dbState := buildDBState(store, callID) - db := buildDB(&dbState, gasMeter) - a := buildAPI(api) - q := buildQuerier(querier) - var gasReport C.GasReport - errmsg := uninitializedUnmanagedVector() - - res, err := C.sudo(cache.ptr, cs, e, m, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) - if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { - // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. - return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil -} - -func Reply( - cache Cache, - checksum []byte, - env []byte, - reply []byte, - gasMeter *types.GasMeter, - store types.KVStore, - api *types.GoAPI, - querier *Querier, - gasLimit uint64, - printDebug bool, -) ([]byte, types.GasReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - e := makeView(env) - defer runtime.KeepAlive(env) - r := makeView(reply) - defer runtime.KeepAlive(reply) - var pinner runtime.Pinner - pinner.Pin(gasMeter) - checkAndPinAPI(api, pinner) - checkAndPinQuerier(querier, pinner) - defer pinner.Unpin() - - callID := startCall() - defer endCall(callID) - - dbState := buildDBState(store, callID) - db := buildDB(&dbState, gasMeter) - a := buildAPI(api) - q := buildQuerier(querier) - var gasReport C.GasReport - errmsg := uninitializedUnmanagedVector() - - res, err := C.reply(cache.ptr, cs, e, r, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) - if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { - // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. - return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil -} - -func Query( - cache Cache, - checksum []byte, - env []byte, - msg []byte, - gasMeter *types.GasMeter, - store types.KVStore, - api *types.GoAPI, - querier *Querier, - gasLimit uint64, - printDebug bool, -) ([]byte, types.GasReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - e := makeView(env) - defer runtime.KeepAlive(env) - m := makeView(msg) - defer runtime.KeepAlive(msg) - var pinner runtime.Pinner - pinner.Pin(gasMeter) - checkAndPinAPI(api, pinner) - checkAndPinQuerier(querier, pinner) - defer pinner.Unpin() - - callID := startCall() - defer endCall(callID) - - dbState := buildDBState(store, callID) - db := buildDB(&dbState, gasMeter) - a := buildAPI(api) - q := buildQuerier(querier) - var gasReport C.GasReport - errmsg := uninitializedUnmanagedVector() - - res, err := C.query(cache.ptr, cs, e, m, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) - if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { - // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. - return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil -} - -func IBCChannelOpen( - cache Cache, - checksum []byte, - env []byte, - msg []byte, - gasMeter *types.GasMeter, - store types.KVStore, - api *types.GoAPI, - querier *Querier, - gasLimit uint64, - printDebug bool, -) ([]byte, types.GasReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - e := makeView(env) - defer runtime.KeepAlive(env) - m := makeView(msg) - defer runtime.KeepAlive(msg) - var pinner runtime.Pinner - pinner.Pin(gasMeter) - checkAndPinAPI(api, pinner) - checkAndPinQuerier(querier, pinner) - defer pinner.Unpin() - - callID := startCall() - defer endCall(callID) - - dbState := buildDBState(store, callID) - db := buildDB(&dbState, gasMeter) - a := buildAPI(api) - q := buildQuerier(querier) - var gasReport C.GasReport - errmsg := uninitializedUnmanagedVector() - - res, err := C.ibc_channel_open(cache.ptr, cs, e, m, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) - if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { - // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. - return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil -} - -func IBCChannelConnect( - cache Cache, - checksum []byte, - env []byte, - msg []byte, - gasMeter *types.GasMeter, - store types.KVStore, - api *types.GoAPI, - querier *Querier, - gasLimit uint64, - printDebug bool, -) ([]byte, types.GasReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - e := makeView(env) - defer runtime.KeepAlive(env) - m := makeView(msg) - defer runtime.KeepAlive(msg) - var pinner runtime.Pinner - pinner.Pin(gasMeter) - checkAndPinAPI(api, pinner) - checkAndPinQuerier(querier, pinner) - defer pinner.Unpin() - - callID := startCall() - defer endCall(callID) - - dbState := buildDBState(store, callID) - db := buildDB(&dbState, gasMeter) - a := buildAPI(api) - q := buildQuerier(querier) - var gasReport C.GasReport - errmsg := uninitializedUnmanagedVector() - - res, err := C.ibc_channel_connect(cache.ptr, cs, e, m, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) - if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { - // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. - return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil -} - -func IBCChannelClose( - cache Cache, - checksum []byte, - env []byte, - msg []byte, - gasMeter *types.GasMeter, - store types.KVStore, - api *types.GoAPI, - querier *Querier, - gasLimit uint64, - printDebug bool, -) ([]byte, types.GasReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - e := makeView(env) - defer runtime.KeepAlive(env) - m := makeView(msg) - defer runtime.KeepAlive(msg) - var pinner runtime.Pinner - pinner.Pin(gasMeter) - checkAndPinAPI(api, pinner) - checkAndPinQuerier(querier, pinner) - defer pinner.Unpin() - - callID := startCall() - defer endCall(callID) - - dbState := buildDBState(store, callID) - db := buildDB(&dbState, gasMeter) - a := buildAPI(api) - q := buildQuerier(querier) - var gasReport C.GasReport - errmsg := uninitializedUnmanagedVector() - - res, err := C.ibc_channel_close(cache.ptr, cs, e, m, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) - if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { - // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. - return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil -} - -func IBCPacketReceive( - cache Cache, - checksum []byte, - env []byte, - packet []byte, - gasMeter *types.GasMeter, - store types.KVStore, - api *types.GoAPI, - querier *Querier, - gasLimit uint64, - printDebug bool, -) ([]byte, types.GasReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - e := makeView(env) - defer runtime.KeepAlive(env) - pa := makeView(packet) - defer runtime.KeepAlive(packet) - var pinner runtime.Pinner - pinner.Pin(gasMeter) - checkAndPinAPI(api, pinner) - checkAndPinQuerier(querier, pinner) - defer pinner.Unpin() - - callID := startCall() - defer endCall(callID) - - dbState := buildDBState(store, callID) - db := buildDB(&dbState, gasMeter) - a := buildAPI(api) - q := buildQuerier(querier) - var gasReport C.GasReport - errmsg := uninitializedUnmanagedVector() - - res, err := C.ibc_packet_receive(cache.ptr, cs, e, pa, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) - if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { - // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. - return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil -} - -func IBCPacketAck( - cache Cache, - checksum []byte, - env []byte, - ack []byte, - gasMeter *types.GasMeter, - store types.KVStore, - api *types.GoAPI, - querier *Querier, - gasLimit uint64, - printDebug bool, -) ([]byte, types.GasReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - e := makeView(env) - defer runtime.KeepAlive(env) - ac := makeView(ack) - defer runtime.KeepAlive(ack) - var pinner runtime.Pinner - pinner.Pin(gasMeter) - checkAndPinAPI(api, pinner) - checkAndPinQuerier(querier, pinner) - defer pinner.Unpin() - - callID := startCall() - defer endCall(callID) - - dbState := buildDBState(store, callID) - db := buildDB(&dbState, gasMeter) - a := buildAPI(api) - q := buildQuerier(querier) - var gasReport C.GasReport - errmsg := uninitializedUnmanagedVector() - - res, err := C.ibc_packet_ack(cache.ptr, cs, e, ac, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) - if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { - // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. - return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil -} - -func IBCPacketTimeout( - cache Cache, - checksum []byte, - env []byte, - packet []byte, - gasMeter *types.GasMeter, - store types.KVStore, - api *types.GoAPI, - querier *Querier, - gasLimit uint64, - printDebug bool, -) ([]byte, types.GasReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - e := makeView(env) - defer runtime.KeepAlive(env) - pa := makeView(packet) - defer runtime.KeepAlive(packet) - var pinner runtime.Pinner - pinner.Pin(gasMeter) - checkAndPinAPI(api, pinner) - checkAndPinQuerier(querier, pinner) - defer pinner.Unpin() - - callID := startCall() - defer endCall(callID) - - dbState := buildDBState(store, callID) - db := buildDB(&dbState, gasMeter) - a := buildAPI(api) - q := buildQuerier(querier) - var gasReport C.GasReport - errmsg := uninitializedUnmanagedVector() - - res, err := C.ibc_packet_timeout(cache.ptr, cs, e, pa, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) - if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { - // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. - return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil -} - -func IBCSourceCallback( - cache Cache, - checksum []byte, - env []byte, - msg []byte, - gasMeter *types.GasMeter, - store types.KVStore, - api *types.GoAPI, - querier *Querier, - gasLimit uint64, - printDebug bool, -) ([]byte, types.GasReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - e := makeView(env) - defer runtime.KeepAlive(env) - msgBytes := makeView(msg) - defer runtime.KeepAlive(msg) - var pinner runtime.Pinner - pinner.Pin(gasMeter) - checkAndPinAPI(api, pinner) - checkAndPinQuerier(querier, pinner) - defer pinner.Unpin() - - callID := startCall() - defer endCall(callID) - - dbState := buildDBState(store, callID) - db := buildDB(&dbState, gasMeter) - a := buildAPI(api) - q := buildQuerier(querier) - var gasReport C.GasReport - errmsg := uninitializedUnmanagedVector() - - res, err := C.ibc_source_callback(cache.ptr, cs, e, msgBytes, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) - if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { - // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. - return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil -} - -func IBCDestinationCallback( - cache Cache, - checksum []byte, - env []byte, - msg []byte, - gasMeter *types.GasMeter, - store types.KVStore, - api *types.GoAPI, - querier *Querier, - gasLimit uint64, - printDebug bool, -) ([]byte, types.GasReport, error) { - cs := makeView(checksum) - defer runtime.KeepAlive(checksum) - e := makeView(env) - defer runtime.KeepAlive(env) - msgBytes := makeView(msg) - defer runtime.KeepAlive(msg) - var pinner runtime.Pinner - pinner.Pin(gasMeter) - checkAndPinAPI(api, pinner) - checkAndPinQuerier(querier, pinner) - defer pinner.Unpin() - - callID := startCall() - defer endCall(callID) - - dbState := buildDBState(store, callID) - db := buildDB(&dbState, gasMeter) - a := buildAPI(api) - q := buildQuerier(querier) - var gasReport C.GasReport - errmsg := uninitializedUnmanagedVector() - - res, err := C.ibc_destination_callback(cache.ptr, cs, e, msgBytes, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) - if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { - // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. - return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) - } - return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil -} - -func convertGasReport(report C.GasReport) types.GasReport { - return types.GasReport{ - Limit: uint64(report.limit), - Remaining: uint64(report.remaining), - UsedExternally: uint64(report.used_externally), - UsedInternally: uint64(report.used_internally), - } -} - -/**** To error module ***/ - -func errorWithMessage(err error, b C.UnmanagedVector) error { - // we always destroy the unmanaged vector to avoid a memory leak - msg := copyAndDestroyUnmanagedVector(b) - - // this checks for out of gas as a special case - if errno, ok := err.(syscall.Errno); ok && int(errno) == 2 { - return types.OutOfGasError{} - } - if msg == nil { - return err - } - return fmt.Errorf("%s", string(msg)) -} - -// checkAndPinAPI checks and pins the API and relevant pointers inside of it. -// All errors will result in panics as they indicate misuse of the wasmvm API and are not expected -// to be caused by user data. -func checkAndPinAPI(api *types.GoAPI, pinner runtime.Pinner) { - if api == nil { - panic("API must not be nil. If you don't want to provide API functionality, please create an instance that returns an error on every call to HumanizeAddress(), CanonicalizeAddress() and ValidateAddress().") - } - - // func cHumanizeAddress assumes this is set - if api.HumanizeAddress == nil { - panic("HumanizeAddress in API must not be nil. If you don't want to provide API functionality, please create an instance that returns an error on every call to HumanizeAddress(), CanonicalizeAddress() and ValidateAddress().") - } - - // func cCanonicalizeAddress assumes this is set - if api.CanonicalizeAddress == nil { - panic("CanonicalizeAddress in API must not be nil. If you don't want to provide API functionality, please create an instance that returns an error on every call to HumanizeAddress(), CanonicalizeAddress() and ValidateAddress().") - } - - // func cValidateAddress assumes this is set - if api.ValidateAddress == nil { - panic("ValidateAddress in API must not be nil. If you don't want to provide API functionality, please create an instance that returns an error on every call to HumanizeAddress(), CanonicalizeAddress() and ValidateAddress().") - } - - pinner.Pin(api) // this pointer is used in Rust (`state` in `C.GoApi`) and must not change -} - -// checkAndPinQuerier checks and pins the querier. -// All errors will result in panics as they indicate misuse of the wasmvm API and are not expected -// to be caused by user data. -func checkAndPinQuerier(querier *Querier, pinner runtime.Pinner) { - if querier == nil { - panic("Querier must not be nil. If you don't want to provide querier functionality, please create an instance that returns an error on every call to Query().") - } - - pinner.Pin(querier) // this pointer is used in Rust (`state` in `C.GoQuerier`) and must not change -} diff --git a/internal/api/lib_test.go b/internal/api/lib_test.go deleted file mode 100644 index 67470308b..000000000 --- a/internal/api/lib_test.go +++ /dev/null @@ -1,1448 +0,0 @@ -package api - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/CosmWasm/wasmvm/v2/types" -) - -const ( - TESTING_PRINT_DEBUG = false - TESTING_GAS_LIMIT = uint64(500_000_000_000) // ~0.5ms - TESTING_MEMORY_LIMIT = 32 // MiB - TESTING_CACHE_SIZE = 100 // MiB -) - -var TESTING_CAPABILITIES = []string{"staking", "stargate", "iterator", "cosmwasm_1_1", "cosmwasm_1_2", "cosmwasm_1_3"} - -func TestInitAndReleaseCache(t *testing.T) { - tmpdir, err := os.MkdirTemp("", "wasmvm-testing") - require.NoError(t, err) - defer os.RemoveAll(tmpdir) - - config := types.VMConfig{ - Cache: types.CacheOptions{ - BaseDir: tmpdir, - AvailableCapabilities: TESTING_CAPABILITIES, - MemoryCacheSizeBytes: types.NewSizeMebi(TESTING_CACHE_SIZE), - InstanceMemoryLimitBytes: types.NewSizeMebi(TESTING_MEMORY_LIMIT), - }, - } - cache, err := InitCache(config) - require.NoError(t, err) - ReleaseCache(cache) -} - -// wasmd expects us to create the base directory -// https://github.com/CosmWasm/wasmd/blob/v0.30.0/x/wasm/keeper/keeper.go#L128 -func TestInitCacheWorksForNonExistentDir(t *testing.T) { - tmpdir, err := os.MkdirTemp("", "wasmvm-testing") - require.NoError(t, err) - defer os.RemoveAll(tmpdir) - - createMe := filepath.Join(tmpdir, "does-not-yet-exist") - config := types.VMConfig{ - Cache: types.CacheOptions{ - BaseDir: createMe, - AvailableCapabilities: TESTING_CAPABILITIES, - MemoryCacheSizeBytes: types.NewSizeMebi(TESTING_CACHE_SIZE), - InstanceMemoryLimitBytes: types.NewSizeMebi(TESTING_MEMORY_LIMIT), - }, - } - cache, err := InitCache(config) - require.NoError(t, err) - ReleaseCache(cache) -} - -func TestInitCacheErrorsForBrokenDir(t *testing.T) { - // Use colon to make this fail on Windows - // https://gist.github.com/doctaphred/d01d05291546186941e1b7ddc02034d3 - // On Unix we should not have permission to create this. - cannotBeCreated := "/foo:bar" - config := types.VMConfig{ - Cache: types.CacheOptions{ - BaseDir: cannotBeCreated, - AvailableCapabilities: TESTING_CAPABILITIES, - MemoryCacheSizeBytes: types.NewSizeMebi(TESTING_CACHE_SIZE), - InstanceMemoryLimitBytes: types.NewSizeMebi(TESTING_MEMORY_LIMIT), - }, - } - _, err := InitCache(config) - require.ErrorContains(t, err, "Could not create base directory") -} - -func TestInitLockingPreventsConcurrentAccess(t *testing.T) { - tmpdir, err := os.MkdirTemp("", "wasmvm-testing") - require.NoError(t, err) - defer os.RemoveAll(tmpdir) - - config1 := types.VMConfig{ - Cache: types.CacheOptions{ - BaseDir: tmpdir, - AvailableCapabilities: TESTING_CAPABILITIES, - MemoryCacheSizeBytes: types.NewSizeMebi(TESTING_CACHE_SIZE), - InstanceMemoryLimitBytes: types.NewSizeMebi(TESTING_MEMORY_LIMIT), - }, - } - cache1, err1 := InitCache(config1) - require.NoError(t, err1) - - config2 := types.VMConfig{ - Cache: types.CacheOptions{ - BaseDir: tmpdir, - AvailableCapabilities: TESTING_CAPABILITIES, - MemoryCacheSizeBytes: types.NewSizeMebi(TESTING_CACHE_SIZE), - InstanceMemoryLimitBytes: types.NewSizeMebi(TESTING_MEMORY_LIMIT), - }, - } - _, err2 := InitCache(config2) - require.ErrorContains(t, err2, "Could not lock exclusive.lock") - - ReleaseCache(cache1) - - // Now we can try again - config3 := types.VMConfig{ - Cache: types.CacheOptions{ - BaseDir: tmpdir, - AvailableCapabilities: TESTING_CAPABILITIES, - MemoryCacheSizeBytes: types.NewSizeMebi(TESTING_CACHE_SIZE), - InstanceMemoryLimitBytes: types.NewSizeMebi(TESTING_MEMORY_LIMIT), - }, - } - cache3, err3 := InitCache(config3) - require.NoError(t, err3) - ReleaseCache(cache3) -} - -func TestInitLockingAllowsMultipleInstancesInDifferentDirs(t *testing.T) { - tmpdir1, err := os.MkdirTemp("", "wasmvm-testing1") - require.NoError(t, err) - tmpdir2, err := os.MkdirTemp("", "wasmvm-testing2") - require.NoError(t, err) - tmpdir3, err := os.MkdirTemp("", "wasmvm-testing3") - require.NoError(t, err) - defer os.RemoveAll(tmpdir1) - defer os.RemoveAll(tmpdir2) - defer os.RemoveAll(tmpdir3) - - config1 := types.VMConfig{ - Cache: types.CacheOptions{ - BaseDir: tmpdir1, - AvailableCapabilities: TESTING_CAPABILITIES, - MemoryCacheSizeBytes: types.NewSizeMebi(TESTING_CACHE_SIZE), - InstanceMemoryLimitBytes: types.NewSizeMebi(TESTING_MEMORY_LIMIT), - }, - } - cache1, err1 := InitCache(config1) - require.NoError(t, err1) - config2 := types.VMConfig{ - Cache: types.CacheOptions{ - BaseDir: tmpdir2, - AvailableCapabilities: TESTING_CAPABILITIES, - MemoryCacheSizeBytes: types.NewSizeMebi(TESTING_CACHE_SIZE), - InstanceMemoryLimitBytes: types.NewSizeMebi(TESTING_MEMORY_LIMIT), - }, - } - cache2, err2 := InitCache(config2) - require.NoError(t, err2) - config3 := types.VMConfig{ - Cache: types.CacheOptions{ - BaseDir: tmpdir3, - AvailableCapabilities: TESTING_CAPABILITIES, - MemoryCacheSizeBytes: types.NewSizeMebi(TESTING_CACHE_SIZE), - InstanceMemoryLimitBytes: types.NewSizeMebi(TESTING_MEMORY_LIMIT), - }, - } - cache3, err3 := InitCache(config3) - require.NoError(t, err3) - - ReleaseCache(cache1) - ReleaseCache(cache2) - ReleaseCache(cache3) -} - -func TestInitCacheEmptyCapabilities(t *testing.T) { - tmpdir, err := os.MkdirTemp("", "wasmvm-testing") - require.NoError(t, err) - defer os.RemoveAll(tmpdir) - config := types.VMConfig{ - Cache: types.CacheOptions{ - BaseDir: tmpdir, - AvailableCapabilities: []string{}, - MemoryCacheSizeBytes: types.NewSizeMebi(TESTING_CACHE_SIZE), - InstanceMemoryLimitBytes: types.NewSizeMebi(TESTING_MEMORY_LIMIT), - }, - } - cache, err := InitCache(config) - require.NoError(t, err) - ReleaseCache(cache) -} - -func withCache(t testing.TB) (Cache, func()) { - tmpdir, err := os.MkdirTemp("", "wasmvm-testing") - require.NoError(t, err) - config := types.VMConfig{ - Cache: types.CacheOptions{ - BaseDir: tmpdir, - AvailableCapabilities: TESTING_CAPABILITIES, - MemoryCacheSizeBytes: types.NewSizeMebi(TESTING_CACHE_SIZE), - InstanceMemoryLimitBytes: types.NewSizeMebi(TESTING_MEMORY_LIMIT), - }, - } - cache, err := InitCache(config) - require.NoError(t, err) - - cleanup := func() { - os.RemoveAll(tmpdir) - ReleaseCache(cache) - } - return cache, cleanup -} - -func TestStoreCodeAndGetCode(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - - wasm, err := os.ReadFile("../../testdata/hackatom.wasm") - require.NoError(t, err) - - checksum, err := StoreCode(cache, wasm) - require.NoError(t, err) - expectedChecksum := sha256.Sum256(wasm) - require.Equal(t, expectedChecksum[:], checksum) - - code, err := GetCode(cache, checksum) - require.NoError(t, err) - require.Equal(t, wasm, code) -} - -func TestRemoveCode(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - - wasm, err := os.ReadFile("../../testdata/hackatom.wasm") - require.NoError(t, err) - - checksum, err := StoreCode(cache, wasm) - require.NoError(t, err) - - // First removal works - err = RemoveCode(cache, checksum) - require.NoError(t, err) - - // Second removal fails - err = RemoveCode(cache, checksum) - require.ErrorContains(t, err, "Wasm file does not exist") -} - -func TestStoreCodeFailsWithBadData(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - - wasm := []byte("some invalid data") - _, err := StoreCode(cache, wasm) - require.Error(t, err) -} - -func TestStoreCodeUnchecked(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - - wasm, err := os.ReadFile("../../testdata/hackatom.wasm") - require.NoError(t, err) - - checksum, err := StoreCodeUnchecked(cache, wasm) - require.NoError(t, err) - expectedChecksum := sha256.Sum256(wasm) - require.Equal(t, expectedChecksum[:], checksum) - - code, err := GetCode(cache, checksum) - require.NoError(t, err) - require.Equal(t, wasm, code) -} - -func TestPin(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - - wasm, err := os.ReadFile("../../testdata/hackatom.wasm") - require.NoError(t, err) - - checksum, err := StoreCode(cache, wasm) - require.NoError(t, err) - - err = Pin(cache, checksum) - require.NoError(t, err) - - // Can be called again with no effect - err = Pin(cache, checksum) - require.NoError(t, err) -} - -func TestPinErrors(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - var err error - - // Nil checksum (errors in wasmvm Rust code) - var nilChecksum []byte - err = Pin(cache, nilChecksum) - require.ErrorContains(t, err, "Null/Nil argument: checksum") - - // Checksum too short (errors in wasmvm Rust code) - brokenChecksum := []byte{0x3f, 0xd7, 0x5a, 0x76} - err = Pin(cache, brokenChecksum) - require.ErrorContains(t, err, "Checksum not of length 32") - - // Unknown checksum (errors in cosmwasm-vm) - unknownChecksum := []byte{ - 0x72, 0x2c, 0x8c, 0x99, 0x3f, 0xd7, 0x5a, 0x76, 0x27, 0xd6, 0x9e, 0xd9, 0x41, 0x34, - 0x4f, 0xe2, 0xa1, 0x42, 0x3a, 0x3e, 0x75, 0xef, 0xd3, 0xe6, 0x77, 0x8a, 0x14, 0x28, - 0x84, 0x22, 0x71, 0x04, - } - err = Pin(cache, unknownChecksum) - require.ErrorContains(t, err, "Error opening Wasm file for reading") -} - -func TestUnpin(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - - wasm, err := os.ReadFile("../../testdata/hackatom.wasm") - require.NoError(t, err) - - checksum, err := StoreCode(cache, wasm) - require.NoError(t, err) - - err = Pin(cache, checksum) - require.NoError(t, err) - - err = Unpin(cache, checksum) - require.NoError(t, err) - - // Can be called again with no effect - err = Unpin(cache, checksum) - require.NoError(t, err) -} - -func TestUnpinErrors(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - var err error - - // Nil checksum (errors in wasmvm Rust code) - var nilChecksum []byte - err = Unpin(cache, nilChecksum) - require.ErrorContains(t, err, "Null/Nil argument: checksum") - - // Checksum too short (errors in wasmvm Rust code) - brokenChecksum := []byte{0x3f, 0xd7, 0x5a, 0x76} - err = Unpin(cache, brokenChecksum) - require.ErrorContains(t, err, "Checksum not of length 32") - - // No error case triggered in cosmwasm-vm is known right now -} - -func TestGetMetrics(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - - // GetMetrics 1 - metrics, err := GetMetrics(cache) - require.NoError(t, err) - assert.Equal(t, &types.Metrics{}, metrics) - - // Store contract - wasm, err := os.ReadFile("../../testdata/hackatom.wasm") - require.NoError(t, err) - checksum, err := StoreCode(cache, wasm) - require.NoError(t, err) - - // GetMetrics 2 - metrics, err = GetMetrics(cache) - require.NoError(t, err) - assert.Equal(t, &types.Metrics{}, metrics) - - // Instantiate 1 - gasMeter := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter := types.GasMeter(gasMeter) - store := NewLookup(gasMeter) - api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Array[types.Coin]{types.NewCoin(100, "ATOM")}) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - msg1 := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) - _, _, err = Instantiate(cache, checksum, env, info, msg1, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - - // GetMetrics 3 - metrics, err = GetMetrics(cache) - assert.NoError(t, err) - require.Equal(t, uint32(0), metrics.HitsMemoryCache) - require.Equal(t, uint32(1), metrics.HitsFsCache) - require.Equal(t, uint64(1), metrics.ElementsMemoryCache) - require.InEpsilon(t, 3700000, metrics.SizeMemoryCache, 0.25) - - // Instantiate 2 - msg2 := []byte(`{"verifier": "fred", "beneficiary": "susi"}`) - _, _, err = Instantiate(cache, checksum, env, info, msg2, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - - // GetMetrics 4 - metrics, err = GetMetrics(cache) - assert.NoError(t, err) - require.Equal(t, uint32(1), metrics.HitsMemoryCache) - require.Equal(t, uint32(1), metrics.HitsFsCache) - require.Equal(t, uint64(1), metrics.ElementsMemoryCache) - require.InEpsilon(t, 3700000, metrics.SizeMemoryCache, 0.25) - - // Pin - err = Pin(cache, checksum) - require.NoError(t, err) - - // GetMetrics 5 - metrics, err = GetMetrics(cache) - assert.NoError(t, err) - require.Equal(t, uint32(1), metrics.HitsMemoryCache) - require.Equal(t, uint32(2), metrics.HitsFsCache) - require.Equal(t, uint64(1), metrics.ElementsPinnedMemoryCache) - require.Equal(t, uint64(1), metrics.ElementsMemoryCache) - require.InEpsilon(t, 3700000, metrics.SizePinnedMemoryCache, 0.25) - require.InEpsilon(t, 3700000, metrics.SizeMemoryCache, 0.25) - - // Instantiate 3 - msg3 := []byte(`{"verifier": "fred", "beneficiary": "bert"}`) - _, _, err = Instantiate(cache, checksum, env, info, msg3, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - - // GetMetrics 6 - metrics, err = GetMetrics(cache) - assert.NoError(t, err) - require.Equal(t, uint32(1), metrics.HitsPinnedMemoryCache) - require.Equal(t, uint32(1), metrics.HitsMemoryCache) - require.Equal(t, uint32(2), metrics.HitsFsCache) - require.Equal(t, uint64(1), metrics.ElementsPinnedMemoryCache) - require.Equal(t, uint64(1), metrics.ElementsMemoryCache) - require.InEpsilon(t, 3700000, metrics.SizePinnedMemoryCache, 0.25) - require.InEpsilon(t, 3700000, metrics.SizeMemoryCache, 0.25) - - // Unpin - err = Unpin(cache, checksum) - require.NoError(t, err) - - // GetMetrics 7 - metrics, err = GetMetrics(cache) - assert.NoError(t, err) - require.Equal(t, uint32(1), metrics.HitsPinnedMemoryCache) - require.Equal(t, uint32(1), metrics.HitsMemoryCache) - require.Equal(t, uint32(2), metrics.HitsFsCache) - require.Equal(t, uint64(0), metrics.ElementsPinnedMemoryCache) - require.Equal(t, uint64(1), metrics.ElementsMemoryCache) - require.Equal(t, uint64(0), metrics.SizePinnedMemoryCache) - require.InEpsilon(t, 3700000, metrics.SizeMemoryCache, 0.25) - - // Instantiate 4 - msg4 := []byte(`{"verifier": "fred", "beneficiary": "jeff"}`) - _, _, err = Instantiate(cache, checksum, env, info, msg4, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - - // GetMetrics 8 - metrics, err = GetMetrics(cache) - assert.NoError(t, err) - require.Equal(t, uint32(1), metrics.HitsPinnedMemoryCache) - require.Equal(t, uint32(2), metrics.HitsMemoryCache) - require.Equal(t, uint32(2), metrics.HitsFsCache) - require.Equal(t, uint64(0), metrics.ElementsPinnedMemoryCache) - require.Equal(t, uint64(1), metrics.ElementsMemoryCache) - require.Equal(t, uint64(0), metrics.SizePinnedMemoryCache) - require.InEpsilon(t, 3700000, metrics.SizeMemoryCache, 0.25) -} - -func TestGetPinnedMetrics(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - - // GetMetrics 1 - metrics, err := GetPinnedMetrics(cache) - require.NoError(t, err) - assert.Equal(t, &types.PinnedMetrics{PerModule: make([]types.PerModuleEntry, 0)}, metrics) - - // Store contract 1 - wasm, err := os.ReadFile("../../testdata/hackatom.wasm") - require.NoError(t, err) - checksum, err := StoreCode(cache, wasm) - require.NoError(t, err) - - err = Pin(cache, checksum) - require.NoError(t, err) - - // Store contract 2 - cyberpunkWasm, err := os.ReadFile("../../testdata/cyberpunk.wasm") - require.NoError(t, err) - cyberpunkChecksum, err := StoreCode(cache, cyberpunkWasm) - require.NoError(t, err) - - err = Pin(cache, cyberpunkChecksum) - require.NoError(t, err) - - findMetrics := func(list []types.PerModuleEntry, checksum types.Checksum) *types.PerModuleMetrics { - found := (*types.PerModuleMetrics)(nil) - - for _, structure := range list { - if bytes.Equal(structure.Checksum, checksum) { - found = &structure.Metrics - break - } - } - - return found - } - - // GetMetrics 2 - metrics, err = GetPinnedMetrics(cache) - require.NoError(t, err) - assert.Equal(t, 2, len(metrics.PerModule)) - - hackatomMetrics := findMetrics(metrics.PerModule, checksum) - cyberpunkMetrics := findMetrics(metrics.PerModule, cyberpunkChecksum) - - assert.Equal(t, uint32(0), hackatomMetrics.Hits) - assert.NotEqual(t, uint32(0), hackatomMetrics.Size) - assert.Equal(t, uint32(0), cyberpunkMetrics.Hits) - assert.NotEqual(t, uint32(0), cyberpunkMetrics.Size) - - // Instantiate 1 - gasMeter := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter := types.GasMeter(gasMeter) - store := NewLookup(gasMeter) - api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Array[types.Coin]{types.NewCoin(100, "ATOM")}) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - msg1 := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) - _, _, err = Instantiate(cache, checksum, env, info, msg1, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - - // GetMetrics 3 - metrics, err = GetPinnedMetrics(cache) - require.NoError(t, err) - assert.Equal(t, 2, len(metrics.PerModule)) - - hackatomMetrics = findMetrics(metrics.PerModule, checksum) - cyberpunkMetrics = findMetrics(metrics.PerModule, cyberpunkChecksum) - - assert.Equal(t, uint32(1), hackatomMetrics.Hits) - assert.NotEqual(t, uint32(0), hackatomMetrics.Size) - assert.Equal(t, uint32(0), cyberpunkMetrics.Hits) - assert.NotEqual(t, uint32(0), cyberpunkMetrics.Size) -} - -func TestInstantiate(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - - // create contract - wasm, err := os.ReadFile("../../testdata/hackatom.wasm") - require.NoError(t, err) - checksum, err := StoreCode(cache, wasm) - require.NoError(t, err) - - gasMeter := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter := types.GasMeter(gasMeter) - // instantiate it with this store - store := NewLookup(gasMeter) - api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Array[types.Coin]{types.NewCoin(100, "ATOM")}) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - msg := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) - - res, cost, err := Instantiate(cache, checksum, env, info, msg, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireOkResponse(t, res, 0) - assert.Equal(t, uint64(0xb1fe27), cost.UsedInternally) - - var result types.ContractResult - err = json.Unmarshal(res, &result) - require.NoError(t, err) - require.Equal(t, "", result.Err) - require.Equal(t, 0, len(result.Ok.Messages)) -} - -func TestExecute(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - checksum := createHackatomContract(t, cache) - - gasMeter1 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter1 := types.GasMeter(gasMeter1) - // instantiate it with this store - store := NewLookup(gasMeter1) - api := NewMockAPI() - balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, balance) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - - msg := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) - - start := time.Now() - res, cost, err := Instantiate(cache, checksum, env, info, msg, &igasMeter1, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - diff := time.Since(start) - require.NoError(t, err) - requireOkResponse(t, res, 0) - assert.Equal(t, uint64(0xb1fe27), cost.UsedInternally) - t.Logf("Time (%d gas): %s\n", cost.UsedInternally, diff) - - // execute with the same store - gasMeter2 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter2 := types.GasMeter(gasMeter2) - store.SetGasMeter(gasMeter2) - env = MockEnvBin(t) - info = MockInfoBin(t, "fred") - start = time.Now() - res, cost, err = Execute(cache, checksum, env, info, []byte(`{"release":{}}`), &igasMeter2, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - diff = time.Since(start) - require.NoError(t, err) - assert.Equal(t, uint64(0x1416da5), cost.UsedInternally) - t.Logf("Time (%d gas): %s\n", cost.UsedInternally, diff) - - // make sure it read the balance properly and we got 250 atoms - var result types.ContractResult - err = json.Unmarshal(res, &result) - require.NoError(t, err) - require.Equal(t, "", result.Err) - require.Equal(t, 1, len(result.Ok.Messages)) - - // Ensure we got our custom event - assert.Equal(t, len(result.Ok.Events), 1) - ev := result.Ok.Events[0] - assert.Equal(t, ev.Type, "hackatom") - assert.Equal(t, len(ev.Attributes), 1) - assert.Equal(t, ev.Attributes[0].Key, "action") - assert.Equal(t, ev.Attributes[0].Value, "release") - - dispatch := result.Ok.Messages[0].Msg - require.NotNil(t, dispatch.Bank, "%#v", dispatch) - require.NotNil(t, dispatch.Bank.Send, "%#v", dispatch) - send := dispatch.Bank.Send - assert.Equal(t, "bob", send.ToAddress) - assert.Equal(t, balance, send.Amount) - // check the data is properly formatted - expectedData := []byte{0xF0, 0x0B, 0xAA} - assert.Equal(t, expectedData, result.Ok.Data) -} - -func TestExecutePanic(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - checksum := createCyberpunkContract(t, cache) - - maxGas := TESTING_GAS_LIMIT - gasMeter1 := NewMockGasMeter(maxGas) - igasMeter1 := types.GasMeter(gasMeter1) - // instantiate it with this store - store := NewLookup(gasMeter1) - api := NewMockAPI() - balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, balance) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - - res, _, err := Instantiate(cache, checksum, env, info, []byte(`{}`), &igasMeter1, store, api, &querier, maxGas, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireOkResponse(t, res, 0) - - // execute a panic - gasMeter2 := NewMockGasMeter(maxGas) - igasMeter2 := types.GasMeter(gasMeter2) - store.SetGasMeter(gasMeter2) - info = MockInfoBin(t, "fred") - _, _, err = Execute(cache, checksum, env, info, []byte(`{"panic":{}}`), &igasMeter2, store, api, &querier, maxGas, TESTING_PRINT_DEBUG) - require.ErrorContains(t, err, "RuntimeError: Aborted: panicked at 'This page intentionally faulted'") -} - -func TestExecuteUnreachable(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - checksum := createCyberpunkContract(t, cache) - - maxGas := TESTING_GAS_LIMIT - gasMeter1 := NewMockGasMeter(maxGas) - igasMeter1 := types.GasMeter(gasMeter1) - // instantiate it with this store - store := NewLookup(gasMeter1) - api := NewMockAPI() - balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, balance) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - - res, _, err := Instantiate(cache, checksum, env, info, []byte(`{}`), &igasMeter1, store, api, &querier, maxGas, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireOkResponse(t, res, 0) - - // execute a panic - gasMeter2 := NewMockGasMeter(maxGas) - igasMeter2 := types.GasMeter(gasMeter2) - store.SetGasMeter(gasMeter2) - info = MockInfoBin(t, "fred") - _, _, err = Execute(cache, checksum, env, info, []byte(`{"unreachable":{}}`), &igasMeter2, store, api, &querier, maxGas, TESTING_PRINT_DEBUG) - require.ErrorContains(t, err, "RuntimeError: unreachable") -} - -func TestExecuteCpuLoop(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - checksum := createCyberpunkContract(t, cache) - - gasMeter1 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter1 := types.GasMeter(gasMeter1) - // instantiate it with this store - store := NewLookup(gasMeter1) - api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, nil) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - - msg := []byte(`{}`) - - start := time.Now() - res, cost, err := Instantiate(cache, checksum, env, info, msg, &igasMeter1, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - diff := time.Since(start) - require.NoError(t, err) - requireOkResponse(t, res, 0) - assert.Equal(t, uint64(0x79f527), cost.UsedInternally) - t.Logf("Time (%d gas): %s\n", cost.UsedInternally, diff) - - // execute a cpu loop - maxGas := uint64(40_000_000) - gasMeter2 := NewMockGasMeter(maxGas) - igasMeter2 := types.GasMeter(gasMeter2) - store.SetGasMeter(gasMeter2) - info = MockInfoBin(t, "fred") - start = time.Now() - _, cost, err = Execute(cache, checksum, env, info, []byte(`{"cpu_loop":{}}`), &igasMeter2, store, api, &querier, maxGas, TESTING_PRINT_DEBUG) - diff = time.Since(start) - require.Error(t, err) - assert.Equal(t, cost.UsedInternally, maxGas) - t.Logf("CPULoop Time (%d gas): %s\n", cost.UsedInternally, diff) -} - -func TestExecuteStorageLoop(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - checksum := createCyberpunkContract(t, cache) - - gasMeter1 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter1 := types.GasMeter(gasMeter1) - // instantiate it with this store - store := NewLookup(gasMeter1) - api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, nil) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - - msg := []byte(`{}`) - - res, _, err := Instantiate(cache, checksum, env, info, msg, &igasMeter1, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireOkResponse(t, res, 0) - - // execute a storage loop - maxGas := uint64(40_000_000) - gasMeter2 := NewMockGasMeter(maxGas) - igasMeter2 := types.GasMeter(gasMeter2) - store.SetGasMeter(gasMeter2) - info = MockInfoBin(t, "fred") - start := time.Now() - _, gasReport, err := Execute(cache, checksum, env, info, []byte(`{"storage_loop":{}}`), &igasMeter2, store, api, &querier, maxGas, TESTING_PRINT_DEBUG) - diff := time.Since(start) - require.Error(t, err) - t.Logf("StorageLoop Time (%d gas): %s\n", gasReport.UsedInternally, diff) - t.Logf("Gas used: %d\n", gasMeter2.GasConsumed()) - t.Logf("Wasm gas: %d\n", gasReport.UsedInternally) - - // the "sdk gas" * GasMultiplier + the wasm cost should equal the maxGas (or be very close) - totalCost := gasReport.UsedInternally + gasMeter2.GasConsumed() - require.Equal(t, int64(maxGas), int64(totalCost)) -} - -func BenchmarkContractCall(b *testing.B) { - cache, cleanup := withCache(b) - defer cleanup() - - checksum := createCyberpunkContract(b, cache) - - gasMeter1 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter1 := types.GasMeter(gasMeter1) - // instantiate it with this store - store := NewLookup(gasMeter1) - api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, nil) - env := MockEnvBin(b) - info := MockInfoBin(b, "creator") - - msg := []byte(`{}`) - - res, _, err := Instantiate(cache, checksum, env, info, msg, &igasMeter1, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(b, err) - requireOkResponse(b, res, 0) - - b.ResetTimer() - for n := 0; n < b.N; n++ { - gasMeter2 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter2 := types.GasMeter(gasMeter2) - store.SetGasMeter(gasMeter2) - info = MockInfoBin(b, "fred") - msg := []byte(`{"allocate_large_memory":{"pages":0}}`) // replace with noop once we have it - res, _, err = Execute(cache, checksum, env, info, msg, &igasMeter2, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(b, err) - requireOkResponse(b, res, 0) - } -} - -func Benchmark100ConcurrentContractCalls(b *testing.B) { - cache, cleanup := withCache(b) - defer cleanup() - - checksum := createCyberpunkContract(b, cache) - - gasMeter1 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter1 := types.GasMeter(gasMeter1) - // instantiate it with this store - store := NewLookup(gasMeter1) - api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, nil) - env := MockEnvBin(b) - info := MockInfoBin(b, "creator") - - msg := []byte(`{}`) - - res, _, err := Instantiate(cache, checksum, env, info, msg, &igasMeter1, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(b, err) - requireOkResponse(b, res, 0) - - const callCount = 100 // Calls per benchmark iteration - - b.ResetTimer() - for n := 0; n < b.N; n++ { - var wg sync.WaitGroup - wg.Add(callCount) - for i := 0; i < callCount; i++ { - go func() { - gasMeter2 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter2 := types.GasMeter(gasMeter2) - store.SetGasMeter(gasMeter2) - info = MockInfoBin(b, "fred") - msg := []byte(`{"allocate_large_memory":{"pages":0}}`) // replace with noop once we have it - res, _, err = Execute(cache, checksum, env, info, msg, &igasMeter2, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(b, err) - requireOkResponse(b, res, 0) - - wg.Done() - }() - } - wg.Wait() - } -} - -func TestExecuteUserErrorsInApiCalls(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - checksum := createHackatomContract(t, cache) - - maxGas := TESTING_GAS_LIMIT - gasMeter1 := NewMockGasMeter(maxGas) - igasMeter1 := types.GasMeter(gasMeter1) - // instantiate it with this store - store := NewLookup(gasMeter1) - balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, balance) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - - defaultApi := NewMockAPI() - msg := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) - res, _, err := Instantiate(cache, checksum, env, info, msg, &igasMeter1, store, defaultApi, &querier, maxGas, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireOkResponse(t, res, 0) - - gasMeter2 := NewMockGasMeter(maxGas) - igasMeter2 := types.GasMeter(gasMeter2) - store.SetGasMeter(gasMeter2) - info = MockInfoBin(t, "fred") - failingApi := NewMockFailureAPI() - res, _, err = Execute(cache, checksum, env, info, []byte(`{"user_errors_in_api_calls":{}}`), &igasMeter2, store, failingApi, &querier, maxGas, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireOkResponse(t, res, 0) -} - -func TestMigrate(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - checksum := createHackatomContract(t, cache) - - gasMeter := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter := types.GasMeter(gasMeter) - // instantiate it with this store - store := NewLookup(gasMeter) - api := NewMockAPI() - balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, balance) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - msg := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) - - res, _, err := Instantiate(cache, checksum, env, info, msg, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireOkResponse(t, res, 0) - - // verifier is fred - query := []byte(`{"verifier":{}}`) - data, _, err := Query(cache, checksum, env, query, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - var qResult types.QueryResult - err = json.Unmarshal(data, &qResult) - require.NoError(t, err) - require.Equal(t, "", qResult.Err) - require.Equal(t, string(qResult.Ok), `{"verifier":"fred"}`) - - // migrate to a new verifier - alice - // we use the same code blob as we are testing hackatom self-migration - _, _, err = Migrate(cache, checksum, env, []byte(`{"verifier":"alice"}`), &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - - // should update verifier to alice - data, _, err = Query(cache, checksum, env, query, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - var qResult2 types.QueryResult - err = json.Unmarshal(data, &qResult2) - require.NoError(t, err) - require.Equal(t, "", qResult2.Err) - require.Equal(t, `{"verifier":"alice"}`, string(qResult2.Ok)) -} - -func TestMultipleInstances(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - checksum := createHackatomContract(t, cache) - - // instance1 controlled by fred - gasMeter1 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter1 := types.GasMeter(gasMeter1) - store1 := NewLookup(gasMeter1) - api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Array[types.Coin]{types.NewCoin(100, "ATOM")}) - env := MockEnvBin(t) - info := MockInfoBin(t, "regen") - msg := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) - res, cost, err := Instantiate(cache, checksum, env, info, msg, &igasMeter1, store1, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireOkResponse(t, res, 0) - // we now count wasm gas charges and db writes - assert.Equal(t, uint64(0xb0c2cd), cost.UsedInternally) - - // instance2 controlled by mary - gasMeter2 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter2 := types.GasMeter(gasMeter2) - store2 := NewLookup(gasMeter2) - info = MockInfoBin(t, "chrous") - msg = []byte(`{"verifier": "mary", "beneficiary": "sue"}`) - res, cost, err = Instantiate(cache, checksum, env, info, msg, &igasMeter2, store2, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireOkResponse(t, res, 0) - assert.Equal(t, uint64(0xb1760a), cost.UsedInternally) - - // fail to execute store1 with mary - resp := exec(t, cache, checksum, "mary", store1, api, querier, 0xa7c5ce) - require.Equal(t, "Unauthorized", resp.Err) - - // succeed to execute store1 with fred - resp = exec(t, cache, checksum, "fred", store1, api, querier, 0x140e8ad) - require.Equal(t, "", resp.Err) - require.Equal(t, 1, len(resp.Ok.Messages)) - attributes := resp.Ok.Attributes - require.Equal(t, 2, len(attributes)) - require.Equal(t, "destination", attributes[1].Key) - require.Equal(t, "bob", attributes[1].Value) - - // succeed to execute store2 with mary - resp = exec(t, cache, checksum, "mary", store2, api, querier, 0x1412b29) - require.Equal(t, "", resp.Err) - require.Equal(t, 1, len(resp.Ok.Messages)) - attributes = resp.Ok.Attributes - require.Equal(t, 2, len(attributes)) - require.Equal(t, "destination", attributes[1].Key) - require.Equal(t, "sue", attributes[1].Value) -} - -func TestSudo(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - checksum := createHackatomContract(t, cache) - - gasMeter1 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter1 := types.GasMeter(gasMeter1) - // instantiate it with this store - store := NewLookup(gasMeter1) - api := NewMockAPI() - balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, balance) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - - msg := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) - res, _, err := Instantiate(cache, checksum, env, info, msg, &igasMeter1, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireOkResponse(t, res, 0) - - // call sudo with same store - gasMeter2 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter2 := types.GasMeter(gasMeter2) - store.SetGasMeter(gasMeter2) - env = MockEnvBin(t) - msg = []byte(`{"steal_funds":{"recipient":"community-pool","amount":[{"amount":"700","denom":"gold"}]}}`) - res, _, err = Sudo(cache, checksum, env, msg, &igasMeter2, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - - // make sure it blindly followed orders - var result types.ContractResult - err = json.Unmarshal(res, &result) - require.NoError(t, err) - require.Equal(t, "", result.Err) - require.Equal(t, 1, len(result.Ok.Messages)) - dispatch := result.Ok.Messages[0].Msg - require.NotNil(t, dispatch.Bank, "%#v", dispatch) - require.NotNil(t, dispatch.Bank.Send, "%#v", dispatch) - send := dispatch.Bank.Send - assert.Equal(t, "community-pool", send.ToAddress) - expectedPayout := types.Array[types.Coin]{types.NewCoin(700, "gold")} - assert.Equal(t, expectedPayout, send.Amount) -} - -func TestDispatchSubmessage(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - checksum := createReflectContract(t, cache) - - gasMeter1 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter1 := types.GasMeter(gasMeter1) - // instantiate it with this store - store := NewLookup(gasMeter1) - api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, nil) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - - msg := []byte(`{}`) - res, _, err := Instantiate(cache, checksum, env, info, msg, &igasMeter1, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireOkResponse(t, res, 0) - - // dispatch a submessage - var id uint64 = 1234 - payload := types.SubMsg{ - ID: id, - Msg: types.CosmosMsg{Bank: &types.BankMsg{Send: &types.SendMsg{ - ToAddress: "friend", - Amount: types.Array[types.Coin]{types.NewCoin(1, "token")}, - }}}, - ReplyOn: types.ReplyAlways, - } - payloadBin, err := json.Marshal(payload) - require.NoError(t, err) - payloadMsg := []byte(fmt.Sprintf(`{"reflect_sub_msg":{"msgs":[%s]}}`, string(payloadBin))) - - gasMeter2 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter2 := types.GasMeter(gasMeter2) - store.SetGasMeter(gasMeter2) - env = MockEnvBin(t) - res, _, err = Execute(cache, checksum, env, info, payloadMsg, &igasMeter2, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - - // make sure it blindly followed orders - var result types.ContractResult - err = json.Unmarshal(res, &result) - require.NoError(t, err) - require.Equal(t, "", result.Err) - require.Equal(t, 1, len(result.Ok.Messages)) - dispatch := result.Ok.Messages[0] - assert.Equal(t, id, dispatch.ID) - assert.Equal(t, payload.Msg, dispatch.Msg) - assert.Nil(t, dispatch.GasLimit) - assert.Equal(t, payload.ReplyOn, dispatch.ReplyOn) -} - -func TestReplyAndQuery(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - checksum := createReflectContract(t, cache) - - gasMeter1 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter1 := types.GasMeter(gasMeter1) - // instantiate it with this store - store := NewLookup(gasMeter1) - api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, nil) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - - msg := []byte(`{}`) - res, _, err := Instantiate(cache, checksum, env, info, msg, &igasMeter1, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireOkResponse(t, res, 0) - - var id uint64 = 1234 - data := []byte("foobar") - events := types.Array[types.Event]{{ - Type: "message", - Attributes: types.Array[types.EventAttribute]{{ - Key: "signer", - Value: "caller-addr", - }}, - }} - reply := types.Reply{ - ID: id, - Result: types.SubMsgResult{ - Ok: &types.SubMsgResponse{ - Events: events, - Data: data, - }, - }, - } - replyBin, err := json.Marshal(reply) - require.NoError(t, err) - - gasMeter2 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter2 := types.GasMeter(gasMeter2) - store.SetGasMeter(gasMeter2) - env = MockEnvBin(t) - res, _, err = Reply(cache, checksum, env, replyBin, &igasMeter2, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireOkResponse(t, res, 0) - - // now query the state to see if it stored the data properly - badQuery := []byte(`{"sub_msg_result":{"id":7777}}`) - res, _, err = Query(cache, checksum, env, badQuery, &igasMeter2, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - requireQueryError(t, res) - - query := []byte(`{"sub_msg_result":{"id":1234}}`) - res, _, err = Query(cache, checksum, env, query, &igasMeter2, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - qResult := requireQueryOk(t, res) - - var stored types.Reply - err = json.Unmarshal(qResult, &stored) - require.NoError(t, err) - assert.Equal(t, id, stored.ID) - require.NotNil(t, stored.Result.Ok) - val := stored.Result.Ok - require.Equal(t, data, val.Data) - require.Equal(t, events, val.Events) -} - -func requireOkResponse(t testing.TB, res []byte, expectedMsgs int) { - var result types.ContractResult - err := json.Unmarshal(res, &result) - require.NoError(t, err) - require.Equal(t, "", result.Err) - require.Equal(t, expectedMsgs, len(result.Ok.Messages)) -} - -func requireQueryError(t *testing.T, res []byte) { - var result types.QueryResult - err := json.Unmarshal(res, &result) - require.NoError(t, err) - require.Empty(t, result.Ok) - require.NotEmpty(t, result.Err) -} - -func requireQueryOk(t *testing.T, res []byte) []byte { - var result types.QueryResult - err := json.Unmarshal(res, &result) - require.NoError(t, err) - require.Empty(t, result.Err) - require.NotEmpty(t, result.Ok) - return result.Ok -} - -func createHackatomContract(t testing.TB, cache Cache) []byte { - return createContract(t, cache, "../../testdata/hackatom.wasm") -} - -func createCyberpunkContract(t testing.TB, cache Cache) []byte { - return createContract(t, cache, "../../testdata/cyberpunk.wasm") -} - -func createQueueContract(t testing.TB, cache Cache) []byte { - return createContract(t, cache, "../../testdata/queue.wasm") -} - -func createReflectContract(t testing.TB, cache Cache) []byte { - return createContract(t, cache, "../../testdata/reflect.wasm") -} - -func createFloaty2(t testing.TB, cache Cache) []byte { - return createContract(t, cache, "../../testdata/floaty_2.0.wasm") -} - -func createContract(t testing.TB, cache Cache, wasmFile string) []byte { - wasm, err := os.ReadFile(wasmFile) - require.NoError(t, err) - checksum, err := StoreCode(cache, wasm) - require.NoError(t, err) - return checksum -} - -// exec runs the handle tx with the given signer -func exec(t *testing.T, cache Cache, checksum []byte, signer types.HumanAddress, store types.KVStore, api *types.GoAPI, querier Querier, gasExpected uint64) types.ContractResult { - gasMeter := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter := types.GasMeter(gasMeter) - env := MockEnvBin(t) - info := MockInfoBin(t, signer) - res, cost, err := Execute(cache, checksum, env, info, []byte(`{"release":{}}`), &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - assert.Equal(t, gasExpected, cost.UsedInternally) - - var result types.ContractResult - err = json.Unmarshal(res, &result) - require.NoError(t, err) - return result -} - -func TestQuery(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - checksum := createHackatomContract(t, cache) - - // set up contract - gasMeter1 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter1 := types.GasMeter(gasMeter1) - store := NewLookup(gasMeter1) - api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, types.Array[types.Coin]{types.NewCoin(100, "ATOM")}) - env := MockEnvBin(t) - info := MockInfoBin(t, "creator") - msg := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) - _, _, err := Instantiate(cache, checksum, env, info, msg, &igasMeter1, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - - // invalid query - gasMeter2 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter2 := types.GasMeter(gasMeter2) - store.SetGasMeter(gasMeter2) - query := []byte(`{"Raw":{"val":"config"}}`) - data, _, err := Query(cache, checksum, env, query, &igasMeter2, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - var badResult types.QueryResult - err = json.Unmarshal(data, &badResult) - require.NoError(t, err) - require.Contains(t, badResult.Err, "Error parsing into type hackatom::msg::QueryMsg: unknown variant `Raw`, expected one of") - - // make a valid query - gasMeter3 := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter3 := types.GasMeter(gasMeter3) - store.SetGasMeter(gasMeter3) - query = []byte(`{"verifier":{}}`) - data, _, err = Query(cache, checksum, env, query, &igasMeter3, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - var qResult types.QueryResult - err = json.Unmarshal(data, &qResult) - require.NoError(t, err) - require.Equal(t, "", qResult.Err) - require.Equal(t, string(qResult.Ok), `{"verifier":"fred"}`) -} - -func TestHackatomQuerier(t *testing.T) { - cache, cleanup := withCache(t) - defer cleanup() - checksum := createHackatomContract(t, cache) - - // set up contract - gasMeter := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter := types.GasMeter(gasMeter) - store := NewLookup(gasMeter) - api := NewMockAPI() - initBalance := types.Array[types.Coin]{types.NewCoin(1234, "ATOM"), types.NewCoin(65432, "ETH")} - querier := DefaultQuerier("foobar", initBalance) - - // make a valid query to the other address - query := []byte(`{"other_balance":{"address":"foobar"}}`) - // TODO The query happens before the contract is initialized. How is this legal? - env := MockEnvBin(t) - data, _, err := Query(cache, checksum, env, query, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - var qResult types.QueryResult - err = json.Unmarshal(data, &qResult) - require.NoError(t, err) - require.Equal(t, "", qResult.Err) - var balances types.AllBalancesResponse - err = json.Unmarshal(qResult.Ok, &balances) - require.NoError(t, err) - require.Equal(t, balances.Amount, initBalance) -} - -func TestCustomReflectQuerier(t *testing.T) { - type CapitalizedQuery struct { - Text string `json:"text"` - } - - type QueryMsg struct { - Capitalized *CapitalizedQuery `json:"capitalized,omitempty"` - // There are more queries but we don't use them yet - // https://github.com/CosmWasm/cosmwasm/blob/v0.11.0-alpha3/contracts/reflect/src/msg.rs#L18-L28 - } - - type CapitalizedResponse struct { - Text string `json:"text"` - } - - cache, cleanup := withCache(t) - defer cleanup() - checksum := createReflectContract(t, cache) - - // set up contract - gasMeter := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter := types.GasMeter(gasMeter) - store := NewLookup(gasMeter) - api := NewMockAPI() - initBalance := types.Array[types.Coin]{types.NewCoin(1234, "ATOM")} - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, initBalance) - // we need this to handle the custom requests from the reflect contract - innerQuerier := querier.(*MockQuerier) - innerQuerier.Custom = ReflectCustom{} - querier = Querier(innerQuerier) - - // make a valid query to the other address - queryMsg := QueryMsg{ - Capitalized: &CapitalizedQuery{ - Text: "small Frys :)", - }, - } - query, err := json.Marshal(queryMsg) - require.NoError(t, err) - env := MockEnvBin(t) - data, _, err := Query(cache, checksum, env, query, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - var qResult types.QueryResult - err = json.Unmarshal(data, &qResult) - require.NoError(t, err) - require.Equal(t, "", qResult.Err) - - var response CapitalizedResponse - err = json.Unmarshal(qResult.Ok, &response) - require.NoError(t, err) - require.Equal(t, "SMALL FRYS :)", response.Text) -} - -// TestFloats is a port of the float_instrs_are_deterministic test in cosmwasm-vm -func TestFloats(t *testing.T) { - type Value struct { - U32 *uint32 `json:"u32,omitempty"` - U64 *uint64 `json:"u64,omitempty"` - F32 *uint32 `json:"f32,omitempty"` - F64 *uint64 `json:"f64,omitempty"` - } - - // helper to print the value in the same format as Rust's Debug trait - debugStr := func(value Value) string { - if value.U32 != nil { - return fmt.Sprintf("U32(%d)", *value.U32) - } else if value.U64 != nil { - return fmt.Sprintf("U64(%d)", *value.U64) - } else if value.F32 != nil { - return fmt.Sprintf("F32(%d)", *value.F32) - } else if value.F64 != nil { - return fmt.Sprintf("F64(%d)", *value.F64) - } else { - t.FailNow() - return "" - } - } - - cache, cleanup := withCache(t) - defer cleanup() - checksum := createFloaty2(t, cache) - - gasMeter := NewMockGasMeter(TESTING_GAS_LIMIT) - igasMeter := types.GasMeter(gasMeter) - // instantiate it with this store - store := NewLookup(gasMeter) - api := NewMockAPI() - querier := DefaultQuerier(MOCK_CONTRACT_ADDR, nil) - env := MockEnvBin(t) - - // query instructions - query := []byte(`{"instructions":{}}`) - data, _, err := Query(cache, checksum, env, query, &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - var qResult types.QueryResult - err = json.Unmarshal(data, &qResult) - require.NoError(t, err) - require.Equal(t, "", qResult.Err) - var instructions []string - err = json.Unmarshal(qResult.Ok, &instructions) - require.NoError(t, err) - // little sanity check - require.Equal(t, 70, len(instructions)) - - hasher := sha256.New() - const RUNS_PER_INSTRUCTION = 150 - for _, instr := range instructions { - for seed := 0; seed < RUNS_PER_INSTRUCTION; seed++ { - // query some input values for the instruction - msg := fmt.Sprintf(`{"random_args_for":{"instruction":"%s","seed":%d}}`, instr, seed) - data, _, err = Query(cache, checksum, env, []byte(msg), &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - require.NoError(t, err) - err = json.Unmarshal(data, &qResult) - require.NoError(t, err) - require.Equal(t, "", qResult.Err) - var args []Value - err = json.Unmarshal(qResult.Ok, &args) - require.NoError(t, err) - - // build the run message - argStr, err := json.Marshal(args) - require.NoError(t, err) - msg = fmt.Sprintf(`{"run":{"instruction":"%s","args":%s}}`, instr, argStr) - - // run the instruction - // this might throw a runtime error (e.g. if the instruction traps) - data, _, err = Query(cache, checksum, env, []byte(msg), &igasMeter, store, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG) - var result string - if err != nil { - assert.ErrorContains(t, err, "Error calling the VM: Error executing Wasm: ") - // remove the prefix to make the error message the same as in the cosmwasm-vm test - result = strings.Replace(err.Error(), "Error calling the VM: Error executing Wasm: ", "", 1) - } else { - err = json.Unmarshal(data, &qResult) - require.NoError(t, err) - require.Equal(t, "", qResult.Err) - var response Value - err = json.Unmarshal(qResult.Ok, &response) - require.NoError(t, err) - result = debugStr(response) - } - // add the result to the hash - hasher.Write([]byte(fmt.Sprintf("%s%d%s", instr, seed, result))) - } - } - - hash := hasher.Sum(nil) - require.Equal(t, "95f70fa6451176ab04a9594417a047a1e4d8e2ff809609b8f81099496bee2393", hex.EncodeToString(hash)) -} diff --git a/internal/api/libwasmvm.aarch64.so b/internal/api/libwasmvm.aarch64.so deleted file mode 100755 index dcefe4cb4..000000000 Binary files a/internal/api/libwasmvm.aarch64.so and /dev/null differ diff --git a/internal/api/libwasmvm.dylib b/internal/api/libwasmvm.dylib deleted file mode 100755 index 4ab1b5c03..000000000 Binary files a/internal/api/libwasmvm.dylib and /dev/null differ diff --git a/internal/api/libwasmvm.x86_64.so b/internal/api/libwasmvm.x86_64.so deleted file mode 100755 index 2aa681224..000000000 Binary files a/internal/api/libwasmvm.x86_64.so and /dev/null differ diff --git a/internal/api/link_glibclinux_aarch64.go b/internal/api/link_glibclinux_aarch64.go deleted file mode 100644 index 8742229de..000000000 --- a/internal/api/link_glibclinux_aarch64.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build linux && !muslc && arm64 && !sys_wasmvm - -package api - -// #cgo LDFLAGS: -Wl,-rpath,${SRCDIR} -L${SRCDIR} -lwasmvm.aarch64 -import "C" diff --git a/internal/api/link_glibclinux_x86_64.go b/internal/api/link_glibclinux_x86_64.go deleted file mode 100644 index 9d87a7130..000000000 --- a/internal/api/link_glibclinux_x86_64.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build linux && !muslc && amd64 && !sys_wasmvm - -package api - -// #cgo LDFLAGS: -Wl,-rpath,${SRCDIR} -L${SRCDIR} -lwasmvm.x86_64 -import "C" diff --git a/internal/api/link_mac.go b/internal/api/link_mac.go deleted file mode 100644 index e6d841ea2..000000000 --- a/internal/api/link_mac.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build darwin && !static_wasm && !sys_wasmvm - -package api - -// #cgo LDFLAGS: -Wl,-rpath,${SRCDIR} -L${SRCDIR} -lwasmvm -import "C" diff --git a/internal/api/link_mac_static.go b/internal/api/link_mac_static.go deleted file mode 100644 index d9132e519..000000000 --- a/internal/api/link_mac_static.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build darwin && static_wasm && !sys_wasmvm - -package api - -// #cgo LDFLAGS: -L${SRCDIR} -lwasmvmstatic_darwin -import "C" diff --git a/internal/api/link_muslc_aarch64.go b/internal/api/link_muslc_aarch64.go deleted file mode 100644 index e3ab74aeb..000000000 --- a/internal/api/link_muslc_aarch64.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build linux && muslc && arm64 && !sys_wasmvm - -package api - -// #cgo LDFLAGS: -Wl,-rpath,${SRCDIR} -L${SRCDIR} -lwasmvm_muslc.aarch64 -import "C" diff --git a/internal/api/link_muslc_x86_64.go b/internal/api/link_muslc_x86_64.go deleted file mode 100644 index 58489509f..000000000 --- a/internal/api/link_muslc_x86_64.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build linux && muslc && amd64 && !sys_wasmvm - -package api - -// #cgo LDFLAGS: -Wl,-rpath,${SRCDIR} -L${SRCDIR} -lwasmvm_muslc.x86_64 -import "C" diff --git a/internal/api/link_system.go b/internal/api/link_system.go deleted file mode 100644 index ad354ba55..000000000 --- a/internal/api/link_system.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build sys_wasmvm - -package api - -// #cgo LDFLAGS: -lwasmvm -import "C" diff --git a/internal/api/link_windows.go b/internal/api/link_windows.go deleted file mode 100644 index 8e45cf011..000000000 --- a/internal/api/link_windows.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build windows && !sys_wasmvm - -package api - -// #cgo LDFLAGS: -Wl,-rpath,${SRCDIR} -L${SRCDIR} -lwasmvm -import "C" diff --git a/internal/api/memory.go b/internal/api/memory.go index f2fb06d73..5080aa853 100644 --- a/internal/api/memory.go +++ b/internal/api/memory.go @@ -1,98 +1,68 @@ package api -/* -#include "bindings.h" -*/ -import "C" +import ( + "fmt" + "unsafe" +) -import "unsafe" - -// makeView creates a view into the given byte slice what allows Rust code to read it. -// The byte slice is managed by Go and will be garbage collected. Use runtime.KeepAlive -// to ensure the byte slice lives long enough. -func makeView(s []byte) C.ByteSliceView { - if s == nil { - return C.ByteSliceView{is_nil: true, ptr: cu8_ptr(nil), len: cusize(0)} - } - - // In Go, accessing the 0-th element of an empty array triggers a panic. That is why in the case - // of an empty `[]byte` we can't get the internal heap pointer to the underlying array as we do - // below with `&data[0]`. https://play.golang.org/p/xvDY3g9OqUk - if len(s) == 0 { - return C.ByteSliceView{is_nil: false, ptr: cu8_ptr(nil), len: cusize(0)} - } - - return C.ByteSliceView{ - is_nil: false, - ptr: cu8_ptr(unsafe.Pointer(&s[0])), - len: cusize(len(s)), - } +// Memory represents a block of memory in the Wasm instance +type Memory struct { + ptr unsafe.Pointer + len int } -// Creates a C.UnmanagedVector, which cannot be done in test files directly -func constructUnmanagedVector(is_none cbool, ptr cu8_ptr, len cusize, cap cusize) C.UnmanagedVector { - return C.UnmanagedVector{ - is_none: is_none, - ptr: ptr, - len: len, - cap: cap, +// NewMemory creates a new Memory instance +func NewMemory(ptr unsafe.Pointer, len int) Memory { + return Memory{ + ptr: ptr, + len: len, } } -// uninitializedUnmanagedVector returns an invalid C.UnmanagedVector -// instance. Only use then after someone wrote an instance to it. -func uninitializedUnmanagedVector() C.UnmanagedVector { - return C.UnmanagedVector{} +// ReadByte reads a single byte from memory at the given offset +func (m Memory) ReadByte(offset int) (byte, error) { + if offset < 0 || offset >= m.len { + return 0, fmt.Errorf("offset out of bounds: %d", offset) + } + return *(*byte)(unsafe.Pointer(uintptr(m.ptr) + uintptr(offset))), nil } -func newUnmanagedVector(data []byte) C.UnmanagedVector { - if data == nil { - return C.new_unmanaged_vector(cbool(true), cu8_ptr(nil), cusize(0)) - } else if len(data) == 0 { - // in Go, accessing the 0-th element of an empty array triggers a panic. That is why in the case - // of an empty `[]byte` we can't get the internal heap pointer to the underlying array as we do - // below with `&data[0]`. - // https://play.golang.org/p/xvDY3g9OqUk - return C.new_unmanaged_vector(cbool(false), cu8_ptr(nil), cusize(0)) - } else { - // This will allocate a proper vector with content and return a description of it - return C.new_unmanaged_vector(cbool(false), cu8_ptr(unsafe.Pointer(&data[0])), cusize(len(data))) +// ReadBytes reads a slice of bytes from memory at the given offset +func (m Memory) ReadBytes(offset int, length int) ([]byte, error) { + if offset < 0 || length < 0 || offset+length > m.len { + return nil, fmt.Errorf("read out of bounds: offset=%d length=%d", offset, length) } + data := make([]byte, length) + src := unsafe.Slice((*byte)(unsafe.Pointer(uintptr(m.ptr)+uintptr(offset))), length) + copy(data, src) + return data, nil } -func copyAndDestroyUnmanagedVector(v C.UnmanagedVector) []byte { - var out []byte - if v.is_none { - out = nil - } else if v.cap == cusize(0) { - // There is no allocation we can copy - out = []byte{} - } else { - // C.GoBytes create a copy (https://stackoverflow.com/a/40950744/2013738) - out = C.GoBytes(unsafe.Pointer(v.ptr), cint(v.len)) +// Write writes a slice of bytes to memory at the given offset +func (m Memory) Write(offset int, data []byte) error { + if offset < 0 || offset+len(data) > m.len { + return fmt.Errorf("write out of bounds: offset=%d length=%d", offset, len(data)) } - C.destroy_unmanaged_vector(v) - return out + dest := unsafe.Slice((*byte)(unsafe.Pointer(uintptr(m.ptr)+uintptr(offset))), len(data)) + copy(dest, data) + return nil } -func optionalU64ToPtr(val C.OptionalU64) *uint64 { - if val.is_some { - return (*uint64)(&val.value) +// WriteByte writes a single byte to memory at the given offset +func (m Memory) WriteByte(offset int, b byte) error { + if offset < 0 || offset >= m.len { + return fmt.Errorf("offset out of bounds: %d", offset) } + *(*byte)(unsafe.Pointer(uintptr(m.ptr) + uintptr(offset))) = b return nil } -// copyU8Slice copies the contents of an Option<&[u8]> that was allocated on the Rust side. -// Returns nil if and only if the source is None. -func copyU8Slice(view C.U8SliceView) []byte { - if view.is_none { - return nil - } - if view.len == 0 { - // In this case, we don't want to look into the ptr - return []byte{} - } - // C.GoBytes create a copy (https://stackoverflow.com/a/40950744/2013738) - res := C.GoBytes(unsafe.Pointer(view.ptr), cint(view.len)) - return res +// Length returns the total length of the memory block +func (m Memory) Length() int { + return m.len +} + +// Pointer returns the raw pointer to the memory block +func (m Memory) Pointer() unsafe.Pointer { + return m.ptr } diff --git a/internal/api/memory_test.go b/internal/api/memory_test.go index e1dcbfaac..b44cd029e 100644 --- a/internal/api/memory_test.go +++ b/internal/api/memory_test.go @@ -7,72 +7,67 @@ import ( "github.com/stretchr/testify/require" ) -func TestMakeView(t *testing.T) { +func TestMemory(t *testing.T) { + // Create a test buffer data := []byte{0xaa, 0xbb, 0x64} - dataView := makeView(data) - require.Equal(t, cbool(false), dataView.is_nil) - require.Equal(t, cusize(3), dataView.len) + mem := NewMemory(unsafe.Pointer(&data[0]), len(data)) - empty := []byte{} - emptyView := makeView(empty) - require.Equal(t, cbool(false), emptyView.is_nil) - require.Equal(t, cusize(0), emptyView.len) + // Test ReadByte + b, err := mem.ReadByte(0) + require.NoError(t, err) + require.Equal(t, byte(0xaa), b) - nilView := makeView(nil) - require.Equal(t, cbool(true), nilView.is_nil) -} + b, err = mem.ReadByte(1) + require.NoError(t, err) + require.Equal(t, byte(0xbb), b) -func TestCreateAndDestroyUnmanagedVector(t *testing.T) { - // non-empty - { - original := []byte{0xaa, 0xbb, 0x64} - unmanaged := newUnmanagedVector(original) - require.Equal(t, cbool(false), unmanaged.is_none) - require.Equal(t, 3, int(unmanaged.len)) - require.GreaterOrEqual(t, 3, int(unmanaged.cap)) // Rust implementation decides this - copy := copyAndDestroyUnmanagedVector(unmanaged) - require.Equal(t, original, copy) - } + // Test out of bounds + _, err = mem.ReadByte(-1) + require.Error(t, err) + _, err = mem.ReadByte(3) + require.Error(t, err) - // empty - { - original := []byte{} - unmanaged := newUnmanagedVector(original) - require.Equal(t, cbool(false), unmanaged.is_none) - require.Equal(t, 0, int(unmanaged.len)) - require.GreaterOrEqual(t, 0, int(unmanaged.cap)) // Rust implementation decides this - copy := copyAndDestroyUnmanagedVector(unmanaged) - require.Equal(t, original, copy) - } + // Test ReadBytes + bytes, err := mem.ReadBytes(0, 3) + require.NoError(t, err) + require.Equal(t, data, bytes) - // none - { - var original []byte - unmanaged := newUnmanagedVector(original) - require.Equal(t, cbool(true), unmanaged.is_none) - // We must not make assumtions on the other fields in this case - copy := copyAndDestroyUnmanagedVector(unmanaged) - require.Nil(t, copy) - } -} + // Test out of bounds + _, err = mem.ReadBytes(-1, 1) + require.Error(t, err) + _, err = mem.ReadBytes(0, 4) + require.Error(t, err) + + // Test Write + newData := []byte{0x11, 0x22, 0x33} + err = mem.Write(0, newData) + require.NoError(t, err) + bytes, err = mem.ReadBytes(0, 3) + require.NoError(t, err) + require.Equal(t, newData, bytes) + + // Test out of bounds + err = mem.Write(-1, newData) + require.Error(t, err) + err = mem.Write(1, newData) + require.Error(t, err) + + // Test WriteByte + err = mem.WriteByte(0, 0xff) + require.NoError(t, err) + b, err = mem.ReadByte(0) + require.NoError(t, err) + require.Equal(t, byte(0xff), b) + + // Test out of bounds + err = mem.WriteByte(-1, 0xff) + require.Error(t, err) + err = mem.WriteByte(3, 0xff) + require.Error(t, err) + + // Test Length + require.Equal(t, 3, mem.Length()) -// Like the test above but without `newUnmanagedVector` calls. -// Since only Rust can actually create them, we only test edge cases here. -// -//go:nocheckptr -func TestCopyDestroyUnmanagedVector(t *testing.T) { - { - // ptr, cap and len broken. Do not access those values when is_none is true - invalid_ptr := unsafe.Pointer(uintptr(42)) - uv := constructUnmanagedVector(cbool(true), cu8_ptr(invalid_ptr), cusize(0xBB), cusize(0xAA)) - copy := copyAndDestroyUnmanagedVector(uv) - require.Nil(t, copy) - } - { - // Capacity is 0, so no allocation happened. Do not access the pointer. - invalid_ptr := unsafe.Pointer(uintptr(42)) - uv := constructUnmanagedVector(cbool(false), cu8_ptr(invalid_ptr), cusize(0), cusize(0)) - copy := copyAndDestroyUnmanagedVector(uv) - require.Equal(t, []byte{}, copy) - } + // Test Pointer + require.NotNil(t, mem.Pointer()) } diff --git a/internal/api/transition.go b/internal/api/transition.go new file mode 100644 index 000000000..ce8f70220 --- /dev/null +++ b/internal/api/transition.go @@ -0,0 +1,539 @@ +package api + +import ( + "fmt" + + "github.com/CosmWasm/wasmvm/v2/types" +) + +// Instantiate creates a new instance of a contract +func Instantiate( + cache *Cache, + checksum []byte, + env []byte, + info []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *types.Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + // Load WASM code from cache + code, err := GetCode(*cache, checksum) + if err != nil { + return nil, types.GasReport{}, fmt.Errorf("failed to load code from cache: %w", err) + } + + // Build environment + envObj := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + } + + // Create VM instance + vm := NewWazeroVMWithCache(*cache) + + // Call instantiate + result, gasReport, err := vm.Execute(envObj, info, msg, gasMeter, store, api, querier, gasLimit, printDebug) + if err != nil { + return nil, gasReport, err + } + + return result, gasReport, nil +} + +// Execute creates a new instance and executes the given function +func Execute( + cache *Cache, + checksum []byte, + env []byte, + info []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *types.Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + code, err := GetCode(*cache, checksum) + if err != nil { + return nil, types.GasReport{}, fmt.Errorf("failed to load code from cache: %w", err) + } + + envObj := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + } + + vm := NewWazeroVMWithCache(*cache) + + result, gasReport, err := vm.Execute(envObj, info, msg, gasMeter, store, api, querier, gasLimit, printDebug) + if err != nil { + return nil, gasReport, err + } + + return result, gasReport, nil +} + +// Query creates a new instance and executes a query +func Query( + cache *Cache, + checksum []byte, + env []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *types.Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + code, err := GetCode(*cache, checksum) + if err != nil { + return nil, types.GasReport{}, fmt.Errorf("failed to load code from cache: %w", err) + } + + envObj := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + } + + vm := NewWazeroVMWithCache(*cache) + + result, gasReport, err := vm.Query(envObj, msg, gasMeter, store, api, querier, gasLimit, printDebug) + if err != nil { + return nil, gasReport, err + } + + return result, gasReport, nil +} + +// Migrate creates a new instance and executes a migration +func Migrate( + cache *Cache, + checksum []byte, + env []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *types.Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + code, err := GetCode(*cache, checksum) + if err != nil { + return nil, types.GasReport{}, fmt.Errorf("failed to load code from cache: %w", err) + } + + envObj := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + } + + vm := NewWazeroVMWithCache(*cache) + + result, gasReport, err := vm.Migrate(envObj, msg, gasMeter, store, api, querier, gasLimit, printDebug) + if err != nil { + return nil, gasReport, err + } + + return result, gasReport, nil +} + +// MigrateWithInfo creates a new instance and executes a migration with additional info +func MigrateWithInfo( + cache *Cache, + checksum []byte, + env []byte, + msg []byte, + migrateInfo []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *types.Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + code, err := GetCode(*cache, checksum) + if err != nil { + return nil, types.GasReport{}, fmt.Errorf("failed to load code from cache: %w", err) + } + + envObj := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + } + + vm := NewWazeroVMWithCache(*cache) + + result, gasReport, err := vm.MigrateWithInfo(envObj, msg, migrateInfo, gasMeter, store, api, querier, gasLimit, printDebug) + if err != nil { + return nil, gasReport, err + } + + return result, gasReport, nil +} + +// Sudo creates a new instance and executes a privileged operation +func Sudo( + cache *Cache, + checksum []byte, + env []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *types.Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + code, err := GetCode(*cache, checksum) + if err != nil { + return nil, types.GasReport{}, fmt.Errorf("failed to load code from cache: %w", err) + } + + envObj := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + } + + vm := NewWazeroVMWithCache(*cache) + + result, gasReport, err := vm.Sudo(envObj, msg, gasMeter, store, api, querier, gasLimit, printDebug) + if err != nil { + return nil, gasReport, err + } + + return result, gasReport, nil +} + +// Reply creates a new instance and executes a reply callback +func Reply( + cache *Cache, + checksum []byte, + env []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *types.Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + code, err := GetCode(*cache, checksum) + if err != nil { + return nil, types.GasReport{}, fmt.Errorf("failed to load code from cache: %w", err) + } + + envObj := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + } + + vm := NewWazeroVMWithCache(*cache) + + result, gasReport, err := vm.Reply(envObj, msg, gasMeter, store, api, querier, gasLimit, printDebug) + if err != nil { + return nil, gasReport, err + } + + return result, gasReport, nil +} + +// IBCChannelOpen creates a new instance and executes an IBC channel open +func IBCChannelOpen( + cache *Cache, + checksum []byte, + env []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *types.Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + code, err := GetCode(*cache, checksum) + if err != nil { + return nil, types.GasReport{}, fmt.Errorf("failed to load code from cache: %w", err) + } + + envObj := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + } + + vm := NewWazeroVMWithCache(*cache) + + result, gasReport, err := vm.IBCChannelOpen(envObj, msg, gasMeter, store, api, querier, gasLimit, printDebug) + if err != nil { + return nil, gasReport, err + } + + return result, gasReport, nil +} + +// IBCChannelConnect creates a new instance and executes an IBC channel connect +func IBCChannelConnect( + cache *Cache, + checksum []byte, + env []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *types.Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + code, err := GetCode(*cache, checksum) + if err != nil { + return nil, types.GasReport{}, fmt.Errorf("failed to load code from cache: %w", err) + } + + envObj := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + } + + vm := NewWazeroVMWithCache(*cache) + + result, gasReport, err := vm.IBCChannelConnect(envObj, msg, gasMeter, store, api, querier, gasLimit, printDebug) + if err != nil { + return nil, gasReport, err + } + + return result, gasReport, nil +} + +// IBCChannelClose creates a new instance and executes an IBC channel close +func IBCChannelClose( + cache *Cache, + checksum []byte, + env []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *types.Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + code, err := GetCode(*cache, checksum) + if err != nil { + return nil, types.GasReport{}, fmt.Errorf("failed to load code from cache: %w", err) + } + + envObj := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + } + + vm := NewWazeroVMWithCache(*cache) + + result, gasReport, err := vm.IBCChannelClose(envObj, msg, gasMeter, store, api, querier, gasLimit, printDebug) + if err != nil { + return nil, gasReport, err + } + + return result, gasReport, nil +} + +// IBCPacketReceive creates a new instance and executes an IBC packet receive +func IBCPacketReceive( + cache *Cache, + checksum []byte, + env []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *types.Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + code, err := GetCode(*cache, checksum) + if err != nil { + return nil, types.GasReport{}, fmt.Errorf("failed to load code from cache: %w", err) + } + + envObj := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + } + + vm := NewWazeroVMWithCache(*cache) + + result, gasReport, err := vm.IBCPacketReceive(envObj, msg, gasMeter, store, api, querier, gasLimit, printDebug) + if err != nil { + return nil, gasReport, err + } + + return result, gasReport, nil +} + +// IBCPacketAck creates a new instance and executes an IBC packet ack +func IBCPacketAck( + cache *Cache, + checksum []byte, + env []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *types.Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + code, err := GetCode(*cache, checksum) + if err != nil { + return nil, types.GasReport{}, fmt.Errorf("failed to load code from cache: %w", err) + } + + envObj := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + } + + vm := NewWazeroVMWithCache(*cache) + + result, gasReport, err := vm.IBCPacketAck(envObj, msg, gasMeter, store, api, querier, gasLimit, printDebug) + if err != nil { + return nil, gasReport, err + } + + return result, gasReport, nil +} + +// IBCPacketTimeout creates a new instance and executes an IBC packet timeout +func IBCPacketTimeout( + cache *Cache, + checksum []byte, + env []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *types.Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + code, err := GetCode(*cache, checksum) + if err != nil { + return nil, types.GasReport{}, fmt.Errorf("failed to load code from cache: %w", err) + } + + envObj := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + } + + vm := NewWazeroVMWithCache(*cache) + + result, gasReport, err := vm.IBCPacketTimeout(envObj, msg, gasMeter, store, api, querier, gasLimit, printDebug) + if err != nil { + return nil, gasReport, err + } + + return result, gasReport, nil +} + +// IBCSourceCallback creates a new instance and executes an IBC source callback +func IBCSourceCallback( + cache *Cache, + checksum []byte, + env []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *types.Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + code, err := GetCode(*cache, checksum) + if err != nil { + return nil, types.GasReport{}, fmt.Errorf("failed to load code from cache: %w", err) + } + + envObj := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + } + + vm := NewWazeroVMWithCache(*cache) + + result, gasReport, err := vm.IBCSourceCallback(envObj, msg, gasMeter, store, api, querier, gasLimit, printDebug) + if err != nil { + return nil, gasReport, err + } + + return result, gasReport, nil +} + +// IBCDestinationCallback creates a new instance and executes an IBC destination callback +func IBCDestinationCallback( + cache *Cache, + checksum []byte, + env []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *types.Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + code, err := GetCode(*cache, checksum) + if err != nil { + return nil, types.GasReport{}, fmt.Errorf("failed to load code from cache: %w", err) + } + + envObj := &Environment{ + Code: code, + Store: store, + API: *api, + Querier: *querier, + } + + vm := NewWazeroVMWithCache(*cache) + + result, gasReport, err := vm.IBCDestinationCallback(envObj, msg, gasMeter, store, api, querier, gasLimit, printDebug) + if err != nil { + return nil, gasReport, err + } + + return result, gasReport, nil +} diff --git a/internal/api/version.go b/internal/api/version.go index 43a13f0b9..6865bbe13 100644 --- a/internal/api/version.go +++ b/internal/api/version.go @@ -1,17 +1,9 @@ package api -/* -#include "bindings.h" -*/ -import "C" +// Version represents the version of the Wazero runtime +const Version = "v2.0.0" -func LibwasmvmVersion() (string, error) { - version_ptr, err := C.version_str() - if err != nil { - return "", err - } - // For C.GoString documentation see https://pkg.go.dev/cmd/cgo and - // https://gist.github.com/helinwang/2c7bd2867ea5110f70e6431a7c80cd9b - version_copy := C.GoString(version_ptr) - return version_copy, nil +// GetVersion returns the version of the Wazero runtime +func GetVersion() string { + return Version } diff --git a/internal/api/version_test.go b/internal/api/version_test.go index 038b1de13..25e32af4f 100644 --- a/internal/api/version_test.go +++ b/internal/api/version_test.go @@ -7,8 +7,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestLibwasmvmVersion(t *testing.T) { - version, err := LibwasmvmVersion() - require.NoError(t, err) - require.Regexp(t, regexp.MustCompile(`^([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-z0-9.]+)?$`), version) +func TestGetVersion(t *testing.T) { + version := GetVersion() + require.Regexp(t, regexp.MustCompile(`^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-z0-9.]+)?$`), version) } diff --git a/internal/api/wazero.go b/internal/api/wazero.go new file mode 100644 index 000000000..e61620bb2 --- /dev/null +++ b/internal/api/wazero.go @@ -0,0 +1,590 @@ +package api + +import ( + "context" + "encoding/binary" + "fmt" + + "github.com/CosmWasm/wasmvm/v2/types" + "github.com/tetratelabs/wazero" + wazeroapi "github.com/tetratelabs/wazero/api" +) + +// WazeroInstance represents a Wazero instance +type WazeroInstance struct { + ctx context.Context + module wazeroapi.Module + env *Environment +} + +// WazeroGasMeter implements types.GasMeter +type WazeroGasMeter struct { + consumed types.Gas +} + +func (g *WazeroGasMeter) GasConsumed() types.Gas { + return g.consumed +} + +func (g *WazeroGasMeter) ConsumeGas(amount types.Gas, reason string) { + g.consumed += amount +} + +// createModule creates a new Wazero module +func createModule(ctx context.Context, code []byte, env *Environment, gasMeter *WazeroGasMeter, gasLimit uint64) (wazeroapi.Module, error) { + // Create runtime + config := wazero.NewRuntimeConfig(). + WithMemoryLimitPages(65536). + WithCloseOnContextDone(true) + + r := wazero.NewRuntimeWithConfig(ctx, config) + + // Build host module + builder := r.NewHostModuleBuilder("env") + + // Add host functions + builder.NewFunctionBuilder(). + WithFunc(func(ctx context.Context, m wazeroapi.Module, stack []uint64) { + // db_read implementation + keyPtr := uint32(stack[0]) + keyLen := uint32(stack[1]) + valPtr := uint32(stack[2]) + + // Read key from memory + mem := m.Memory() + if mem == nil { + stack[0] = 1 // Error + return + } + + key, ok := mem.Read(keyPtr, keyLen) + if !ok { + stack[0] = 1 // Error + return + } + + // Call store.Get + value := env.Store.Get(key) + if value == nil { + stack[0] = 1 // Error + return + } + + // Write value to memory + if !mem.Write(valPtr, value) { + stack[0] = 1 // Error + return + } + + stack[0] = 0 // Success + }). + WithParameterNames("key_ptr", "key_len", "val_ptr"). + Export("db_read") + + builder.NewFunctionBuilder(). + WithFunc(func(ctx context.Context, m wazeroapi.Module, stack []uint64) { + // db_write implementation + keyPtr := uint32(stack[0]) + keyLen := uint32(stack[1]) + valPtr := uint32(stack[2]) + valLen := uint32(stack[3]) + + // Read key and value from memory + mem := m.Memory() + if mem == nil { + stack[0] = 1 // Error + return + } + + key, ok := mem.Read(keyPtr, keyLen) + if !ok { + stack[0] = 1 // Error + return + } + + value, ok := mem.Read(valPtr, valLen) + if !ok { + stack[0] = 1 // Error + return + } + + // Call store.Set + env.Store.Set(key, value) + stack[0] = 0 // Success + }). + WithParameterNames("key_ptr", "key_len", "val_ptr", "val_len"). + Export("db_write") + + builder.NewFunctionBuilder(). + WithFunc(func(ctx context.Context, m wazeroapi.Module, stack []uint64) { + // db_remove implementation + keyPtr := uint32(stack[0]) + keyLen := uint32(stack[1]) + + // Read key from memory + mem := m.Memory() + if mem == nil { + stack[0] = 1 // Error + return + } + + key, ok := mem.Read(keyPtr, keyLen) + if !ok { + stack[0] = 1 // Error + return + } + + // Call store.Delete + env.Store.Delete(key) + stack[0] = 0 // Success + }). + WithParameterNames("key_ptr", "key_len"). + Export("db_remove") + + builder.NewFunctionBuilder(). + WithFunc(func(ctx context.Context, m wazeroapi.Module, stack []uint64) { + // gas_consume implementation + amount := uint64(stack[0]) + gasMeter.ConsumeGas(amount, "wasm gas") + }). + WithParameterNames("amount"). + Export("gas_consume") + + // Create host module + if _, err := builder.Instantiate(ctx); err != nil { + return nil, fmt.Errorf("failed to create host module: %w", err) + } + + // Compile module + compiled, err := r.CompileModule(ctx, code) + if err != nil { + return nil, fmt.Errorf("failed to compile module: %w", err) + } + + // Instantiate module + module, err := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithName("env")) + if err != nil { + return nil, fmt.Errorf("failed to instantiate module: %w", err) + } + + return module, nil +} + +// NewWazeroInstance creates a new WazeroInstance +func NewWazeroInstance(env *Environment, code []byte, gasMeter *WazeroGasMeter, gasLimit uint64) (*WazeroInstance, error) { + // Initialize context + ctx := context.Background() + + // Create module + module, err := createModule(ctx, code, env, gasMeter, gasLimit) + if err != nil { + return nil, err + } + + return &WazeroInstance{ + ctx: ctx, + module: module, + env: env, + }, nil +} + +// Close releases resources associated with the instance +func (i *WazeroInstance) Close() error { + if i.module != nil { + return i.module.Close(i.ctx) + } + return nil +} + +// allocateAndWrite allocates memory and writes data to it +func (i *WazeroInstance) allocateAndWrite(data []byte) (uint64, error) { + // Allocate memory + size := uint32(len(data)) + ptr, err := i.module.ExportedFunction("allocate").Call(i.ctx, uint64(size)) + if err != nil { + return 0, err + } + + // Write data to memory + mem := i.module.Memory() + if mem == nil { + return 0, fmt.Errorf("no memory exported") + } + + if !mem.Write(uint32(ptr[0]), data) { + return 0, fmt.Errorf("failed to write to memory") + } + + return ptr[0], nil +} + +// readResult reads a result from memory +func (i *WazeroInstance) readResult(ptr uint64) ([]byte, error) { + mem := i.module.Memory() + if mem == nil { + return nil, fmt.Errorf("no memory exported") + } + + // Read length + lenPtr := ptr + 4 + lenBytes, ok := mem.Read(uint32(lenPtr), 4) + if !ok { + return nil, fmt.Errorf("failed to read result length") + } + length := binary.LittleEndian.Uint32(lenBytes) + + // Read data + dataPtr := ptr + 8 + data, ok := mem.Read(uint32(dataPtr), length) + if !ok { + return nil, fmt.Errorf("failed to read result data") + } + + return data, nil +} + +// callFunction calls a Wasm function +func (i *WazeroInstance) callFunction(name string, args ...uint64) (uint64, error) { + fn := i.module.ExportedFunction(name) + if fn == nil { + return 0, fmt.Errorf("function %s not found", name) + } + + result, err := fn.Call(i.ctx, args...) + if err != nil { + return 0, err + } + + return result[0], nil +} + +// AnalyzeCode analyzes the code for capabilities +func (i *WazeroInstance) AnalyzeCode() (*types.AnalysisReport, error) { + report := &types.AnalysisReport{ + HasIBCEntryPoints: false, + } + + // Check for IBC entry points + ibcFunctions := []string{ + "ibc_channel_open", + "ibc_channel_connect", + "ibc_channel_close", + "ibc_packet_receive", + "ibc_packet_ack", + "ibc_packet_timeout", + } + + hasAllIBCFunctions := true + for _, name := range ibcFunctions { + if i.module.ExportedFunction(name) == nil { + hasAllIBCFunctions = false + break + } + } + + report.HasIBCEntryPoints = hasAllIBCFunctions + return report, nil +} + +// IBCPacketTimeout handles an IBC packet timeout +func (i *WazeroInstance) IBCPacketTimeout(code []byte, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *types.Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + // Write message to memory + msgPtr, err := i.allocateAndWrite(msg) + if err != nil { + return nil, types.GasReport{}, err + } + + // Call the Wasm function + result, err := i.callFunction("ibc_packet_timeout", msgPtr) + if err != nil { + return nil, types.GasReport{}, err + } + + // Read result from memory + data, err := i.readResult(result) + if err != nil { + return nil, types.GasReport{}, err + } + + return data, types.GasReport{}, nil +} + +// IBCChannelOpen handles IBC channel open +func (i *WazeroInstance) IBCChannelOpen(code []byte, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *types.Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + msgPtr, err := i.allocateAndWrite(msg) + if err != nil { + return nil, types.GasReport{}, err + } + result, err := i.callFunction("ibc_channel_open", msgPtr) + if err != nil { + return nil, types.GasReport{}, err + } + data, err := i.readResult(result) + if err != nil { + return nil, types.GasReport{}, err + } + return data, types.GasReport{}, nil +} + +// IBCChannelConnect handles IBC channel connect +func (i *WazeroInstance) IBCChannelConnect(code []byte, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *types.Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + msgPtr, err := i.allocateAndWrite(msg) + if err != nil { + return nil, types.GasReport{}, err + } + result, err := i.callFunction("ibc_channel_connect", msgPtr) + if err != nil { + return nil, types.GasReport{}, err + } + data, err := i.readResult(result) + if err != nil { + return nil, types.GasReport{}, err + } + return data, types.GasReport{}, nil +} + +// IBCChannelClose handles IBC channel close +func (i *WazeroInstance) IBCChannelClose(code []byte, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *types.Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + msgPtr, err := i.allocateAndWrite(msg) + if err != nil { + return nil, types.GasReport{}, err + } + result, err := i.callFunction("ibc_channel_close", msgPtr) + if err != nil { + return nil, types.GasReport{}, err + } + data, err := i.readResult(result) + if err != nil { + return nil, types.GasReport{}, err + } + return data, types.GasReport{}, nil +} + +// IBCPacketReceive handles IBC packet receive +func (i *WazeroInstance) IBCPacketReceive(code []byte, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *types.Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + msgPtr, err := i.allocateAndWrite(msg) + if err != nil { + return nil, types.GasReport{}, err + } + result, err := i.callFunction("ibc_packet_receive", msgPtr) + if err != nil { + return nil, types.GasReport{}, err + } + data, err := i.readResult(result) + if err != nil { + return nil, types.GasReport{}, err + } + return data, types.GasReport{}, nil +} + +// IBCPacketAck handles IBC packet ack +func (i *WazeroInstance) IBCPacketAck(code []byte, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *types.Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + msgPtr, err := i.allocateAndWrite(msg) + if err != nil { + return nil, types.GasReport{}, err + } + result, err := i.callFunction("ibc_packet_ack", msgPtr) + if err != nil { + return nil, types.GasReport{}, err + } + data, err := i.readResult(result) + if err != nil { + return nil, types.GasReport{}, err + } + return data, types.GasReport{}, nil +} + +// IBCSourceCallback handles IBC source callback +func (i *WazeroInstance) IBCSourceCallback(code []byte, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *types.Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + msgPtr, err := i.allocateAndWrite(msg) + if err != nil { + return nil, types.GasReport{}, err + } + result, err := i.callFunction("ibc_source_callback", msgPtr) + if err != nil { + return nil, types.GasReport{}, err + } + data, err := i.readResult(result) + if err != nil { + return nil, types.GasReport{}, err + } + return data, types.GasReport{}, nil +} + +// IBCDestinationCallback handles IBC destination callback +func (i *WazeroInstance) IBCDestinationCallback(code []byte, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *types.Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + msgPtr, err := i.allocateAndWrite(msg) + if err != nil { + return nil, types.GasReport{}, err + } + result, err := i.callFunction("ibc_destination_callback", msgPtr) + if err != nil { + return nil, types.GasReport{}, err + } + data, err := i.readResult(result) + if err != nil { + return nil, types.GasReport{}, err + } + return data, types.GasReport{}, nil +} + +// Execute executes a contract with the given parameters +func (i *WazeroInstance) Execute(code []byte, info []byte, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *types.Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + // Write parameters to memory + infoPtr, err := i.allocateAndWrite(info) + if err != nil { + return nil, types.GasReport{}, err + } + + msgPtr, err := i.allocateAndWrite(msg) + if err != nil { + return nil, types.GasReport{}, err + } + + // Call execute function + resultPtr, err := i.callFunction("execute", infoPtr, msgPtr) + if err != nil { + return nil, types.GasReport{}, err + } + + // Read result + result, err := i.readResult(resultPtr) + if err != nil { + return nil, types.GasReport{}, err + } + + return result, types.GasReport{}, nil +} + +// Instantiate creates a new instance of a contract +func (i *WazeroInstance) Instantiate(code []byte, info []byte, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *types.Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + // Write parameters to memory + infoPtr, err := i.allocateAndWrite(info) + if err != nil { + return nil, types.GasReport{}, err + } + + msgPtr, err := i.allocateAndWrite(msg) + if err != nil { + return nil, types.GasReport{}, err + } + + // Call instantiate function + resultPtr, err := i.callFunction("instantiate", infoPtr, msgPtr) + if err != nil { + return nil, types.GasReport{}, err + } + + // Read result + result, err := i.readResult(resultPtr) + if err != nil { + return nil, types.GasReport{}, err + } + + return result, types.GasReport{}, nil +} + +// Query executes a query on a contract +func (i *WazeroInstance) Query(code []byte, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *types.Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + msgPtr, err := i.allocateAndWrite(msg) + if err != nil { + return nil, types.GasReport{}, err + } + + resultPtr, err := i.callFunction("query", msgPtr) + if err != nil { + return nil, types.GasReport{}, err + } + + result, err := i.readResult(resultPtr) + if err != nil { + return nil, types.GasReport{}, err + } + + return result, types.GasReport{}, nil +} + +// Migrate executes a migration on a contract +func (i *WazeroInstance) Migrate(code []byte, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *types.Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + msgPtr, err := i.allocateAndWrite(msg) + if err != nil { + return nil, types.GasReport{}, err + } + + resultPtr, err := i.callFunction("migrate", msgPtr) + if err != nil { + return nil, types.GasReport{}, err + } + + result, err := i.readResult(resultPtr) + if err != nil { + return nil, types.GasReport{}, err + } + + return result, types.GasReport{}, nil +} + +// MigrateWithInfo executes a migration with additional info +func (i *WazeroInstance) MigrateWithInfo(code []byte, msg []byte, migrateInfo []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *types.Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + msgPtr, err := i.allocateAndWrite(msg) + if err != nil { + return nil, types.GasReport{}, err + } + + infoPtr, err := i.allocateAndWrite(migrateInfo) + if err != nil { + return nil, types.GasReport{}, err + } + + resultPtr, err := i.callFunction("migrate_with_info", msgPtr, infoPtr) + if err != nil { + return nil, types.GasReport{}, err + } + + result, err := i.readResult(resultPtr) + if err != nil { + return nil, types.GasReport{}, err + } + + return result, types.GasReport{}, nil +} + +// Sudo executes a privileged operation +func (i *WazeroInstance) Sudo(code []byte, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *types.Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + msgPtr, err := i.allocateAndWrite(msg) + if err != nil { + return nil, types.GasReport{}, err + } + + resultPtr, err := i.callFunction("sudo", msgPtr) + if err != nil { + return nil, types.GasReport{}, err + } + + result, err := i.readResult(resultPtr) + if err != nil { + return nil, types.GasReport{}, err + } + + return result, types.GasReport{}, nil +} + +// Reply handles a reply callback +func (i *WazeroInstance) Reply(code []byte, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *types.Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + msgPtr, err := i.allocateAndWrite(msg) + if err != nil { + return nil, types.GasReport{}, err + } + + resultPtr, err := i.callFunction("reply", msgPtr) + if err != nil { + return nil, types.GasReport{}, err + } + + result, err := i.readResult(resultPtr) + if err != nil { + return nil, types.GasReport{}, err + } + + return result, types.GasReport{}, nil +} diff --git a/internal/api/wazero_adapter.go b/internal/api/wazero_adapter.go new file mode 100644 index 000000000..44e767afb --- /dev/null +++ b/internal/api/wazero_adapter.go @@ -0,0 +1,212 @@ +package api + +import ( + "github.com/CosmWasm/wasmvm/v2/types" +) + +// Querier is an alias for types.Querier +type Querier = types.Querier + +// WazeroVM represents a Wazero-based VM +type WazeroVM struct { + cache Cache + env *Environment + printDebug bool +} + +// NewWazeroVMWithCache creates a new WazeroVM instance with a given cache +func NewWazeroVMWithCache(cache Cache) *WazeroVM { + return &WazeroVM{ + cache: cache, + } +} + +// CreateInstance creates a new WazeroInstance with the given environment and gas limit +func (vm *WazeroVM) CreateInstance(env *Environment, gasLimit uint64) (*WazeroInstance, error) { + gasMeter := &WazeroGasMeter{} + instance, err := NewWazeroInstance(env, env.Code, gasMeter, gasLimit) + if err != nil { + return nil, err + } + return instance, nil +} + +// Execute creates a new instance and executes the given function +func (vm *WazeroVM) Execute(env *Environment, info []byte, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + instance, err := vm.CreateInstance(env, gasLimit) + if err != nil { + return nil, types.GasReport{}, err + } + defer instance.Close() + return instance.Execute(env.Code, info, msg, gasMeter, store, api, querier, gasLimit, printDebug) +} + +// Query creates a new instance and executes a query +func (vm *WazeroVM) Query(env *Environment, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + instance, err := vm.CreateInstance(env, gasLimit) + if err != nil { + return nil, types.GasReport{}, err + } + defer instance.Close() + return instance.Query(env.Code, msg, gasMeter, store, api, querier, gasLimit, printDebug) +} + +// Migrate creates a new instance and executes a migration +func (vm *WazeroVM) Migrate(env *Environment, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + instance, err := vm.CreateInstance(env, gasLimit) + if err != nil { + return nil, types.GasReport{}, err + } + defer instance.Close() + return instance.Migrate(env.Code, msg, gasMeter, store, api, querier, gasLimit, printDebug) +} + +// MigrateWithInfo creates a new instance and executes a migration with additional info +func (vm *WazeroVM) MigrateWithInfo(env *Environment, msg []byte, migrateInfo []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + instance, err := vm.CreateInstance(env, gasLimit) + if err != nil { + return nil, types.GasReport{}, err + } + defer instance.Close() + return instance.MigrateWithInfo(env.Code, msg, migrateInfo, gasMeter, store, api, querier, gasLimit, printDebug) +} + +// Sudo creates a new instance and executes a privileged function +func (vm *WazeroVM) Sudo(env *Environment, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + instance, err := vm.CreateInstance(env, gasLimit) + if err != nil { + return nil, types.GasReport{}, err + } + defer instance.Close() + return instance.Sudo(env.Code, msg, gasMeter, store, api, querier, gasLimit, printDebug) +} + +// Reply creates a new instance and executes a reply callback +func (vm *WazeroVM) Reply(env *Environment, reply []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + instance, err := vm.CreateInstance(env, gasLimit) + if err != nil { + return nil, types.GasReport{}, err + } + defer instance.Close() + return instance.Reply(env.Code, reply, gasMeter, store, api, querier, gasLimit, printDebug) +} + +// IBCChannelOpen creates a new instance and executes an IBC channel open +func (vm *WazeroVM) IBCChannelOpen(env *Environment, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + instance, err := vm.CreateInstance(env, gasLimit) + if err != nil { + return nil, types.GasReport{}, err + } + defer instance.Close() + return instance.IBCChannelOpen(env.Code, msg, gasMeter, store, api, querier, gasLimit, printDebug) +} + +// IBCChannelConnect creates a new instance and executes an IBC channel connect +func (vm *WazeroVM) IBCChannelConnect(env *Environment, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + instance, err := vm.CreateInstance(env, gasLimit) + if err != nil { + return nil, types.GasReport{}, err + } + defer instance.Close() + return instance.IBCChannelConnect(env.Code, msg, gasMeter, store, api, querier, gasLimit, printDebug) +} + +// IBCChannelClose creates a new instance and executes an IBC channel close +func (vm *WazeroVM) IBCChannelClose(env *Environment, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + instance, err := vm.CreateInstance(env, gasLimit) + if err != nil { + return nil, types.GasReport{}, err + } + defer instance.Close() + return instance.IBCChannelClose(env.Code, msg, gasMeter, store, api, querier, gasLimit, printDebug) +} + +// IBCPacketReceive creates a new instance and executes an IBC packet receive +func (vm *WazeroVM) IBCPacketReceive(env *Environment, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + instance, err := vm.CreateInstance(env, gasLimit) + if err != nil { + return nil, types.GasReport{}, err + } + defer instance.Close() + return instance.IBCPacketReceive(env.Code, msg, gasMeter, store, api, querier, gasLimit, printDebug) +} + +// IBCPacketAck creates a new instance and executes an IBC packet ack +func (vm *WazeroVM) IBCPacketAck(env *Environment, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + instance, err := vm.CreateInstance(env, gasLimit) + if err != nil { + return nil, types.GasReport{}, err + } + defer instance.Close() + return instance.IBCPacketAck(env.Code, msg, gasMeter, store, api, querier, gasLimit, printDebug) +} + +// IBCPacketTimeout creates a new instance and executes an IBC packet timeout +func (vm *WazeroVM) IBCPacketTimeout(env *Environment, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + instance, err := vm.CreateInstance(env, gasLimit) + if err != nil { + return nil, types.GasReport{}, err + } + defer instance.Close() + return instance.IBCPacketTimeout(env.Code, msg, gasMeter, store, api, querier, gasLimit, printDebug) +} + +// IBCSourceCallback creates a new instance and executes an IBC source callback +func (vm *WazeroVM) IBCSourceCallback(env *Environment, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + instance, err := vm.CreateInstance(env, gasLimit) + if err != nil { + return nil, types.GasReport{}, err + } + defer instance.Close() + return instance.IBCSourceCallback(env.Code, msg, gasMeter, store, api, querier, gasLimit, printDebug) +} + +// IBCDestinationCallback creates a new instance and executes an IBC destination callback +func (vm *WazeroVM) IBCDestinationCallback(env *Environment, msg []byte, gasMeter *types.GasMeter, store types.KVStore, api *types.GoAPI, querier *Querier, gasLimit uint64, printDebug bool) ([]byte, types.GasReport, error) { + instance, err := vm.CreateInstance(env, gasLimit) + if err != nil { + return nil, types.GasReport{}, err + } + defer instance.Close() + return instance.IBCDestinationCallback(env.Code, msg, gasMeter, store, api, querier, gasLimit, printDebug) +} + +// AnalyzeCode returns a report about the given code +func (vm *WazeroVM) AnalyzeCode(checksum []byte) (*types.AnalysisReport, error) { + code, err := GetCode(vm.cache, checksum) + if err != nil { + return nil, err + } + + env := &Environment{ + Code: code, + } + + instance, err := NewWazeroInstance(env, code, &WazeroGasMeter{}, 0) + if err != nil { + return nil, err + } + defer instance.Close() + + return instance.AnalyzeCode() +} + +// GetMetrics returns metrics about the VM +func (vm *WazeroVM) GetMetrics() (*types.Metrics, error) { + return &types.Metrics{}, nil +} + +// GetPinnedMetrics returns metrics about pinned contracts +func (vm *WazeroVM) GetPinnedMetrics() (*types.PinnedMetrics, error) { + return &types.PinnedMetrics{}, nil +} + +// Pin pins a contract in memory +func (vm *WazeroVM) Pin(checksum []byte) error { + return nil +} + +// Unpin unpins a contract from memory +func (vm *WazeroVM) Unpin(checksum []byte) error { + return nil +} diff --git a/lib.go b/lib.go deleted file mode 100644 index 161f36f4c..000000000 --- a/lib.go +++ /dev/null @@ -1,59 +0,0 @@ -// This file contains the part of the API that is exposed no matter if libwasmvm -// is available or not. Symbols from lib_libwasmvm.go are added conditionally. - -package cosmwasm - -import ( - "bytes" - "crypto/sha256" - "fmt" - - "github.com/CosmWasm/wasmvm/v2/types" -) - -// Checksum represents a hash of the Wasm bytecode that serves as an ID. Must be generated from this library. -type Checksum = types.Checksum - -// WasmCode is an alias for raw bytes of the wasm compiled code -type WasmCode []byte - -// KVStore is a reference to some sub-kvstore that is valid for one instance of a code -type KVStore = types.KVStore - -// GoAPI is a reference to some "precompiles", go callbacks -type GoAPI = types.GoAPI - -// Querier lets us make read-only queries on other modules -type Querier = types.Querier - -// GasMeter is a read-only version of the sdk gas meter -type GasMeter = types.GasMeter - -// LibwasmvmVersion returns the version of the loaded library -// at runtime. This can be used for debugging to verify the loaded version -// matches the expected version. -// -// When cgo is disabled at build time, this returns an error at runtime. -func LibwasmvmVersion() (string, error) { - return libwasmvmVersionImpl() -} - -// CreateChecksum performs the hashing of Wasm bytes to obtain the CosmWasm checksum. -// -// Ony Wasm blobs are allowed as inputs and a magic byte check will be performed -// to avoid accidental misusage. -func CreateChecksum(wasm []byte) (Checksum, error) { - if len(wasm) == 0 { - return Checksum{}, fmt.Errorf("Wasm bytes nil or empty") - } - if len(wasm) < 4 { - return Checksum{}, fmt.Errorf("Wasm bytes shorter than 4 bytes") - } - // magic number for Wasm is "\0asm" - // See https://webassembly.github.io/spec/core/binary/modules.html#binary-module - if !bytes.Equal(wasm[:4], []byte("\x00\x61\x73\x6D")) { - return Checksum{}, fmt.Errorf("Wasm bytes do not start with Wasm magic number") - } - hash := sha256.Sum256(wasm) - return Checksum(hash[:]), nil -} diff --git a/lib_libwasmvm.go b/lib_libwasmvm.go index d07bc189f..6fd260ea7 100644 --- a/lib_libwasmvm.go +++ b/lib_libwasmvm.go @@ -1,11 +1,11 @@ -//go:build cgo && !nolink_libwasmvm - -// This file contains the part of the API that is exposed when libwasmvm -// is available (i.e. cgo is enabled and nolink_libwasmvm is not set). +//lib_libwasmvm.go: +// 1. it can inspire the wrath of the compiler. package cosmwasm import ( + "bytes" + "crypto/sha256" "encoding/json" "fmt" @@ -13,42 +13,19 @@ import ( "github.com/CosmWasm/wasmvm/v2/types" ) -// VM is the main entry point to this library. -// You should create an instance with its own subdirectory to manage state inside, -// and call it for all cosmwasm code related actions. +// VM represents a Wasm VM with cache type VM struct { - cache api.Cache + cache *api.Cache printDebug bool } -// NewVM creates a new VM. -// -// `dataDir` is a base directory for Wasm blobs and various caches. -// `supportedCapabilities` is a list of capabilities supported by the chain. -// `memoryLimit` is the memory limit of each contract execution (in MiB) -// `printDebug` is a flag to enable/disable printing debug logs from the contract to STDOUT. This should be false in production environments. -// `cacheSize` sets the size in MiB of an in-memory cache for e.g. module caching. Set to 0 to disable. -// `deserCost` sets the gas cost of deserializing one byte of data. -func NewVM(dataDir string, supportedCapabilities []string, memoryLimit uint32, printDebug bool, cacheSize uint32) (*VM, error) { - return NewVMWithConfig(types.VMConfig{ - Cache: types.CacheOptions{ - BaseDir: dataDir, - AvailableCapabilities: supportedCapabilities, - MemoryCacheSizeBytes: types.NewSizeMebi(cacheSize), - InstanceMemoryLimitBytes: types.NewSizeMebi(memoryLimit), - }, - }, printDebug) -} - -// NewVMWithConfig creates a new VM with a custom configuration. -// This allows for more fine-grained control over the VM's behavior compared to NewVM and -// can be extended more easily in the future. -func NewVMWithConfig(config types.VMConfig, printDebug bool) (*VM, error) { +// NewVM creates new wasm VM +func NewVM(config types.VMConfig) (*VM, error) { cache, err := api.InitCache(config) if err != nil { return nil, err } - return &VM{cache: cache, printDebug: printDebug}, nil + return &VM{cache: cache, printDebug: false}, nil } // Cleanup should be called when no longer using this instances. @@ -73,18 +50,18 @@ func (vm *VM) StoreCode(code WasmCode, gasLimit uint64) (Checksum, uint64, error return nil, gasCost, types.OutOfGasError{} } - checksum, err := api.StoreCode(vm.cache, code) + checksum, err := api.StoreCode(*vm.cache, code) return checksum, gasCost, err } // StoreCodeUnchecked is the same as StoreCode but skips static validation checks. // Use this for adding code that was checked before, particularly in the case of state sync. func (vm *VM) StoreCodeUnchecked(code WasmCode) (Checksum, error) { - return api.StoreCodeUnchecked(vm.cache, code) + return api.StoreCodeUnchecked(*vm.cache, code) } func (vm *VM) RemoveCode(checksum Checksum) error { - return api.RemoveCode(vm.cache, checksum) + return api.RemoveCode(*vm.cache, checksum) } // GetCode will load the original Wasm code for the given checksum. @@ -95,14 +72,14 @@ func (vm *VM) RemoveCode(checksum Checksum) error { // and the larger binary blobs (wasm and compiled modules) are all managed // by libwasmvm/cosmwasm-vm (Rust part). func (vm *VM) GetCode(checksum Checksum) (WasmCode, error) { - return api.GetCode(vm.cache, checksum) + return api.GetCode(*vm.cache, checksum) } // Pin pins a code to an in-memory cache, such that is // always loaded quickly when executed. // Pin is idempotent. func (vm *VM) Pin(checksum Checksum) error { - return api.Pin(vm.cache, checksum) + return api.Pin(*vm.cache, checksum) } // Unpin removes the guarantee of a contract to be pinned (see Pin). @@ -110,25 +87,25 @@ func (vm *VM) Pin(checksum Checksum) error { // the implementor's choice. // Unpin is idempotent. func (vm *VM) Unpin(checksum Checksum) error { - return api.Unpin(vm.cache, checksum) + return api.Unpin(*vm.cache, checksum) } // Returns a report of static analysis of the wasm contract (uncompiled). // This contract must have been stored in the cache previously (via Create). // Only info currently returned is if it exposes all ibc entry points, but this may grow later func (vm *VM) AnalyzeCode(checksum Checksum) (*types.AnalysisReport, error) { - return api.AnalyzeCode(vm.cache, checksum) + return api.AnalyzeCode(*vm.cache, checksum) } // GetMetrics some internal metrics for monitoring purposes. func (vm *VM) GetMetrics() (*types.Metrics, error) { - return api.GetMetrics(vm.cache) + return api.GetMetrics(*vm.cache) } // GetPinnedMetrics returns some internal metrics of pinned contracts for monitoring purposes. // The order of entries is non-deterministic and the values are node-specific. Don't use this in consensus-critical contexts. func (vm *VM) GetPinnedMetrics() (*types.PinnedMetrics, error) { - return api.GetPinnedMetrics(vm.cache) + return api.GetPinnedMetrics(*vm.cache) } // Instantiate will create a new contract based on the given Checksum. @@ -526,20 +503,8 @@ func (vm *VM) IBCPacketReceive( return &result, gasReport.UsedInternally, nil } -// IBCPacketAck is available on IBC-enabled contracts and is called when an -// the response for an outgoing packet (previously sent by this contract) -// is received -func (vm *VM) IBCPacketAck( - checksum Checksum, - env types.Env, - msg types.IBCPacketAckMsg, - store KVStore, - goapi GoAPI, - querier Querier, - gasMeter GasMeter, - gasLimit uint64, - deserCost types.UFraction, -) (*types.IBCBasicResult, uint64, error) { +// IBCPacketAck processes an acknowledgment packet +func (vm *VM) IBCPacketAck(checksum Checksum, env types.Env, msg types.IBCPacketAckMsg, store KVStore, goapi GoAPI, querier Querier, gasMeter GasMeter, gasLimit uint64, deserCost types.UFraction) (*types.IBCBasicResult, uint64, error) { envBin, err := json.Marshal(env) if err != nil { return nil, 0, err @@ -548,7 +513,13 @@ func (vm *VM) IBCPacketAck( if err != nil { return nil, 0, err } - data, gasReport, err := api.IBCPacketAck(vm.cache, checksum, envBin, msgBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) + + // Convert interfaces to pointers + gasMeterPtr := &gasMeter + goapiPtr := &goapi + querierPtr := &querier + + data, gasReport, err := api.IBCPacketAck(vm.cache, checksum, envBin, msgBin, gasMeterPtr, store, goapiPtr, querierPtr, gasLimit, vm.printDebug) if err != nil { return nil, gasReport.UsedInternally, err } @@ -713,3 +684,50 @@ func DeserializeResponse(gasLimit uint64, deserCost types.UFraction, gasReport * return nil } + +// Checksum represents a hash of the Wasm bytecode that serves as an ID. Must be generated from this library. +type Checksum = types.Checksum + +// WasmCode is an alias for raw bytes of the wasm compiled code +type WasmCode []byte + +// KVStore is a reference to some sub-kvstore that is valid for one instance of a code +type KVStore = types.KVStore + +// GoAPI is a reference to some "precompiles", go callbacks +type GoAPI = types.GoAPI + +// Querier lets us make read-only queries on other modules +type Querier = types.Querier + +// GasMeter is a read-only version of the sdk gas meter +type GasMeter = types.GasMeter + +// LibwasmvmVersion returns the version of the loaded library +// at runtime. This can be used for debugging to verify the loaded version +// matches the expected version. +// +// When cgo is disabled at build time, this returns an error at runtime. +func LibwasmvmVersion() (string, error) { + return libwasmvmVersionImpl() +} + +// CreateChecksum performs the hashing of Wasm bytes to obtain the CosmWasm checksum. +// +// Ony Wasm blobs are allowed as inputs and a magic byte check will be performed +// to avoid accidental misusage. +func CreateChecksum(wasm []byte) (Checksum, error) { + if len(wasm) == 0 { + return Checksum{}, fmt.Errorf("Wasm bytes nil or empty") + } + if len(wasm) < 4 { + return Checksum{}, fmt.Errorf("Wasm bytes shorter than 4 bytes") + } + // magic number for Wasm is "\0asm" + // See https://webassembly.github.io/spec/core/binary/modules.html#binary-module + if !bytes.Equal(wasm[:4], []byte("\x00\x61\x73\x6D")) { + return Checksum{}, fmt.Errorf("Wasm bytes do not start with Wasm magic number") + } + hash := sha256.Sum256(wasm) + return Checksum(hash[:]), nil +} diff --git a/lib_libwasmvm_test.go b/lib_libwasmvm_test.go index 8d9350d61..7807c53e0 100644 --- a/lib_libwasmvm_test.go +++ b/lib_libwasmvm_test.go @@ -1,5 +1,3 @@ -//go:build cgo && !nolink_libwasmvm - package cosmwasm import ( @@ -33,7 +31,16 @@ const ( func withVM(t *testing.T) *VM { tmpdir, err := os.MkdirTemp("", "wasmvm-testing") require.NoError(t, err) - vm, err := NewVM(tmpdir, TESTING_CAPABILITIES, TESTING_MEMORY_LIMIT, TESTING_PRINT_DEBUG, TESTING_CACHE_SIZE) + config := types.VMConfig{ + WasmLimits: types.WasmLimits{}, + Cache: types.CacheOptions{ + BaseDir: tmpdir, + AvailableCapabilities: TESTING_CAPABILITIES, + MemoryCacheSizeBytes: types.NewSizeKibi(TESTING_CACHE_SIZE * 1024), // Convert MiB to bytes + InstanceMemoryLimitBytes: types.NewSizeKibi(TESTING_MEMORY_LIMIT * 1024), // Convert MiB to bytes + }, + } + vm, err := NewVM(config) require.NoError(t, err) t.Cleanup(func() { diff --git a/version_cgo.go b/version_cgo.go deleted file mode 100644 index 7129ce5dc..000000000 --- a/version_cgo.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build cgo && !nolink_libwasmvm - -package cosmwasm - -import ( - "github.com/CosmWasm/wasmvm/v2/internal/api" -) - -func libwasmvmVersionImpl() (string, error) { - return api.LibwasmvmVersion() -} diff --git a/version_no_cgo.go b/version_no_cgo.go index cc7131fca..c06f16fbd 100644 --- a/version_no_cgo.go +++ b/version_no_cgo.go @@ -1,5 +1,3 @@ -//go:build !cgo || nolink_libwasmvm - package cosmwasm import (