Skip to content

Commit

Permalink
Add DeterministicError type for redactError
Browse files Browse the repository at this point in the history
  • Loading branch information
chipshort committed Feb 28, 2024
1 parent 1ee2c49 commit 4e7e534
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 32 deletions.
10 changes: 5 additions & 5 deletions x/wasm/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ func (k Keeper) instantiate(
return nil, nil, errorsmod.Wrap(types.ErrVMError, err.Error())

Check warning on line 324 in x/wasm/keeper/keeper.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/keeper.go#L324

Added line #L324 was not covered by tests
}
if res.Err != "" {
return nil, nil, errorsmod.Wrap(types.ErrInstantiateFailed, res.Err)
return nil, nil, types.MarkErrorDeterministic(errorsmod.Wrap(types.ErrInstantiateFailed, res.Err))
}

// persist instance first
Expand Down Expand Up @@ -408,7 +408,7 @@ func (k Keeper) execute(ctx context.Context, contractAddress, caller sdk.AccAddr
return nil, errorsmod.Wrap(types.ErrVMError, execErr.Error())
}
if res.Err != "" {
return nil, errorsmod.Wrap(types.ErrExecuteFailed, res.Err)
return nil, types.MarkErrorDeterministic(errorsmod.Wrap(types.ErrExecuteFailed, res.Err))
}

sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
Expand Down Expand Up @@ -485,7 +485,7 @@ func (k Keeper) migrate(
return nil, errorsmod.Wrap(types.ErrVMError, err.Error())
}
if res.Err != "" {
return nil, errorsmod.Wrap(types.ErrMigrationFailed, res.Err)
return nil, types.MarkErrorDeterministic(errorsmod.Wrap(types.ErrMigrationFailed, res.Err))
}
// delete old secondary index entry
err = k.removeFromContractCodeSecondaryIndex(ctx, contractAddress, k.mustGetLastContractHistoryEntry(sdkCtx, contractAddress))
Expand Down Expand Up @@ -550,7 +550,7 @@ func (k Keeper) Sudo(ctx context.Context, contractAddress sdk.AccAddress, msg []
return nil, errorsmod.Wrap(types.ErrVMError, execErr.Error())

Check warning on line 550 in x/wasm/keeper/keeper.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/keeper.go#L550

Added line #L550 was not covered by tests
}
if res.Err != "" {
return nil, errorsmod.Wrap(types.ErrExecuteFailed, res.Err)
return nil, types.MarkErrorDeterministic(errorsmod.Wrap(types.ErrExecuteFailed, res.Err))

Check warning on line 553 in x/wasm/keeper/keeper.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/keeper.go#L553

Added line #L553 was not covered by tests
}

sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
Expand Down Expand Up @@ -590,7 +590,7 @@ func (k Keeper) reply(ctx sdk.Context, contractAddress sdk.AccAddress, reply was
return nil, errorsmod.Wrap(types.ErrVMError, execErr.Error())
}
if res.Err != "" {
return nil, errorsmod.Wrap(types.ErrExecuteFailed, res.Err)
return nil, types.MarkErrorDeterministic(errorsmod.Wrap(types.ErrExecuteFailed, res.Err))

Check warning on line 593 in x/wasm/keeper/keeper.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/keeper.go#L593

Added line #L593 was not covered by tests
}

ctx.EventManager().EmitEvent(sdk.NewEvent(
Expand Down
13 changes: 7 additions & 6 deletions x/wasm/keeper/msg_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,18 +209,19 @@ func redactError(err error) error {
return err
}

// If it is a DeterministicError, we can safely return it without redaction.
// We only check the top level error to avoid changes in the error chain becoming
// consensus-breaking.
if _, ok := err.(types.DeterministicError); ok {
return err
}

// FIXME: do we want to hardcode some constant string mappings here as well?
// Or better document them? (SDK error string may change on a patch release to fix wording)
// sdk/11 is out of gas
// sdk/5 is insufficient funds (on bank send)
// (we can theoretically redact less in the future, but this is a first step to safety)
codespace, code, _ := errorsmod.ABCIInfo(err, false)

// Also do not redact any errors that are coming from the contract,
// as they are always deterministic
if codespace == types.DefaultCodespace && contains(types.ContractErrorCodes, code) {
return err
}
return fmt.Errorf("codespace: %s, code: %d", codespace, code)
}

Expand Down
2 changes: 1 addition & 1 deletion x/wasm/keeper/recurse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ func TestLimitRecursiveQueryGas(t *testing.T) {
expectQueriesFromContract: 10,
expectOutOfGas: false,
expectError: "query wasm contract failed", // Error we get from the contract instance doing the failing query, not wasmd
expectedGas: 10*(GasWork2k+GasReturnHashed) + 3124,
expectedGas: 10*(GasWork2k+GasReturnHashed) - 249,
},
}

Expand Down
8 changes: 4 additions & 4 deletions x/wasm/keeper/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (k Keeper) OnConnectChannel(
return errorsmod.Wrap(types.ErrExecuteFailed, execErr.Error())
}
if res.Err != "" {
return errorsmod.Wrap(types.ErrExecuteFailed, res.Err)
return types.MarkErrorDeterministic(errorsmod.Wrap(types.ErrExecuteFailed, res.Err))

Check warning on line 79 in x/wasm/keeper/relay.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/relay.go#L79

Added line #L79 was not covered by tests
}

return k.handleIBCBasicContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res.Ok)
Expand Down Expand Up @@ -110,7 +110,7 @@ func (k Keeper) OnCloseChannel(
return errorsmod.Wrap(types.ErrExecuteFailed, execErr.Error())
}
if res.Err != "" {
return errorsmod.Wrap(types.ErrExecuteFailed, res.Err)
return types.MarkErrorDeterministic(errorsmod.Wrap(types.ErrExecuteFailed, res.Err))

Check warning on line 113 in x/wasm/keeper/relay.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/relay.go#L113

Added line #L113 was not covered by tests
}

