From 1dba29fc56895b640e655d2fe1df7cd1463b846d Mon Sep 17 00:00:00 2001 From: Damian Nolan Date: Wed, 20 Mar 2024 10:12:24 +0100 Subject: [PATCH 1/6] imp: adjust build directives to be more flexible, allowing cgo but disabling libwasmvm linking --- ibc_test.go | 2 +- lib.go | 2 +- lib_test.go | 2 +- version_cgo.go | 2 +- version_no_cgo.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ibc_test.go b/ibc_test.go index 7c468f008..df919e800 100644 --- a/ibc_test.go +++ b/ibc_test.go @@ -1,4 +1,4 @@ -//go:build cgo +//go:build cgo && !nolink_libwasmvm package cosmwasm diff --git a/lib.go b/lib.go index 453e1b52c..7979ba5c9 100644 --- a/lib.go +++ b/lib.go @@ -1,4 +1,4 @@ -//go:build cgo +//go:build cgo && !nolink_libwasmvm // This file contains the part of the API that is exposed when cgo is enabled. diff --git a/lib_test.go b/lib_test.go index 2cbe999d8..82327ecaf 100644 --- a/lib_test.go +++ b/lib_test.go @@ -1,4 +1,4 @@ -//go:build cgo +//go:build cgo && !nolink_libwasmvm package cosmwasm diff --git a/version_cgo.go b/version_cgo.go index 7c727369e..7129ce5dc 100644 --- a/version_cgo.go +++ b/version_cgo.go @@ -1,4 +1,4 @@ -//go:build cgo +//go:build cgo && !nolink_libwasmvm package cosmwasm diff --git a/version_no_cgo.go b/version_no_cgo.go index 68500aaef..cc7131fca 100644 --- a/version_no_cgo.go +++ b/version_no_cgo.go @@ -1,4 +1,4 @@ -//go:build !cgo +//go:build !cgo || nolink_libwasmvm package cosmwasm From 2e2674392cbd10d0d21604655a35788e82cd5a3d Mon Sep 17 00:00:00 2001 From: Damian Nolan Date: Wed, 20 Mar 2024 10:13:03 +0100 Subject: [PATCH 2/6] chore: update README.md with nolink_libwasmvm doc --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 912d6b222..d662df8ac 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,13 @@ go build . CGO_ENABLED=0 go build . ``` +In the case that it may be desirable to compile with cgo, but with libwasmvm linking disabled an additional build tag is available. + +```sh +# Build with CGO, but with libwasmvm linking disabled +go build -tags "nolink_libwasmvm" +``` + ## Supported Platforms See [COMPILER_VERSIONS.md](docs/COMPILER_VERSIONS.md) for information on Go and From 4bef6634e28f14e2f4717b08e877d58d6e29d7e4 Mon Sep 17 00:00:00 2001 From: Damian Nolan Date: Wed, 20 Mar 2024 10:13:38 +0100 Subject: [PATCH 3/6] chore: add nolink_libwasmvm job to circleci config --- .circleci/config.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9f005f4a8..2818d8a9d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -201,6 +201,25 @@ jobs: - run: name: Test package "cosmwasm" without cgo command: CGO_ENABLED=0 go test . + + # Build types and cosmwasm with libwasmvm linking disabled + nolink_libwasmvm: + docker: + - image: cimg/go:1.21.4 + steps: + - checkout + - run: + name: Build package "types" without cgo + command: go build -tags "nolink_libwasmvm" ./types + - run: + name: Build package "cosmwasm" without cgo + command: go build -tags "nolink_libwasmvm" . + - run: + name: Test package "types" without cgo + command: go test -tags "nolink_libwasmvm" ./types + - run: + name: Test package "cosmwasm" with libwasmvm linking disabled + command: go test -tags "nolink_libwasmvm" . tidy-go: docker: @@ -433,6 +452,7 @@ workflows: - libwasmvm_audit - format-go - wasmvm_no_cgo + - nolink_libwasmvm - tidy-go - format-scripts - lint-scripts From 7465b379bea023be557daca5aaa3e659131d2831 Mon Sep 17 00:00:00 2001 From: Damian Nolan Date: Tue, 26 Mar 2024 10:59:01 +0100 Subject: [PATCH 4/6] fix: correct circleci workflow names --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2818d8a9d..f7718fa39 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -209,13 +209,13 @@ jobs: steps: - checkout - run: - name: Build package "types" without cgo + name: Build package "types" with libwasmvm linking disabled command: go build -tags "nolink_libwasmvm" ./types - run: - name: Build package "cosmwasm" without cgo + name: Build package "cosmwasm" with libwasmvm linking disabled command: go build -tags "nolink_libwasmvm" . - run: - name: Test package "types" without cgo + name: Test package "types" with libwasmvm linking disabled command: go test -tags "nolink_libwasmvm" ./types - run: name: Test package "cosmwasm" with libwasmvm linking disabled From 79e4627d8670e516cb7da9772ee4f4d6ad14a389 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 28 Mar 2024 16:46:29 +0100 Subject: [PATCH 5/6] Update docs of lib.go/lib_no_cgo.go --- lib.go | 3 ++- lib_no_cgo.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib.go b/lib.go index 7979ba5c9..e4f3e8565 100644 --- a/lib.go +++ b/lib.go @@ -1,6 +1,7 @@ //go:build cgo && !nolink_libwasmvm -// This file contains the part of the API that is exposed when cgo is enabled. +// 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). package cosmwasm diff --git a/lib_no_cgo.go b/lib_no_cgo.go index 176d89f17..45d263d22 100644 --- a/lib_no_cgo.go +++ b/lib_no_cgo.go @@ -1,4 +1,5 @@ -// This file contains the part of the API that is exposed when cgo is disabled. +// This file contains the part of the API that is exposed no matter if libwasmvm +// is available or not. Symbols from lib.go are added conditionally. package cosmwasm From bcde382d445380bb066fe1e90f323646a1f86bfb Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Fri, 29 Mar 2024 13:03:53 +0100 Subject: [PATCH 6/6] Rename lib.go -> lib_libwasmvm.go --- lib.go | 587 +++--------------------------------------- lib_libwasmvm.go | 574 +++++++++++++++++++++++++++++++++++++++++ lib_libwasmvm_test.go | 412 +++++++++++++++++++++++++++++ lib_no_cgo.go | 59 ----- lib_no_cgo_test.go | 33 --- lib_test.go | 413 ++--------------------------- 6 files changed, 1039 insertions(+), 1039 deletions(-) create mode 100644 lib_libwasmvm.go create mode 100644 lib_libwasmvm_test.go delete mode 100644 lib_no_cgo.go delete mode 100644 lib_no_cgo_test.go diff --git a/lib.go b/lib.go index e4f3e8565..1704ff0eb 100644 --- a/lib.go +++ b/lib.go @@ -1,574 +1,59 @@ -//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). +// 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 ( - "encoding/json" + "bytes" + "crypto/sha256" "fmt" - "github.com/CosmWasm/wasmvm/v2/internal/api" "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. -type VM struct { - 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) { - cache, err := api.InitCache(dataDir, supportedCapabilities, cacheSize, memoryLimit) - if err != nil { - return nil, err - } - return &VM{cache: cache, printDebug: printDebug}, nil -} - -// Cleanup should be called when no longer using this instances. -// It frees resources in libwasmvm (the Rust part) and releases a lock in the base directory. -func (vm *VM) Cleanup() { - api.ReleaseCache(vm.cache) -} - -// StoreCode will compile the Wasm code, and store the resulting compiled module -// as well as the original code. Both can be referenced later via Checksum. -// This must be done one time for given code, after which it can be -// instatitated many times, and each instance called many times. -// -// For example, the code for all ERC-20 contracts should be the same. -// This function stores the code for that contract only once, but it can -// be instantiated with custom inputs in the future. -// -// Returns both the checksum, as well as the gas cost of compilation (in CosmWasm Gas) or an error. -func (vm *VM) StoreCode(code WasmCode, gasLimit uint64) (Checksum, uint64, error) { - gasCost := compileCost(code) - if gasLimit < gasCost { - return nil, gasCost, types.OutOfGasError{} - } - - 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) -} - -func (vm *VM) RemoveCode(checksum Checksum) error { - return api.RemoveCode(vm.cache, checksum) -} - -// GetCode will load the original Wasm code for the given checksum. -// This will only succeed if that checksum was previously returned from -// a call to StoreCode. -// -// This can be used so that the (short) checksum is stored in the iavl tree -// 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) -} - -// 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) -} - -// Unpin removes the guarantee of a contract to be pinned (see Pin). -// After calling this, the code may or may not remain in memory depending on -// the implementor's choice. -// Unpin is idempotent. -func (vm *VM) Unpin(checksum Checksum) error { - 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) -} - -// GetMetrics some internal metrics for monitoring purposes. -func (vm *VM) GetMetrics() (*types.Metrics, error) { - return api.GetMetrics(vm.cache) -} - -// Instantiate will create a new contract based on the given Checksum. -// We can set the initMsg (contract "genesis") here, and it then receives -// an account and address and can be invoked (Execute) many times. -// -// Storage should be set with a PrefixedKVStore that this code can safely access. -// -// Under the hood, we may recompile the wasm, use a cached native compile, or even use a cached instance -// for performance. -func (vm *VM) Instantiate( - checksum Checksum, - env types.Env, - info types.MessageInfo, - initMsg []byte, - store KVStore, - goapi GoAPI, - querier Querier, - gasMeter GasMeter, - gasLimit uint64, - deserCost types.UFraction, -) (*types.ContractResult, uint64, error) { - envBin, err := json.Marshal(env) - if err != nil { - return nil, 0, err - } - infoBin, err := json.Marshal(info) - if err != nil { - return nil, 0, err - } - data, gasReport, err := api.Instantiate(vm.cache, checksum, envBin, infoBin, initMsg, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) - if err != nil { - return nil, gasReport.UsedInternally, err - } +// Checksum represents a hash of the Wasm bytecode that serves as an ID. Must be generated from this library. +type Checksum = types.Checksum - var result types.ContractResult - err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) - if err != nil { - return nil, gasReport.UsedInternally, err - } - return &result, gasReport.UsedInternally, nil -} +// WasmCode is an alias for raw bytes of the wasm compiled code +type WasmCode []byte -// Execute calls a given contract. Since the only difference between contracts with the same Checksum is the -// data in their local storage, and their address in the outside world, we need no ContractID here. -// (That is a detail for the external, sdk-facing, side). -// -// The caller is responsible for passing the correct `store` (which must have been initialized exactly once), -// and setting the env with relevant info on this instance (address, balance, etc) -func (vm *VM) Execute( - checksum Checksum, - env types.Env, - info types.MessageInfo, - executeMsg []byte, - store KVStore, - goapi GoAPI, - querier Querier, - gasMeter GasMeter, - gasLimit uint64, - deserCost types.UFraction, -) (*types.ContractResult, uint64, error) { - envBin, err := json.Marshal(env) - if err != nil { - return nil, 0, err - } - infoBin, err := json.Marshal(info) - if err != nil { - return nil, 0, err - } - data, gasReport, err := api.Execute(vm.cache, checksum, envBin, infoBin, executeMsg, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) - if err != nil { - return nil, gasReport.UsedInternally, err - } +// KVStore is a reference to some sub-kvstore that is valid for one instance of a code +type KVStore = types.KVStore - var result types.ContractResult - err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) - if err != nil { - return nil, gasReport.UsedInternally, err - } - return &result, gasReport.UsedInternally, nil -} +// GoAPI is a reference to some "precompiles", go callbacks +type GoAPI = types.GoAPI -// Query allows a client to execute a contract-specific query. If the result is not empty, it should be -// valid json-encoded data to return to the client. -// The meaning of path and data can be determined by the code. Path is the suffix of the abci.QueryRequest.Path -func (vm *VM) Query( - checksum Checksum, - env types.Env, - queryMsg []byte, - store KVStore, - goapi GoAPI, - querier Querier, - gasMeter GasMeter, - gasLimit uint64, - deserCost types.UFraction, -) (*types.QueryResult, uint64, error) { - envBin, err := json.Marshal(env) - if err != nil { - return nil, 0, err - } - data, gasReport, err := api.Query(vm.cache, checksum, envBin, queryMsg, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) - if err != nil { - return nil, gasReport.UsedInternally, err - } - - var result types.QueryResult - err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) - if err != nil { - return nil, gasReport.UsedInternally, err - } - return &result, gasReport.UsedInternally, nil -} - -// Migrate will migrate an existing contract to a new code binary. -// This takes storage of the data from the original contract and the Checksum of the new contract that should -// replace it. This allows it to run a migration step if needed, or return an error if unable to migrate -// the given data. -// -// MigrateMsg has some data on how to perform the migration. -func (vm *VM) Migrate( - checksum Checksum, - env types.Env, - migrateMsg []byte, - store KVStore, - goapi GoAPI, - querier Querier, - gasMeter GasMeter, - gasLimit uint64, - deserCost types.UFraction, -) (*types.ContractResult, uint64, error) { - envBin, err := json.Marshal(env) - if err != nil { - return nil, 0, err - } - data, gasReport, err := api.Migrate(vm.cache, checksum, envBin, migrateMsg, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) - if err != nil { - return nil, gasReport.UsedInternally, err - } +// Querier lets us make read-only queries on other modules +type Querier = types.Querier - var result types.ContractResult - err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) - if err != nil { - return nil, gasReport.UsedInternally, err - } - return &result, gasReport.UsedInternally, nil -} +// GasMeter is a read-only version of the sdk gas meter +type GasMeter = types.GasMeter -// Sudo allows native Go modules to make priviledged (sudo) calls on the contract. -// The contract can expose entry points that cannot be triggered by any transaction, but only via -// native Go modules, and delegate the access control to the system. +// 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. // -// These work much like Migrate (same scenario) but allows custom apps to extend the priviledged entry points -// without forking cosmwasm-vm. -func (vm *VM) Sudo( - checksum Checksum, - env types.Env, - sudoMsg []byte, - store KVStore, - goapi GoAPI, - querier Querier, - gasMeter GasMeter, - gasLimit uint64, - deserCost types.UFraction, -) (*types.ContractResult, uint64, error) { - envBin, err := json.Marshal(env) - if err != nil { - return nil, 0, err - } - data, gasReport, err := api.Sudo(vm.cache, checksum, envBin, sudoMsg, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) - if err != nil { - return nil, gasReport.UsedInternally, err - } - - var result types.ContractResult - err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) - if err != nil { - return nil, gasReport.UsedInternally, err - } - return &result, gasReport.UsedInternally, nil +// When cgo is disabled at build time, this returns an error at runtime. +func LibwasmvmVersion() (string, error) { + return libwasmvmVersionImpl() } -// Reply allows the native Go wasm modules to make a priviledged call to return the result -// of executing a SubMsg. +// CreateChecksum performs the hashing of Wasm bytes to obtain the CosmWasm checksum. // -// These work much like Sudo (same scenario) but focuses on one specific case (and one message type) -func (vm *VM) Reply( - checksum Checksum, - env types.Env, - reply types.Reply, - store KVStore, - goapi GoAPI, - querier Querier, - gasMeter GasMeter, - gasLimit uint64, - deserCost types.UFraction, -) (*types.ContractResult, uint64, error) { - envBin, err := json.Marshal(env) - if err != nil { - return nil, 0, err +// 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") } - replyBin, err := json.Marshal(reply) - if err != nil { - return nil, 0, err + if len(wasm) < 4 { + return Checksum{}, fmt.Errorf("Wasm bytes shorter than 4 bytes") } - data, gasReport, err := api.Reply(vm.cache, checksum, envBin, replyBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) - if err != nil { - return nil, gasReport.UsedInternally, err + // 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 not start with Wasm magic number") } - - var result types.ContractResult - err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) - if err != nil { - return nil, gasReport.UsedInternally, err - } - return &result, gasReport.UsedInternally, nil -} - -// IBCChannelOpen is available on IBC-enabled contracts and is a hook to call into -// during the handshake pahse -func (vm *VM) IBCChannelOpen( - checksum Checksum, - env types.Env, - msg types.IBCChannelOpenMsg, - store KVStore, - goapi GoAPI, - querier Querier, - gasMeter GasMeter, - gasLimit uint64, - deserCost types.UFraction, -) (*types.IBCChannelOpenResult, uint64, error) { - envBin, err := json.Marshal(env) - if err != nil { - return nil, 0, err - } - msgBin, err := json.Marshal(msg) - if err != nil { - return nil, 0, err - } - data, gasReport, err := api.IBCChannelOpen(vm.cache, checksum, envBin, msgBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) - if err != nil { - return nil, gasReport.UsedInternally, err - } - - var result types.IBCChannelOpenResult - err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) - if err != nil { - return nil, gasReport.UsedInternally, err - } - return &result, gasReport.UsedInternally, nil -} - -// IBCChannelConnect is available on IBC-enabled contracts and is a hook to call into -// during the handshake pahse -func (vm *VM) IBCChannelConnect( - checksum Checksum, - env types.Env, - msg types.IBCChannelConnectMsg, - 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 - } - msgBin, err := json.Marshal(msg) - if err != nil { - return nil, 0, err - } - data, gasReport, err := api.IBCChannelConnect(vm.cache, checksum, envBin, msgBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) - if err != nil { - return nil, gasReport.UsedInternally, err - } - - var result types.IBCBasicResult - err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) - if err != nil { - return nil, gasReport.UsedInternally, err - } - return &result, gasReport.UsedInternally, nil -} - -// IBCChannelClose is available on IBC-enabled contracts and is a hook to call into -// at the end of the channel lifetime -func (vm *VM) IBCChannelClose( - checksum Checksum, - env types.Env, - msg types.IBCChannelCloseMsg, - 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 - } - msgBin, err := json.Marshal(msg) - if err != nil { - return nil, 0, err - } - data, gasReport, err := api.IBCChannelClose(vm.cache, checksum, envBin, msgBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) - if err != nil { - return nil, gasReport.UsedInternally, err - } - - var result types.IBCBasicResult - err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) - if err != nil { - return nil, gasReport.UsedInternally, err - } - return &result, gasReport.UsedInternally, nil -} - -// IBCPacketReceive is available on IBC-enabled contracts and is called when an incoming -// packet is received on a channel belonging to this contract -func (vm *VM) IBCPacketReceive( - checksum Checksum, - env types.Env, - msg types.IBCPacketReceiveMsg, - store KVStore, - goapi GoAPI, - querier Querier, - gasMeter GasMeter, - gasLimit uint64, - deserCost types.UFraction, -) (*types.IBCReceiveResult, uint64, error) { - envBin, err := json.Marshal(env) - if err != nil { - return nil, 0, err - } - msgBin, err := json.Marshal(msg) - if err != nil { - return nil, 0, err - } - data, gasReport, err := api.IBCPacketReceive(vm.cache, checksum, envBin, msgBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) - if err != nil { - return nil, gasReport.UsedInternally, err - } - - var result types.IBCReceiveResult - err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) - if err != nil { - return nil, gasReport.UsedInternally, err - } - 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) { - envBin, err := json.Marshal(env) - if err != nil { - return nil, 0, err - } - msgBin, err := json.Marshal(msg) - if err != nil { - return nil, 0, err - } - data, gasReport, err := api.IBCPacketAck(vm.cache, checksum, envBin, msgBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) - if err != nil { - return nil, gasReport.UsedInternally, err - } - - var result types.IBCBasicResult - err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) - if err != nil { - return nil, gasReport.UsedInternally, err - } - return &result, gasReport.UsedInternally, nil -} - -// IBCPacketTimeout is available on IBC-enabled contracts and is called when an -// outgoing packet (previously sent by this contract) will provably never be executed. -// Usually handled like ack returning an error -func (vm *VM) IBCPacketTimeout( - checksum Checksum, - env types.Env, - msg types.IBCPacketTimeoutMsg, - 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 - } - msgBin, err := json.Marshal(msg) - if err != nil { - return nil, 0, err - } - data, gasReport, err := api.IBCPacketTimeout(vm.cache, checksum, envBin, msgBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) - if err != nil { - return nil, gasReport.UsedInternally, err - } - - var result types.IBCBasicResult - err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) - if err != nil { - return nil, gasReport.UsedInternally, err - } - return &result, gasReport.UsedInternally, nil -} - -func compileCost(code WasmCode) uint64 { - // CostPerByte is how much CosmWasm gas is charged *per byte* for compiling WASM code. - // Benchmarks and numbers (in SDK Gas) were discussed in: - // https://github.com/CosmWasm/wasmd/pull/634#issuecomment-938056803 - const CostPerByte uint64 = 3 * 140_000 - - return CostPerByte * uint64(len(code)) -} - -// hasSubMessages is an interface for contract results that can contain sub-messages. -type hasSubMessages interface { - SubMessages() []types.SubMsg -} - -func DeserializeResponse(gasLimit uint64, deserCost types.UFraction, gasReport *types.GasReport, data []byte, response any) error { - gasForDeserialization := deserCost.Mul(uint64(len(data))).Floor() - if gasLimit < gasForDeserialization+gasReport.UsedInternally { - return fmt.Errorf("Insufficient gas left to deserialize contract execution result (%d bytes)", len(data)) - } - gasReport.UsedInternally += gasForDeserialization - gasReport.Remaining -= gasForDeserialization - - err := json.Unmarshal(data, response) - if err != nil { - return err - } - - // All responses that have sub-messages need their payload size to be checked - const ReplyPayloadMaxBytes = 128 * 1024 // 128 KiB - if response, ok := response.(hasSubMessages); ok { - for i, m := range response.SubMessages() { - // each payload needs to be below maximum size - if len(m.Payload) > ReplyPayloadMaxBytes { - return fmt.Errorf("reply contains submessage at index %d with payload larger than %d bytes: %d bytes", i, ReplyPayloadMaxBytes, len(m.Payload)) - } - } - } - - return nil + hash := sha256.Sum256(wasm) + return Checksum(hash[:]), nil } diff --git a/lib_libwasmvm.go b/lib_libwasmvm.go new file mode 100644 index 000000000..e4f3e8565 --- /dev/null +++ b/lib_libwasmvm.go @@ -0,0 +1,574 @@ +//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). + +package cosmwasm + +import ( + "encoding/json" + "fmt" + + "github.com/CosmWasm/wasmvm/v2/internal/api" + "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. +type VM struct { + 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) { + cache, err := api.InitCache(dataDir, supportedCapabilities, cacheSize, memoryLimit) + if err != nil { + return nil, err + } + return &VM{cache: cache, printDebug: printDebug}, nil +} + +// Cleanup should be called when no longer using this instances. +// It frees resources in libwasmvm (the Rust part) and releases a lock in the base directory. +func (vm *VM) Cleanup() { + api.ReleaseCache(vm.cache) +} + +// StoreCode will compile the Wasm code, and store the resulting compiled module +// as well as the original code. Both can be referenced later via Checksum. +// This must be done one time for given code, after which it can be +// instatitated many times, and each instance called many times. +// +// For example, the code for all ERC-20 contracts should be the same. +// This function stores the code for that contract only once, but it can +// be instantiated with custom inputs in the future. +// +// Returns both the checksum, as well as the gas cost of compilation (in CosmWasm Gas) or an error. +func (vm *VM) StoreCode(code WasmCode, gasLimit uint64) (Checksum, uint64, error) { + gasCost := compileCost(code) + if gasLimit < gasCost { + return nil, gasCost, types.OutOfGasError{} + } + + 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) +} + +func (vm *VM) RemoveCode(checksum Checksum) error { + return api.RemoveCode(vm.cache, checksum) +} + +// GetCode will load the original Wasm code for the given checksum. +// This will only succeed if that checksum was previously returned from +// a call to StoreCode. +// +// This can be used so that the (short) checksum is stored in the iavl tree +// 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) +} + +// 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) +} + +// Unpin removes the guarantee of a contract to be pinned (see Pin). +// After calling this, the code may or may not remain in memory depending on +// the implementor's choice. +// Unpin is idempotent. +func (vm *VM) Unpin(checksum Checksum) error { + 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) +} + +// GetMetrics some internal metrics for monitoring purposes. +func (vm *VM) GetMetrics() (*types.Metrics, error) { + return api.GetMetrics(vm.cache) +} + +// Instantiate will create a new contract based on the given Checksum. +// We can set the initMsg (contract "genesis") here, and it then receives +// an account and address and can be invoked (Execute) many times. +// +// Storage should be set with a PrefixedKVStore that this code can safely access. +// +// Under the hood, we may recompile the wasm, use a cached native compile, or even use a cached instance +// for performance. +func (vm *VM) Instantiate( + checksum Checksum, + env types.Env, + info types.MessageInfo, + initMsg []byte, + store KVStore, + goapi GoAPI, + querier Querier, + gasMeter GasMeter, + gasLimit uint64, + deserCost types.UFraction, +) (*types.ContractResult, uint64, error) { + envBin, err := json.Marshal(env) + if err != nil { + return nil, 0, err + } + infoBin, err := json.Marshal(info) + if err != nil { + return nil, 0, err + } + data, gasReport, err := api.Instantiate(vm.cache, checksum, envBin, infoBin, initMsg, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) + if err != nil { + return nil, gasReport.UsedInternally, err + } + + var result types.ContractResult + err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) + if err != nil { + return nil, gasReport.UsedInternally, err + } + return &result, gasReport.UsedInternally, nil +} + +// Execute calls a given contract. Since the only difference between contracts with the same Checksum is the +// data in their local storage, and their address in the outside world, we need no ContractID here. +// (That is a detail for the external, sdk-facing, side). +// +// The caller is responsible for passing the correct `store` (which must have been initialized exactly once), +// and setting the env with relevant info on this instance (address, balance, etc) +func (vm *VM) Execute( + checksum Checksum, + env types.Env, + info types.MessageInfo, + executeMsg []byte, + store KVStore, + goapi GoAPI, + querier Querier, + gasMeter GasMeter, + gasLimit uint64, + deserCost types.UFraction, +) (*types.ContractResult, uint64, error) { + envBin, err := json.Marshal(env) + if err != nil { + return nil, 0, err + } + infoBin, err := json.Marshal(info) + if err != nil { + return nil, 0, err + } + data, gasReport, err := api.Execute(vm.cache, checksum, envBin, infoBin, executeMsg, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) + if err != nil { + return nil, gasReport.UsedInternally, err + } + + var result types.ContractResult + err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) + if err != nil { + return nil, gasReport.UsedInternally, err + } + return &result, gasReport.UsedInternally, nil +} + +// Query allows a client to execute a contract-specific query. If the result is not empty, it should be +// valid json-encoded data to return to the client. +// The meaning of path and data can be determined by the code. Path is the suffix of the abci.QueryRequest.Path +func (vm *VM) Query( + checksum Checksum, + env types.Env, + queryMsg []byte, + store KVStore, + goapi GoAPI, + querier Querier, + gasMeter GasMeter, + gasLimit uint64, + deserCost types.UFraction, +) (*types.QueryResult, uint64, error) { + envBin, err := json.Marshal(env) + if err != nil { + return nil, 0, err + } + data, gasReport, err := api.Query(vm.cache, checksum, envBin, queryMsg, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) + if err != nil { + return nil, gasReport.UsedInternally, err + } + + var result types.QueryResult + err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) + if err != nil { + return nil, gasReport.UsedInternally, err + } + return &result, gasReport.UsedInternally, nil +} + +// Migrate will migrate an existing contract to a new code binary. +// This takes storage of the data from the original contract and the Checksum of the new contract that should +// replace it. This allows it to run a migration step if needed, or return an error if unable to migrate +// the given data. +// +// MigrateMsg has some data on how to perform the migration. +func (vm *VM) Migrate( + checksum Checksum, + env types.Env, + migrateMsg []byte, + store KVStore, + goapi GoAPI, + querier Querier, + gasMeter GasMeter, + gasLimit uint64, + deserCost types.UFraction, +) (*types.ContractResult, uint64, error) { + envBin, err := json.Marshal(env) + if err != nil { + return nil, 0, err + } + data, gasReport, err := api.Migrate(vm.cache, checksum, envBin, migrateMsg, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) + if err != nil { + return nil, gasReport.UsedInternally, err + } + + var result types.ContractResult + err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) + if err != nil { + return nil, gasReport.UsedInternally, err + } + return &result, gasReport.UsedInternally, nil +} + +// Sudo allows native Go modules to make priviledged (sudo) calls on the contract. +// The contract can expose entry points that cannot be triggered by any transaction, but only via +// native Go modules, and delegate the access control to the system. +// +// These work much like Migrate (same scenario) but allows custom apps to extend the priviledged entry points +// without forking cosmwasm-vm. +func (vm *VM) Sudo( + checksum Checksum, + env types.Env, + sudoMsg []byte, + store KVStore, + goapi GoAPI, + querier Querier, + gasMeter GasMeter, + gasLimit uint64, + deserCost types.UFraction, +) (*types.ContractResult, uint64, error) { + envBin, err := json.Marshal(env) + if err != nil { + return nil, 0, err + } + data, gasReport, err := api.Sudo(vm.cache, checksum, envBin, sudoMsg, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) + if err != nil { + return nil, gasReport.UsedInternally, err + } + + var result types.ContractResult + err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) + if err != nil { + return nil, gasReport.UsedInternally, err + } + return &result, gasReport.UsedInternally, nil +} + +// Reply allows the native Go wasm modules to make a priviledged call to return the result +// of executing a SubMsg. +// +// These work much like Sudo (same scenario) but focuses on one specific case (and one message type) +func (vm *VM) Reply( + checksum Checksum, + env types.Env, + reply types.Reply, + store KVStore, + goapi GoAPI, + querier Querier, + gasMeter GasMeter, + gasLimit uint64, + deserCost types.UFraction, +) (*types.ContractResult, uint64, error) { + envBin, err := json.Marshal(env) + if err != nil { + return nil, 0, err + } + replyBin, err := json.Marshal(reply) + if err != nil { + return nil, 0, err + } + data, gasReport, err := api.Reply(vm.cache, checksum, envBin, replyBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) + if err != nil { + return nil, gasReport.UsedInternally, err + } + + var result types.ContractResult + err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) + if err != nil { + return nil, gasReport.UsedInternally, err + } + return &result, gasReport.UsedInternally, nil +} + +// IBCChannelOpen is available on IBC-enabled contracts and is a hook to call into +// during the handshake pahse +func (vm *VM) IBCChannelOpen( + checksum Checksum, + env types.Env, + msg types.IBCChannelOpenMsg, + store KVStore, + goapi GoAPI, + querier Querier, + gasMeter GasMeter, + gasLimit uint64, + deserCost types.UFraction, +) (*types.IBCChannelOpenResult, uint64, error) { + envBin, err := json.Marshal(env) + if err != nil { + return nil, 0, err + } + msgBin, err := json.Marshal(msg) + if err != nil { + return nil, 0, err + } + data, gasReport, err := api.IBCChannelOpen(vm.cache, checksum, envBin, msgBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) + if err != nil { + return nil, gasReport.UsedInternally, err + } + + var result types.IBCChannelOpenResult + err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) + if err != nil { + return nil, gasReport.UsedInternally, err + } + return &result, gasReport.UsedInternally, nil +} + +// IBCChannelConnect is available on IBC-enabled contracts and is a hook to call into +// during the handshake pahse +func (vm *VM) IBCChannelConnect( + checksum Checksum, + env types.Env, + msg types.IBCChannelConnectMsg, + 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 + } + msgBin, err := json.Marshal(msg) + if err != nil { + return nil, 0, err + } + data, gasReport, err := api.IBCChannelConnect(vm.cache, checksum, envBin, msgBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) + if err != nil { + return nil, gasReport.UsedInternally, err + } + + var result types.IBCBasicResult + err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) + if err != nil { + return nil, gasReport.UsedInternally, err + } + return &result, gasReport.UsedInternally, nil +} + +// IBCChannelClose is available on IBC-enabled contracts and is a hook to call into +// at the end of the channel lifetime +func (vm *VM) IBCChannelClose( + checksum Checksum, + env types.Env, + msg types.IBCChannelCloseMsg, + 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 + } + msgBin, err := json.Marshal(msg) + if err != nil { + return nil, 0, err + } + data, gasReport, err := api.IBCChannelClose(vm.cache, checksum, envBin, msgBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) + if err != nil { + return nil, gasReport.UsedInternally, err + } + + var result types.IBCBasicResult + err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) + if err != nil { + return nil, gasReport.UsedInternally, err + } + return &result, gasReport.UsedInternally, nil +} + +// IBCPacketReceive is available on IBC-enabled contracts and is called when an incoming +// packet is received on a channel belonging to this contract +func (vm *VM) IBCPacketReceive( + checksum Checksum, + env types.Env, + msg types.IBCPacketReceiveMsg, + store KVStore, + goapi GoAPI, + querier Querier, + gasMeter GasMeter, + gasLimit uint64, + deserCost types.UFraction, +) (*types.IBCReceiveResult, uint64, error) { + envBin, err := json.Marshal(env) + if err != nil { + return nil, 0, err + } + msgBin, err := json.Marshal(msg) + if err != nil { + return nil, 0, err + } + data, gasReport, err := api.IBCPacketReceive(vm.cache, checksum, envBin, msgBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) + if err != nil { + return nil, gasReport.UsedInternally, err + } + + var result types.IBCReceiveResult + err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) + if err != nil { + return nil, gasReport.UsedInternally, err + } + 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) { + envBin, err := json.Marshal(env) + if err != nil { + return nil, 0, err + } + msgBin, err := json.Marshal(msg) + if err != nil { + return nil, 0, err + } + data, gasReport, err := api.IBCPacketAck(vm.cache, checksum, envBin, msgBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) + if err != nil { + return nil, gasReport.UsedInternally, err + } + + var result types.IBCBasicResult + err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) + if err != nil { + return nil, gasReport.UsedInternally, err + } + return &result, gasReport.UsedInternally, nil +} + +// IBCPacketTimeout is available on IBC-enabled contracts and is called when an +// outgoing packet (previously sent by this contract) will provably never be executed. +// Usually handled like ack returning an error +func (vm *VM) IBCPacketTimeout( + checksum Checksum, + env types.Env, + msg types.IBCPacketTimeoutMsg, + 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 + } + msgBin, err := json.Marshal(msg) + if err != nil { + return nil, 0, err + } + data, gasReport, err := api.IBCPacketTimeout(vm.cache, checksum, envBin, msgBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) + if err != nil { + return nil, gasReport.UsedInternally, err + } + + var result types.IBCBasicResult + err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) + if err != nil { + return nil, gasReport.UsedInternally, err + } + return &result, gasReport.UsedInternally, nil +} + +func compileCost(code WasmCode) uint64 { + // CostPerByte is how much CosmWasm gas is charged *per byte* for compiling WASM code. + // Benchmarks and numbers (in SDK Gas) were discussed in: + // https://github.com/CosmWasm/wasmd/pull/634#issuecomment-938056803 + const CostPerByte uint64 = 3 * 140_000 + + return CostPerByte * uint64(len(code)) +} + +// hasSubMessages is an interface for contract results that can contain sub-messages. +type hasSubMessages interface { + SubMessages() []types.SubMsg +} + +func DeserializeResponse(gasLimit uint64, deserCost types.UFraction, gasReport *types.GasReport, data []byte, response any) error { + gasForDeserialization := deserCost.Mul(uint64(len(data))).Floor() + if gasLimit < gasForDeserialization+gasReport.UsedInternally { + return fmt.Errorf("Insufficient gas left to deserialize contract execution result (%d bytes)", len(data)) + } + gasReport.UsedInternally += gasForDeserialization + gasReport.Remaining -= gasForDeserialization + + err := json.Unmarshal(data, response) + if err != nil { + return err + } + + // All responses that have sub-messages need their payload size to be checked + const ReplyPayloadMaxBytes = 128 * 1024 // 128 KiB + if response, ok := response.(hasSubMessages); ok { + for i, m := range response.SubMessages() { + // each payload needs to be below maximum size + if len(m.Payload) > ReplyPayloadMaxBytes { + return fmt.Errorf("reply contains submessage at index %d with payload larger than %d bytes: %d bytes", i, ReplyPayloadMaxBytes, len(m.Payload)) + } + } + } + + return nil +} diff --git a/lib_libwasmvm_test.go b/lib_libwasmvm_test.go new file mode 100644 index 000000000..82327ecaf --- /dev/null +++ b/lib_libwasmvm_test.go @@ -0,0 +1,412 @@ +//go:build cgo && !nolink_libwasmvm + +package cosmwasm + +import ( + "encoding/json" + "fmt" + "math" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/CosmWasm/wasmvm/v2/internal/api" + "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"} + +const ( + CYBERPUNK_TEST_CONTRACT = "./testdata/cyberpunk.wasm" + HACKATOM_TEST_CONTRACT = "./testdata/hackatom.wasm" +) + +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) + require.NoError(t, err) + + t.Cleanup(func() { + vm.Cleanup() + os.RemoveAll(tmpdir) + }) + return vm +} + +func createTestContract(t *testing.T, vm *VM, path string) Checksum { + wasm, err := os.ReadFile(path) + require.NoError(t, err) + checksum, _, err := vm.StoreCode(wasm, TESTING_GAS_LIMIT) + require.NoError(t, err) + return checksum +} + +func TestStoreCode(t *testing.T) { + vm := withVM(t) + + // Valid hackatom contract + { + wasm, err := os.ReadFile(HACKATOM_TEST_CONTRACT) + require.NoError(t, err) + _, _, err = vm.StoreCode(wasm, TESTING_GAS_LIMIT) + require.NoError(t, err) + } + + // Valid cyberpunk contract + { + wasm, err := os.ReadFile(CYBERPUNK_TEST_CONTRACT) + require.NoError(t, err) + _, _, err = vm.StoreCode(wasm, TESTING_GAS_LIMIT) + require.NoError(t, err) + } + + // Valid Wasm with no exports + { + // echo '(module)' | wat2wasm - -o empty.wasm + // hexdump -C < empty.wasm + + wasm := []byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00} + _, _, err := vm.StoreCode(wasm, TESTING_GAS_LIMIT) + require.ErrorContains(t, err, "Error during static Wasm validation: Wasm contract must contain exactly one memory") + } + + // No Wasm + { + wasm := []byte("foobar") + _, _, err := vm.StoreCode(wasm, TESTING_GAS_LIMIT) + require.ErrorContains(t, err, "Wasm bytecode could not be deserialized") + } + + // Empty + { + wasm := []byte("") + _, _, err := vm.StoreCode(wasm, TESTING_GAS_LIMIT) + require.ErrorContains(t, err, "Wasm bytecode could not be deserialized") + } + + // Nil + { + var wasm []byte = nil + _, _, err := vm.StoreCode(wasm, TESTING_GAS_LIMIT) + require.ErrorContains(t, err, "Null/Nil argument: wasm") + } +} + +func TestStoreCodeAndGet(t *testing.T) { + vm := withVM(t) + + wasm, err := os.ReadFile(HACKATOM_TEST_CONTRACT) + require.NoError(t, err) + + checksum, _, err := vm.StoreCode(wasm, TESTING_GAS_LIMIT) + require.NoError(t, err) + + code, err := vm.GetCode(checksum) + require.NoError(t, err) + require.Equal(t, WasmCode(wasm), code) +} + +func TestRemoveCode(t *testing.T) { + vm := withVM(t) + + wasm, err := os.ReadFile(HACKATOM_TEST_CONTRACT) + require.NoError(t, err) + + checksum, _, err := vm.StoreCode(wasm, TESTING_GAS_LIMIT) + require.NoError(t, err) + + err = vm.RemoveCode(checksum) + require.NoError(t, err) + + err = vm.RemoveCode(checksum) + require.ErrorContains(t, err, "Wasm file does not exist") +} + +func TestHappyPath(t *testing.T) { + vm := withVM(t) + checksum := createTestContract(t, vm, HACKATOM_TEST_CONTRACT) + + deserCost := types.UFraction{Numerator: 1, Denominator: 1} + gasMeter1 := api.NewMockGasMeter(TESTING_GAS_LIMIT) + // instantiate it with this store + store := api.NewLookup(gasMeter1) + goapi := api.NewMockAPI() + balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} + querier := api.DefaultQuerier(api.MOCK_CONTRACT_ADDR, balance) + + // instantiate + env := api.MockEnv() + info := api.MockInfo("creator", nil) + msg := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) + i, _, err := vm.Instantiate(checksum, env, info, msg, store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) + require.NoError(t, err) + require.NotNil(t, i.Ok) + ires := i.Ok + require.Equal(t, 0, len(ires.Messages)) + + // execute + gasMeter2 := api.NewMockGasMeter(TESTING_GAS_LIMIT) + store.SetGasMeter(gasMeter2) + env = api.MockEnv() + info = api.MockInfo("fred", nil) + h, _, err := vm.Execute(checksum, env, info, []byte(`{"release":{}}`), store, *goapi, querier, gasMeter2, TESTING_GAS_LIMIT, deserCost) + require.NoError(t, err) + require.NotNil(t, h.Ok) + hres := h.Ok + require.Equal(t, 1, len(hres.Messages)) + + // make sure it read the balance properly and we got 250 atoms + dispatch := hres.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, hres.Data) +} + +func TestEnv(t *testing.T) { + vm := withVM(t) + checksum := createTestContract(t, vm, CYBERPUNK_TEST_CONTRACT) + + deserCost := types.UFraction{Numerator: 1, Denominator: 1} + gasMeter1 := api.NewMockGasMeter(TESTING_GAS_LIMIT) + // instantiate it with this store + store := api.NewLookup(gasMeter1) + goapi := api.NewMockAPI() + balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} + querier := api.DefaultQuerier(api.MOCK_CONTRACT_ADDR, balance) + + // instantiate + env := api.MockEnv() + info := api.MockInfo("creator", nil) + i, _, err := vm.Instantiate(checksum, env, info, []byte(`{}`), store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) + require.NoError(t, err) + require.NotNil(t, i.Ok) + ires := i.Ok + require.Equal(t, 0, len(ires.Messages)) + + // Execute mirror env without Transaction + env = types.Env{ + Block: types.BlockInfo{ + Height: 444, + Time: 1955939743_123456789, + ChainID: "nice-chain", + }, + Contract: types.ContractInfo{ + Address: "wasm10dyr9899g6t0pelew4nvf4j5c3jcgv0r5d3a5l", + }, + Transaction: nil, + } + info = api.MockInfo("creator", nil) + msg := []byte(`{"mirror_env": {}}`) + i, _, err = vm.Execute(checksum, env, info, msg, store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) + require.NoError(t, err) + require.NotNil(t, i.Ok) + ires = i.Ok + expected, _ := json.Marshal(env) + require.Equal(t, expected, ires.Data) + + // Execute mirror env with Transaction + env = types.Env{ + Block: types.BlockInfo{ + Height: 444, + Time: 1955939743_123456789, + ChainID: "nice-chain", + }, + Contract: types.ContractInfo{ + Address: "wasm10dyr9899g6t0pelew4nvf4j5c3jcgv0r5d3a5l", + }, + Transaction: &types.TransactionInfo{ + Index: 18, + }, + } + info = api.MockInfo("creator", nil) + msg = []byte(`{"mirror_env": {}}`) + i, _, err = vm.Execute(checksum, env, info, msg, store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) + require.NoError(t, err) + require.NotNil(t, i.Ok) + ires = i.Ok + expected, _ = json.Marshal(env) + require.Equal(t, expected, ires.Data) +} + +func TestGetMetrics(t *testing.T) { + vm := withVM(t) + + // GetMetrics 1 + metrics, err := vm.GetMetrics() + require.NoError(t, err) + assert.Equal(t, &types.Metrics{}, metrics) + + // Create contract + checksum := createTestContract(t, vm, HACKATOM_TEST_CONTRACT) + + deserCost := types.UFraction{Numerator: 1, Denominator: 1} + + // GetMetrics 2 + metrics, err = vm.GetMetrics() + require.NoError(t, err) + assert.Equal(t, &types.Metrics{}, metrics) + + // Instantiate 1 + gasMeter1 := api.NewMockGasMeter(TESTING_GAS_LIMIT) + // instantiate it with this store + store := api.NewLookup(gasMeter1) + goapi := api.NewMockAPI() + balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} + querier := api.DefaultQuerier(api.MOCK_CONTRACT_ADDR, balance) + + env := api.MockEnv() + info := api.MockInfo("creator", nil) + msg1 := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) + i, _, err := vm.Instantiate(checksum, env, info, msg1, store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) + require.NoError(t, err) + require.NotNil(t, i.Ok) + ires := i.Ok + require.Equal(t, 0, len(ires.Messages)) + + // GetMetrics 3 + metrics, err = vm.GetMetrics() + 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) + t.Log(metrics.SizeMemoryCache) + require.InEpsilon(t, 2832576, metrics.SizeMemoryCache, 0.25) + + // Instantiate 2 + msg2 := []byte(`{"verifier": "fred", "beneficiary": "susi"}`) + i, _, err = vm.Instantiate(checksum, env, info, msg2, store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) + require.NoError(t, err) + require.NotNil(t, i.Ok) + ires = i.Ok + require.Equal(t, 0, len(ires.Messages)) + + // GetMetrics 4 + metrics, err = vm.GetMetrics() + 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, 2832576, metrics.SizeMemoryCache, 0.25) + + // Pin + err = vm.Pin(checksum) + require.NoError(t, err) + + // GetMetrics 5 + metrics, err = vm.GetMetrics() + 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, 2832576, metrics.SizePinnedMemoryCache, 0.25) + require.InEpsilon(t, 2832576, metrics.SizeMemoryCache, 0.25) + + // Instantiate 3 + msg3 := []byte(`{"verifier": "fred", "beneficiary": "bert"}`) + i, _, err = vm.Instantiate(checksum, env, info, msg3, store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) + require.NoError(t, err) + require.NotNil(t, i.Ok) + ires = i.Ok + require.Equal(t, 0, len(ires.Messages)) + + // GetMetrics 6 + metrics, err = vm.GetMetrics() + 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, 2832576, metrics.SizePinnedMemoryCache, 0.25) + require.InEpsilon(t, 2832576, metrics.SizeMemoryCache, 0.25) + + // Unpin + err = vm.Unpin(checksum) + require.NoError(t, err) + + // GetMetrics 7 + metrics, err = vm.GetMetrics() + 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, 2832576, metrics.SizeMemoryCache, 0.25) + + // Instantiate 4 + msg4 := []byte(`{"verifier": "fred", "beneficiary": "jeff"}`) + i, _, err = vm.Instantiate(checksum, env, info, msg4, store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) + require.NoError(t, err) + require.NotNil(t, i.Ok) + ires = i.Ok + require.Equal(t, 0, len(ires.Messages)) + + // GetMetrics 8 + metrics, err = vm.GetMetrics() + 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, 2832576, metrics.SizeMemoryCache, 0.25) +} + +func TestLongPayloadDeserialization(t *testing.T) { + deserCost := types.UFraction{Numerator: 1, Denominator: 1} + gasReport := types.GasReport{} + + // Create a valid payload + validPayload := make([]byte, 128*1024) + validPayloadJSON, err := json.Marshal(validPayload) + require.NoError(t, err) + resultJson := []byte(fmt.Sprintf(`{"ok":{"messages":[{"id":0,"msg":{"bank":{"send":{"to_address":"bob","amount":[{"denom":"ATOM","amount":"250"}]}}},"payload":%s,"reply_on":"never"}],"data":"8Auq","attributes":[],"events":[]}}`, validPayloadJSON)) + + // Test that a valid payload can be deserialized + var result types.ContractResult + err = DeserializeResponse(math.MaxUint64, deserCost, &gasReport, resultJson, &result) + require.NoError(t, err) + require.Equal(t, validPayload, result.Ok.Messages[0].Payload) + + // Create an invalid payload (too large) + invalidPayload := make([]byte, 128*1024+1) + invalidPayloadJSON, err := json.Marshal(invalidPayload) + require.NoError(t, err) + resultJson = []byte(fmt.Sprintf(`{"ok":{"messages":[{"id":0,"msg":{"bank":{"send":{"to_address":"bob","amount":[{"denom":"ATOM","amount":"250"}]}}},"payload":%s,"reply_on":"never"}],"attributes":[],"events":[]}}`, invalidPayloadJSON)) + + // Test that an invalid payload cannot be deserialized + err = DeserializeResponse(math.MaxUint64, deserCost, &gasReport, resultJson, &result) + require.Error(t, err) + require.Contains(t, err.Error(), "payload") + + // Test that an invalid payload cannot be deserialized to IBCBasicResult + var ibcResult types.IBCBasicResult + err = DeserializeResponse(math.MaxUint64, deserCost, &gasReport, resultJson, &ibcResult) + require.Error(t, err) + require.Contains(t, err.Error(), "payload") + + // Test that an invalid payload cannot be deserialized to IBCReceiveResult + var ibcReceiveResult types.IBCReceiveResult + err = DeserializeResponse(math.MaxUint64, deserCost, &gasReport, resultJson, &ibcReceiveResult) + require.Error(t, err) + require.Contains(t, err.Error(), "payload") +} diff --git a/lib_no_cgo.go b/lib_no_cgo.go deleted file mode 100644 index 45d263d22..000000000 --- a/lib_no_cgo.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.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 not start with Wasm magic number") - } - hash := sha256.Sum256(wasm) - return Checksum(hash[:]), nil -} diff --git a/lib_no_cgo_test.go b/lib_no_cgo_test.go deleted file mode 100644 index 3cc5cf916..000000000 --- a/lib_no_cgo_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package cosmwasm - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/CosmWasm/wasmvm/v2/types" -) - -func TestCreateChecksum(t *testing.T) { - // nil - _, err := CreateChecksum(nil) - require.ErrorContains(t, err, "nil or empty") - - // empty - _, err = CreateChecksum([]byte{}) - require.ErrorContains(t, err, "nil or empty") - - // short - _, err = CreateChecksum([]byte("\x00\x61\x73")) - require.ErrorContains(t, err, " shorter than 4 bytes") - - // Wasm blob returns correct hash - // echo "(module)" > my.wat && wat2wasm my.wat && hexdump -C my.wasm && sha256sum my.wasm - checksum, err := CreateChecksum([]byte("\x00\x61\x73\x6d\x01\x00\x00\x00")) - require.NoError(t, err) - require.Equal(t, types.ForceNewChecksum("93a44bbb96c751218e4c00d479e4c14358122a389acca16205b1e4d0dc5f9476"), checksum) - - // Text file fails - _, err = CreateChecksum([]byte("Hello world")) - require.ErrorContains(t, err, "do not not start with Wasm magic number") -} diff --git a/lib_test.go b/lib_test.go index 82327ecaf..3cc5cf916 100644 --- a/lib_test.go +++ b/lib_test.go @@ -1,412 +1,33 @@ -//go:build cgo && !nolink_libwasmvm - package cosmwasm import ( - "encoding/json" - "fmt" - "math" - "os" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/CosmWasm/wasmvm/v2/internal/api" "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"} +func TestCreateChecksum(t *testing.T) { + // nil + _, err := CreateChecksum(nil) + require.ErrorContains(t, err, "nil or empty") -const ( - CYBERPUNK_TEST_CONTRACT = "./testdata/cyberpunk.wasm" - HACKATOM_TEST_CONTRACT = "./testdata/hackatom.wasm" -) - -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) - require.NoError(t, err) + // empty + _, err = CreateChecksum([]byte{}) + require.ErrorContains(t, err, "nil or empty") - t.Cleanup(func() { - vm.Cleanup() - os.RemoveAll(tmpdir) - }) - return vm -} + // short + _, err = CreateChecksum([]byte("\x00\x61\x73")) + require.ErrorContains(t, err, " shorter than 4 bytes") -func createTestContract(t *testing.T, vm *VM, path string) Checksum { - wasm, err := os.ReadFile(path) - require.NoError(t, err) - checksum, _, err := vm.StoreCode(wasm, TESTING_GAS_LIMIT) + // Wasm blob returns correct hash + // echo "(module)" > my.wat && wat2wasm my.wat && hexdump -C my.wasm && sha256sum my.wasm + checksum, err := CreateChecksum([]byte("\x00\x61\x73\x6d\x01\x00\x00\x00")) require.NoError(t, err) - return checksum -} - -func TestStoreCode(t *testing.T) { - vm := withVM(t) - - // Valid hackatom contract - { - wasm, err := os.ReadFile(HACKATOM_TEST_CONTRACT) - require.NoError(t, err) - _, _, err = vm.StoreCode(wasm, TESTING_GAS_LIMIT) - require.NoError(t, err) - } - - // Valid cyberpunk contract - { - wasm, err := os.ReadFile(CYBERPUNK_TEST_CONTRACT) - require.NoError(t, err) - _, _, err = vm.StoreCode(wasm, TESTING_GAS_LIMIT) - require.NoError(t, err) - } - - // Valid Wasm with no exports - { - // echo '(module)' | wat2wasm - -o empty.wasm - // hexdump -C < empty.wasm - - wasm := []byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00} - _, _, err := vm.StoreCode(wasm, TESTING_GAS_LIMIT) - require.ErrorContains(t, err, "Error during static Wasm validation: Wasm contract must contain exactly one memory") - } - - // No Wasm - { - wasm := []byte("foobar") - _, _, err := vm.StoreCode(wasm, TESTING_GAS_LIMIT) - require.ErrorContains(t, err, "Wasm bytecode could not be deserialized") - } - - // Empty - { - wasm := []byte("") - _, _, err := vm.StoreCode(wasm, TESTING_GAS_LIMIT) - require.ErrorContains(t, err, "Wasm bytecode could not be deserialized") - } - - // Nil - { - var wasm []byte = nil - _, _, err := vm.StoreCode(wasm, TESTING_GAS_LIMIT) - require.ErrorContains(t, err, "Null/Nil argument: wasm") - } -} - -func TestStoreCodeAndGet(t *testing.T) { - vm := withVM(t) - - wasm, err := os.ReadFile(HACKATOM_TEST_CONTRACT) - require.NoError(t, err) - - checksum, _, err := vm.StoreCode(wasm, TESTING_GAS_LIMIT) - require.NoError(t, err) - - code, err := vm.GetCode(checksum) - require.NoError(t, err) - require.Equal(t, WasmCode(wasm), code) -} - -func TestRemoveCode(t *testing.T) { - vm := withVM(t) - - wasm, err := os.ReadFile(HACKATOM_TEST_CONTRACT) - require.NoError(t, err) - - checksum, _, err := vm.StoreCode(wasm, TESTING_GAS_LIMIT) - require.NoError(t, err) - - err = vm.RemoveCode(checksum) - require.NoError(t, err) - - err = vm.RemoveCode(checksum) - require.ErrorContains(t, err, "Wasm file does not exist") -} - -func TestHappyPath(t *testing.T) { - vm := withVM(t) - checksum := createTestContract(t, vm, HACKATOM_TEST_CONTRACT) - - deserCost := types.UFraction{Numerator: 1, Denominator: 1} - gasMeter1 := api.NewMockGasMeter(TESTING_GAS_LIMIT) - // instantiate it with this store - store := api.NewLookup(gasMeter1) - goapi := api.NewMockAPI() - balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} - querier := api.DefaultQuerier(api.MOCK_CONTRACT_ADDR, balance) - - // instantiate - env := api.MockEnv() - info := api.MockInfo("creator", nil) - msg := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) - i, _, err := vm.Instantiate(checksum, env, info, msg, store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) - require.NoError(t, err) - require.NotNil(t, i.Ok) - ires := i.Ok - require.Equal(t, 0, len(ires.Messages)) - - // execute - gasMeter2 := api.NewMockGasMeter(TESTING_GAS_LIMIT) - store.SetGasMeter(gasMeter2) - env = api.MockEnv() - info = api.MockInfo("fred", nil) - h, _, err := vm.Execute(checksum, env, info, []byte(`{"release":{}}`), store, *goapi, querier, gasMeter2, TESTING_GAS_LIMIT, deserCost) - require.NoError(t, err) - require.NotNil(t, h.Ok) - hres := h.Ok - require.Equal(t, 1, len(hres.Messages)) - - // make sure it read the balance properly and we got 250 atoms - dispatch := hres.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, hres.Data) -} - -func TestEnv(t *testing.T) { - vm := withVM(t) - checksum := createTestContract(t, vm, CYBERPUNK_TEST_CONTRACT) - - deserCost := types.UFraction{Numerator: 1, Denominator: 1} - gasMeter1 := api.NewMockGasMeter(TESTING_GAS_LIMIT) - // instantiate it with this store - store := api.NewLookup(gasMeter1) - goapi := api.NewMockAPI() - balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} - querier := api.DefaultQuerier(api.MOCK_CONTRACT_ADDR, balance) - - // instantiate - env := api.MockEnv() - info := api.MockInfo("creator", nil) - i, _, err := vm.Instantiate(checksum, env, info, []byte(`{}`), store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) - require.NoError(t, err) - require.NotNil(t, i.Ok) - ires := i.Ok - require.Equal(t, 0, len(ires.Messages)) - - // Execute mirror env without Transaction - env = types.Env{ - Block: types.BlockInfo{ - Height: 444, - Time: 1955939743_123456789, - ChainID: "nice-chain", - }, - Contract: types.ContractInfo{ - Address: "wasm10dyr9899g6t0pelew4nvf4j5c3jcgv0r5d3a5l", - }, - Transaction: nil, - } - info = api.MockInfo("creator", nil) - msg := []byte(`{"mirror_env": {}}`) - i, _, err = vm.Execute(checksum, env, info, msg, store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) - require.NoError(t, err) - require.NotNil(t, i.Ok) - ires = i.Ok - expected, _ := json.Marshal(env) - require.Equal(t, expected, ires.Data) - - // Execute mirror env with Transaction - env = types.Env{ - Block: types.BlockInfo{ - Height: 444, - Time: 1955939743_123456789, - ChainID: "nice-chain", - }, - Contract: types.ContractInfo{ - Address: "wasm10dyr9899g6t0pelew4nvf4j5c3jcgv0r5d3a5l", - }, - Transaction: &types.TransactionInfo{ - Index: 18, - }, - } - info = api.MockInfo("creator", nil) - msg = []byte(`{"mirror_env": {}}`) - i, _, err = vm.Execute(checksum, env, info, msg, store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) - require.NoError(t, err) - require.NotNil(t, i.Ok) - ires = i.Ok - expected, _ = json.Marshal(env) - require.Equal(t, expected, ires.Data) -} - -func TestGetMetrics(t *testing.T) { - vm := withVM(t) - - // GetMetrics 1 - metrics, err := vm.GetMetrics() - require.NoError(t, err) - assert.Equal(t, &types.Metrics{}, metrics) - - // Create contract - checksum := createTestContract(t, vm, HACKATOM_TEST_CONTRACT) - - deserCost := types.UFraction{Numerator: 1, Denominator: 1} - - // GetMetrics 2 - metrics, err = vm.GetMetrics() - require.NoError(t, err) - assert.Equal(t, &types.Metrics{}, metrics) - - // Instantiate 1 - gasMeter1 := api.NewMockGasMeter(TESTING_GAS_LIMIT) - // instantiate it with this store - store := api.NewLookup(gasMeter1) - goapi := api.NewMockAPI() - balance := types.Array[types.Coin]{types.NewCoin(250, "ATOM")} - querier := api.DefaultQuerier(api.MOCK_CONTRACT_ADDR, balance) - - env := api.MockEnv() - info := api.MockInfo("creator", nil) - msg1 := []byte(`{"verifier": "fred", "beneficiary": "bob"}`) - i, _, err := vm.Instantiate(checksum, env, info, msg1, store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) - require.NoError(t, err) - require.NotNil(t, i.Ok) - ires := i.Ok - require.Equal(t, 0, len(ires.Messages)) - - // GetMetrics 3 - metrics, err = vm.GetMetrics() - 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) - t.Log(metrics.SizeMemoryCache) - require.InEpsilon(t, 2832576, metrics.SizeMemoryCache, 0.25) - - // Instantiate 2 - msg2 := []byte(`{"verifier": "fred", "beneficiary": "susi"}`) - i, _, err = vm.Instantiate(checksum, env, info, msg2, store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) - require.NoError(t, err) - require.NotNil(t, i.Ok) - ires = i.Ok - require.Equal(t, 0, len(ires.Messages)) - - // GetMetrics 4 - metrics, err = vm.GetMetrics() - 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, 2832576, metrics.SizeMemoryCache, 0.25) - - // Pin - err = vm.Pin(checksum) - require.NoError(t, err) - - // GetMetrics 5 - metrics, err = vm.GetMetrics() - 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, 2832576, metrics.SizePinnedMemoryCache, 0.25) - require.InEpsilon(t, 2832576, metrics.SizeMemoryCache, 0.25) - - // Instantiate 3 - msg3 := []byte(`{"verifier": "fred", "beneficiary": "bert"}`) - i, _, err = vm.Instantiate(checksum, env, info, msg3, store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) - require.NoError(t, err) - require.NotNil(t, i.Ok) - ires = i.Ok - require.Equal(t, 0, len(ires.Messages)) - - // GetMetrics 6 - metrics, err = vm.GetMetrics() - 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, 2832576, metrics.SizePinnedMemoryCache, 0.25) - require.InEpsilon(t, 2832576, metrics.SizeMemoryCache, 0.25) - - // Unpin - err = vm.Unpin(checksum) - require.NoError(t, err) - - // GetMetrics 7 - metrics, err = vm.GetMetrics() - 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, 2832576, metrics.SizeMemoryCache, 0.25) - - // Instantiate 4 - msg4 := []byte(`{"verifier": "fred", "beneficiary": "jeff"}`) - i, _, err = vm.Instantiate(checksum, env, info, msg4, store, *goapi, querier, gasMeter1, TESTING_GAS_LIMIT, deserCost) - require.NoError(t, err) - require.NotNil(t, i.Ok) - ires = i.Ok - require.Equal(t, 0, len(ires.Messages)) - - // GetMetrics 8 - metrics, err = vm.GetMetrics() - 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, 2832576, metrics.SizeMemoryCache, 0.25) -} - -func TestLongPayloadDeserialization(t *testing.T) { - deserCost := types.UFraction{Numerator: 1, Denominator: 1} - gasReport := types.GasReport{} - - // Create a valid payload - validPayload := make([]byte, 128*1024) - validPayloadJSON, err := json.Marshal(validPayload) - require.NoError(t, err) - resultJson := []byte(fmt.Sprintf(`{"ok":{"messages":[{"id":0,"msg":{"bank":{"send":{"to_address":"bob","amount":[{"denom":"ATOM","amount":"250"}]}}},"payload":%s,"reply_on":"never"}],"data":"8Auq","attributes":[],"events":[]}}`, validPayloadJSON)) - - // Test that a valid payload can be deserialized - var result types.ContractResult - err = DeserializeResponse(math.MaxUint64, deserCost, &gasReport, resultJson, &result) - require.NoError(t, err) - require.Equal(t, validPayload, result.Ok.Messages[0].Payload) - - // Create an invalid payload (too large) - invalidPayload := make([]byte, 128*1024+1) - invalidPayloadJSON, err := json.Marshal(invalidPayload) - require.NoError(t, err) - resultJson = []byte(fmt.Sprintf(`{"ok":{"messages":[{"id":0,"msg":{"bank":{"send":{"to_address":"bob","amount":[{"denom":"ATOM","amount":"250"}]}}},"payload":%s,"reply_on":"never"}],"attributes":[],"events":[]}}`, invalidPayloadJSON)) - - // Test that an invalid payload cannot be deserialized - err = DeserializeResponse(math.MaxUint64, deserCost, &gasReport, resultJson, &result) - require.Error(t, err) - require.Contains(t, err.Error(), "payload") - - // Test that an invalid payload cannot be deserialized to IBCBasicResult - var ibcResult types.IBCBasicResult - err = DeserializeResponse(math.MaxUint64, deserCost, &gasReport, resultJson, &ibcResult) - require.Error(t, err) - require.Contains(t, err.Error(), "payload") + require.Equal(t, types.ForceNewChecksum("93a44bbb96c751218e4c00d479e4c14358122a389acca16205b1e4d0dc5f9476"), checksum) - // Test that an invalid payload cannot be deserialized to IBCReceiveResult - var ibcReceiveResult types.IBCReceiveResult - err = DeserializeResponse(math.MaxUint64, deserCost, &gasReport, resultJson, &ibcReceiveResult) - require.Error(t, err) - require.Contains(t, err.Error(), "payload") + // Text file fails + _, err = CreateChecksum([]byte("Hello world")) + require.ErrorContains(t, err, "do not not start with Wasm magic number") }