diff --git a/go.mod b/go.mod index b0bc35e..3911c2c 100644 --- a/go.mod +++ b/go.mod @@ -154,7 +154,7 @@ require ( replace ( // Use the cosmos-flavored keyring library github.com/99designs/keyring => github.com/cosmos/keyring v1.1.7-0.20210622111912-ef00f8ac3d76 - github.com/CosmWasm/wasmvm => github.com/sei-protocol/sei-wasmvm v1.5.4-sei.0.0.2 + github.com/CosmWasm/wasmvm => github.com/sei-protocol/sei-wasmvm v1.5.4-sei.0.0.3.0.20250819144601-14afbfe2bf78 github.com/confio/ics23/go => github.com/cosmos/cosmos-sdk/ics23/go v0.8.0 github.com/cosmos/cosmos-sdk => github.com/sei-protocol/sei-cosmos v0.3.56 github.com/cosmos/iavl => github.com/sei-protocol/sei-iavl v0.1.9 diff --git a/go.sum b/go.sum index a52a853..0d3356f 100644 --- a/go.sum +++ b/go.sum @@ -782,8 +782,8 @@ github.com/sei-protocol/sei-tendermint v0.5.9 h1:GLAuHGuCd1eDLfoU4uFMbqQo7z47XSV github.com/sei-protocol/sei-tendermint v0.5.9/go.mod h1:ip8M5kkQf8JC05np1xd4KR33ttJoseJi28Ea28ui85E= github.com/sei-protocol/sei-tm-db v0.0.5 h1:3WONKdSXEqdZZeLuWYfK5hP37TJpfaUa13vAyAlvaQY= github.com/sei-protocol/sei-tm-db v0.0.5/go.mod h1:Cpa6rGyczgthq7/0pI31jys2Fw0Nfrc+/jKdP1prVqY= -github.com/sei-protocol/sei-wasmvm v1.5.4-sei.0.0.2 h1:bhfjHJFhYe/Ffq83UsSJrCcZPkAx5TSEJl1z5lBd6ow= -github.com/sei-protocol/sei-wasmvm v1.5.4-sei.0.0.2/go.mod h1:Q0bSEtlktzh7W2hhEaifrFp1Erx11ckQZmjq8FLCyys= +github.com/sei-protocol/sei-wasmvm v1.5.4-sei.0.0.3.0.20250819144601-14afbfe2bf78 h1:jVFJVae967/K3lydMGQ+CtGowE3nLaZ7kxbxPDZ9fCE= +github.com/sei-protocol/sei-wasmvm v1.5.4-sei.0.0.3.0.20250819144601-14afbfe2bf78/go.mod h1:Q0bSEtlktzh7W2hhEaifrFp1Erx11ckQZmjq8FLCyys= github.com/sei-protocol/tm-db v0.0.4 h1:7Y4EU62Xzzg6wKAHEotm7SXQR0aPLcGhKHkh3qd0tnk= github.com/sei-protocol/tm-db v0.0.4/go.mod h1:PWsIWOTwdwC7Ow/GUvx8HgUJTO691pBuorIQD8JvwAs= github.com/shirou/gopsutil v2.20.5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= diff --git a/x/wasm/artifacts/v152/lib.go b/x/wasm/artifacts/v152/lib.go index 579af35..eef3d5f 100644 --- a/x/wasm/artifacts/v152/lib.go +++ b/x/wasm/artifacts/v152/lib.go @@ -355,7 +355,7 @@ func (vm *VM) IBCChannelOpen( gasMeter GasMeter, gasLimit uint64, deserCost types.UFraction, -) (*types.IBC3ChannelOpenResponse, uint64, error) { +) (*types.IBCChannelOpenResult, uint64, error) { envBin, err := json.Marshal(env) if err != nil { return nil, 0, err @@ -377,7 +377,7 @@ func (vm *VM) IBCChannelOpen( if resp.Err != "" { return nil, gasReport.UsedInternally, fmt.Errorf("%s", resp.Err) } - return resp.Ok, gasReport.UsedInternally, nil + return &resp, gasReport.UsedInternally, nil } // IBCChannelConnect is available on IBC-enabled contracts and is a hook to call into diff --git a/x/wasm/artifacts/v155/lib.go b/x/wasm/artifacts/v155/lib.go index b99a594..04d8489 100644 --- a/x/wasm/artifacts/v155/lib.go +++ b/x/wasm/artifacts/v155/lib.go @@ -355,7 +355,7 @@ func (vm *VM) IBCChannelOpen( gasMeter GasMeter, gasLimit uint64, deserCost types.UFraction, -) (*types.IBC3ChannelOpenResponse, uint64, error) { +) (*types.IBCChannelOpenResult, uint64, error) { envBin, err := json.Marshal(env) if err != nil { return nil, 0, err @@ -377,7 +377,7 @@ func (vm *VM) IBCChannelOpen( if resp.Err != "" { return nil, gasReport.UsedInternally, fmt.Errorf("%s", resp.Err) } - return resp.Ok, gasReport.UsedInternally, nil + return &resp, gasReport.UsedInternally, nil } // IBCChannelConnect is available on IBC-enabled contracts and is a hook to call into diff --git a/x/wasm/keeper/msg_dispatcher.go b/x/wasm/keeper/msg_dispatcher.go index 89c587e..96b5a52 100644 --- a/x/wasm/keeper/msg_dispatcher.go +++ b/x/wasm/keeper/msg_dispatcher.go @@ -54,14 +54,18 @@ func (d MessageDispatcher) dispatchMsgWithGasLimit(ctx sdk.Context, contractAddr // catch out of gas panic and just charge the entire gas limit defer func() { if r := recover(); r != nil { - // if it's not an OutOfGas error, raise it again - if _, ok := r.(sdk.ErrorOutOfGas); !ok { + if _, ok := r.(sdk.ErrorOutOfGas); ok { + // consume the gas limit for the submessage and turn panic into error + ctx.GasMeter().ConsumeGas(gasLimit, "Sub-Message OutOfGas panic") + err = sdkerrors.Wrap(sdkerrors.ErrOutOfGas, "SubMsg hit gas limit") + } else { + // if it's not an ErrorOutOfGas, consume the gas used in the sub-context and raise it again + spent := subCtx.GasMeter().GasConsumed() + ctx.GasMeter().ConsumeGas(spent, "From limited Sub-Message") // log it to get the original stack trace somewhere (as panic(r) keeps message but stacktrace to here moduleLogger(ctx).Info("SubMsg rethrowing panic: %#v", r) panic(r) } - ctx.GasMeter().ConsumeGas(gasLimit, "Sub-Message OutOfGas panic") - err = sdkerrors.Wrap(sdkerrors.ErrOutOfGas, "SubMsg hit gas limit") } }() events, data, err = d.messenger.DispatchMsg(subCtx, contractAddr, ibcPort, msg, info, codeInfo) diff --git a/x/wasm/keeper/relay.go b/x/wasm/keeper/relay.go index 6005019..e1961a6 100644 --- a/x/wasm/keeper/relay.go +++ b/x/wasm/keeper/relay.go @@ -24,8 +24,6 @@ func (k Keeper) OnOpenChannel( msg wasmvmtypes.IBCChannelOpenMsg, ) (string, error) { defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc-open-channel") - version := "" - _, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr) if err != nil { return "", err @@ -37,15 +35,26 @@ func (k Keeper) OnOpenChannel( gas := k.runtimeGasForContract(ctx) res, gasUsed, execErr := k.getWasmer(ctx).IBCChannelOpen(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, ctx.GasMeter(), gas, costJSONDeserialization) k.consumeRuntimeGas(ctx, gasUsed) + // check if contract panicked / VM failed if execErr != nil { return "", sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) } - if res != nil { - version = res.Version + if res == nil { + // If this gets executed, that's a bug in wasmvm + return "", sdkerrors.Wrap(types.ErrVMError, "internal wasmvm error") + } + // check contract result + if res.Err != "" { + return "", types.MarkErrorDeterministic(sdkerrors.Wrap(types.ErrExecuteFailed, res.Err)) + } + if res.Ok == nil { + // a nil "ok" value is a valid response and means the contract accepts the incoming channel version + // see https://docs.rs/cosmwasm-std/2.2.2/cosmwasm_std/type.IbcChannelOpenResponse.html + return "", nil } - return version, nil + return res.Ok.Version, nil } // OnConnectChannel calls the contract to let it know the IBC channel was established. diff --git a/x/wasm/keeper/relay_test.go b/x/wasm/keeper/relay_test.go index dd9c3da..51190bc 100644 --- a/x/wasm/keeper/relay_test.go +++ b/x/wasm/keeper/relay_test.go @@ -56,9 +56,9 @@ func TestOnOpenChannel(t *testing.T) { t.Run(name, func(t *testing.T) { myChannel := wasmvmtypes.IBCChannel{Version: "my test channel"} myMsg := wasmvmtypes.IBCChannelOpenMsg{OpenTry: &wasmvmtypes.IBCOpenTry{Channel: myChannel, CounterpartyVersion: "foo"}} - m.IBCChannelOpenFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) { + m.IBCChannelOpenFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCChannelOpenResult, uint64, error) { assert.Equal(t, myMsg, msg) - return &wasmvmtypes.IBC3ChannelOpenResponse{}, spec.contractGas * DefaultGasMultiplier, spec.contractErr + return &wasmvmtypes.IBCChannelOpenResult{}, spec.contractGas * DefaultGasMultiplier, spec.contractErr } ctx, _ := parentCtx.CacheContext() diff --git a/x/wasm/keeper/wasmtesting/mock_engine.go b/x/wasm/keeper/wasmtesting/mock_engine.go index ebf7dbc..8528fbc 100644 --- a/x/wasm/keeper/wasmtesting/mock_engine.go +++ b/x/wasm/keeper/wasmtesting/mock_engine.go @@ -27,7 +27,7 @@ type MockWasmer struct { ReplyFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, reply wasmvmtypes.Reply, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) GetCodeFn func(codeID wasmvm.Checksum) (wasmvm.WasmCode, error) CleanupFn func() - IBCChannelOpenFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) + IBCChannelOpenFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCChannelOpenResult, uint64, error) IBCChannelConnectFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelConnectMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) IBCChannelCloseFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelCloseMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) IBCPacketReceiveFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketReceiveMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCReceiveResult, uint64, error) @@ -38,7 +38,7 @@ type MockWasmer struct { GetMetricsFn func() (*wasmvmtypes.Metrics, error) } -func (m *MockWasmer) IBCChannelOpen(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) { +func (m *MockWasmer) IBCChannelOpen(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCChannelOpenResult, uint64, error) { if m.IBCChannelOpenFn == nil { panic("not supposed to be called!") } @@ -210,7 +210,7 @@ type IBCContractCallbacks interface { gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction, - ) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) + ) (*wasmvmtypes.IBCChannelOpenResult, uint64, error) IBCChannelConnect( codeID wasmvm.Checksum, diff --git a/x/wasm/relay_pingpong_test.go b/x/wasm/relay_pingpong_test.go index 839ecda..f7d86f5 100644 --- a/x/wasm/relay_pingpong_test.go +++ b/x/wasm/relay_pingpong_test.go @@ -179,11 +179,11 @@ func (p *player) Execute(code wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmt } // OnIBCChannelOpen ensures to accept only configured version -func (p player) IBCChannelOpen(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) { +func (p player) IBCChannelOpen(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCChannelOpenResult, uint64, error) { if msg.GetChannel().Version != p.actor { - return &wasmvmtypes.IBC3ChannelOpenResponse{}, 0, nil + return &wasmvmtypes.IBCChannelOpenResult{}, 0, nil } - return &wasmvmtypes.IBC3ChannelOpenResponse{}, 0, nil + return &wasmvmtypes.IBCChannelOpenResult{}, 0, nil } // OnIBCChannelConnect persists connection endpoints diff --git a/x/wasm/relay_test.go b/x/wasm/relay_test.go index 0d3a9fe..220ed4e 100644 --- a/x/wasm/relay_test.go +++ b/x/wasm/relay_test.go @@ -604,8 +604,8 @@ func (c *errorReceiverContract) IBCPacketReceive(codeID wasmvm.Checksum, env was // simple helper struct that implements connection setup methods. type contractStub struct{} -func (s *contractStub) IBCChannelOpen(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) { - return &wasmvmtypes.IBC3ChannelOpenResponse{}, 0, nil +func (s *contractStub) IBCChannelOpen(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCChannelOpenResult, uint64, error) { + return &wasmvmtypes.IBCChannelOpenResult{}, 0, nil } func (s *contractStub) IBCChannelConnect(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelConnectMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { diff --git a/x/wasm/types/errors.go b/x/wasm/types/errors.go index f21c8e8..9aabd59 100644 --- a/x/wasm/types/errors.go +++ b/x/wasm/types/errors.go @@ -88,7 +88,10 @@ var ( // ErrExceedMaxQueryStackSize error if max query stack size is exceeded ErrExceedMaxQueryStackSize = sdkErrors.Register(DefaultCodespace, 27, "max query stack size exceeded") - // unused 28..29 + // unused 28 + + // ErrVMError means an error occurred in wasmvm (not in the contract itself, but in the host environment) + ErrVMError = sdkErrors.Register(DefaultCodespace, 29, "wasmvm error") // ErrExceedMaxCallDepth error if max query stack size is exceeded ErrExceedMaxCallDepth = sdkErrors.Register(DefaultCodespace, 30, "max call depth exceeded") @@ -109,3 +112,34 @@ func (m *ErrNoSuchContract) ABCICode() uint32 { func (m *ErrNoSuchContract) Codespace() string { return DefaultCodespace } + +// DeterministicError is a wrapper type around an error that the creator guarantees to have +// a deterministic error message. +// This means that the `Error()` function must always return the same string on all nodes. +// The DeterministicError has the same error message as the wrapped error. +// DeterministicErrors are not redacted when returned to a contract, +// so not upholding this guarantee can lead to consensus failures. +type DeterministicError struct { + error +} + +var _ error = DeterministicError{} + +// MarkErrorDeterministic marks an error as deterministic. +// Make sure to only do that if the error message is deterministic between systems. +// See [DeterministicError] for more details. +func MarkErrorDeterministic(e error) DeterministicError { + return DeterministicError{error: e} +} + +// Unwrap implements the built-in errors.Unwrap +func (e DeterministicError) Unwrap() error { + return e.error +} + +// Cause is the same as unwrap but used by ABCIInfo +// By returning the wrapped error here, we ensure that the DeterministicError inherits +// the ABCIInfo of the wrapped error. +func (e DeterministicError) Cause() error { + return e.Unwrap() +} diff --git a/x/wasm/types/wasmer_engine.go b/x/wasm/types/wasmer_engine.go index d2c59dc..730f56b 100644 --- a/x/wasm/types/wasmer_engine.go +++ b/x/wasm/types/wasmer_engine.go @@ -154,7 +154,7 @@ type WasmerEngine interface { gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction, - ) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) + ) (*wasmvmtypes.IBCChannelOpenResult, uint64, error) // IBCChannelConnect is available on IBC-enabled contracts and is a hook to call into // during the handshake pahse