return k.handleIBCBasicContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res.Ok)
Expand Down Expand Up @@ -201,7 +201,7 @@ func (k Keeper) OnAckPacket(
return errorsmod.Wrap(types.ErrExecuteFailed, execErr.Error())
}
if res.Err != "" {
return errorsmod.Wrap(types.ErrExecuteFailed, res.Err)
return types.MarkErrorDeterministic(errorsmod.Wrap(types.ErrExecuteFailed, res.Err))

Check warning on line 204 in x/wasm/keeper/relay.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/relay.go#L204

Added line #L204 was not covered by tests
}

return k.handleIBCBasicContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res.Ok)
Expand Down Expand Up @@ -232,7 +232,7 @@ func (k Keeper) OnTimeoutPacket(
return errorsmod.Wrap(types.ErrExecuteFailed, execErr.Error())
}
if res.Err != "" {
return errorsmod.Wrap(types.ErrExecuteFailed, res.Err)
return types.MarkErrorDeterministic(errorsmod.Wrap(types.ErrExecuteFailed, res.Err))

Check warning on line 235 in x/wasm/keeper/relay.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/relay.go#L235

Added line #L235 was not covered by tests
}

return k.handleIBCBasicContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res.Ok)
Expand Down
49 changes: 33 additions & 16 deletions x/wasm/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ import (
var (
DefaultCodespace = ModuleName

// ContractErrorCodes are the error codes for errors returned by the contract
// Since contract execution is deterministic, the errors are also deterministic
ContractErrorCodes = []uint32{InstantiateErrorCode, ExecuteErrorCode, QueryErrorCode, MigrateErrorCode}

// Note: never use code 1 for any errors - that is reserved for ErrInternal in the core cosmos sdk

// ErrCreateFailed error for wasm code that has already been uploaded or failed
Expand All @@ -23,10 +19,10 @@ var (
ErrAccountExists = errorsmod.Register(DefaultCodespace, 3, "contract account already exists")

// ErrInstantiateFailed error for rust instantiate contract failure
ErrInstantiateFailed = errorsmod.Register(DefaultCodespace, InstantiateErrorCode, "instantiate wasm contract failed")
ErrInstantiateFailed = errorsmod.Register(DefaultCodespace, 4, "instantiate wasm contract failed")

// ErrExecuteFailed error for rust execution contract failure
ErrExecuteFailed = errorsmod.Register(DefaultCodespace, ExecuteErrorCode, "execute wasm contract failed")
ErrExecuteFailed = errorsmod.Register(DefaultCodespace, 5, "execute wasm contract failed")

// ErrGasLimit error for out of gas
ErrGasLimit = errorsmod.Register(DefaultCodespace, 6, "insufficient gas")
Expand All @@ -38,13 +34,13 @@ var (
ErrNotFound = errorsmod.Register(DefaultCodespace, 8, "not found")

// ErrQueryFailed error for rust smart query contract failure
ErrQueryFailed = errorsmod.Register(DefaultCodespace, QueryErrorCode, "query wasm contract failed")
ErrQueryFailed = errorsmod.Register(DefaultCodespace, 9, "query wasm contract failed")

// ErrInvalidMsg error when we cannot process the error returned from the contract
ErrInvalidMsg = errorsmod.Register(DefaultCodespace, 10, "invalid CosmosMsg from the contract")

// ErrMigrationFailed error for rust execution contract failure
ErrMigrationFailed = errorsmod.Register(DefaultCodespace, MigrateErrorCode, "migrate wasm contract failed")
ErrMigrationFailed = errorsmod.Register(DefaultCodespace, 11, "migrate wasm contract failed")

// ErrEmpty error for empty content
ErrEmpty = errorsmod.Register(DefaultCodespace, 12, "empty")
Expand Down Expand Up @@ -95,14 +91,6 @@ var (
ErrVMError = errorsmod.Register(DefaultCodespace, 29, "wasmvm error")
)

// Error codes for wasm contract errors
const (
InstantiateErrorCode = 4
ExecuteErrorCode = 5
QueryErrorCode = 9
MigrateErrorCode = 11
)

// WasmVMErrorable mapped error type in wasmvm and are not redacted
type WasmVMErrorable interface {
// ToWasmVMError convert instance to wasmvm friendly error if possible otherwise root cause. never nil
Expand Down Expand Up @@ -164,3 +152,32 @@ func (e WasmVMFlavouredError) Wrap(desc string) error { return errorsmod.Wrap(e,
func (e WasmVMFlavouredError) Wrapf(desc string, args ...interface{}) error {
return errorsmod.Wrapf(e, desc, args...)
}

// 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.
// 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
func (e DeterministicError) Cause() error {
return e.Unwrap()
}
16 changes: 16 additions & 0 deletions x/wasm/types/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,19 @@ func TestWasmVMFlavouredError(t *testing.T) {
t.Run(name, spec.exec)
}
}

func TestDeterministicError(t *testing.T) {
inner := ErrInstantiateFailed
err := MarkErrorDeterministic(inner)

// behaves like a wrapper around inner error
assert.Equal(t, inner.Error(), err.Error())
assert.Equal(t, inner, err.Cause())
assert.Equal(t, inner, err.Unwrap())

// also works with ABCIInfo
codespace, code, _ := errorsmod.ABCIInfo(err, false)
innerCodeSpace, innerCode, _ := errorsmod.ABCIInfo(inner, false)
assert.Equal(t, innerCodeSpace, codespace)
assert.Equal(t, innerCode, code)
}

0 comments on commit 4e7e534

Please sign in to comment.