Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NTRN-142] feat: enhance contract failure with error #352

Merged
merged 11 commits into from
Nov 14, 2023
2 changes: 2 additions & 0 deletions proto/neutron/contractmanager/failure.proto
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ message Failure {
uint64 id = 2;
// Serialized MessageSudoCallback with Packet and Ack(if exists)
bytes sudo_payload = 3;
// Redacted error response of the sudo call. Full error is emitted as an event
string error = 4;
}
17 changes: 14 additions & 3 deletions proto/neutron/contractmanager/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,19 @@ service Query {
rpc Params(QueryParamsRequest) returns (QueryParamsResponse) {
option (google.api.http).get = "/neutron/contractmanager/params";
}
// Queries a Failure by address.

// Queries a Failure by contract address and failure ID.
rpc AddressFailure(QueryFailuresRequest) returns (QueryFailuresResponse) {
option (google.api.http).get = "/neutron/contractmanager/failures/{address}/{failure_id}";
}

// Queries Failures by contract address.
rpc AddressFailures(QueryFailuresRequest) returns (QueryFailuresResponse) {
option (google.api.http).get =
"/neutron/contractmanager/failures/{address}";
}

// Queries a list of Failure items.
// Queries a list of Failures occurred on the network.
rpc Failures(QueryFailuresRequest) returns (QueryFailuresResponse) {
option (google.api.http).get = "/neutron/contractmanager/failures";
}
Expand All @@ -39,11 +45,16 @@ message QueryParamsResponse {
Params params = 1 [ (gogoproto.nullable) = false ];
}

// QueryFailuresRequest is request type for the Query/Failures RPC method.
message QueryFailuresRequest {
// address of the contract which Sudo call failed.
string address = 1;
cosmos.base.query.v1beta1.PageRequest pagination = 2;
// ID of the failure for the given contract.
uint64 failure_id = 2;
cosmos.base.query.v1beta1.PageRequest pagination = 3;
}

// QueryFailuresResponse is response type for the Query/Failures RPC method.
message QueryFailuresResponse {
repeated Failure failures = 1 [ (gogoproto.nullable) = false ];
cosmos.base.query.v1beta1.PageResponse pagination = 2;
Expand Down
1 change: 1 addition & 0 deletions x/contractmanager/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func GetQueryCmd(_ string) *cobra.Command {

cmd.AddCommand(CmdQueryParams())
cmd.AddCommand(CmdFailures())
cmd.AddCommand(CmdFailureDetails())
// this line is used by starport scaffolding # 1

return cmd
Expand Down
72 changes: 66 additions & 6 deletions x/contractmanager/client/cli/query_failure.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package cli

import (
"context"
"fmt"
"strconv"

wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/version"
"github.com/cosmos/cosmos-sdk/x/auth/tx"
contractmanagertypes "github.com/neutron-org/neutron/x/contractmanager/types"
"github.com/spf13/cobra"

"github.com/neutron-org/neutron/x/contractmanager/types"
)

func CmdFailures() *cobra.Command {
Expand All @@ -23,19 +26,19 @@ func CmdFailures() *cobra.Command {
return err
}

queryClient := types.NewQueryClient(clientCtx)
queryClient := contractmanagertypes.NewQueryClient(clientCtx)

address := ""
if len(args) > 0 {
address = args[0]
}

params := &types.QueryFailuresRequest{
params := &contractmanagertypes.QueryFailuresRequest{
Address: address,
Pagination: pageReq,
}

res, err := queryClient.Failures(context.Background(), params)
res, err := queryClient.Failures(cmd.Context(), params)
pr0n00gler marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
Expand All @@ -49,3 +52,60 @@ func CmdFailures() *cobra.Command {

return cmd
}

// CmdFailureDetails returns the command handler for the failure's detailed error querying.
func CmdFailureDetails() *cobra.Command {
cmd := &cobra.Command{
Use: "failure-details [address] [failure-id]",
Short: "Query the detailed error related to a contract's sudo call failure",
Long: "Query the detailed error related to a contract's sudo call failure based on contract's address and failure ID",
Args: cobra.ExactArgs(2),
Example: fmt.Sprintf("%s query failure-details neutron1m0z0kk0qqug74n9u9ul23e28x5fszr628h20xwt6jywjpp64xn4qatgvm0 0", version.AppName),
RunE: func(cmd *cobra.Command, args []string) error {
address := args[0]
failureID, err := strconv.ParseUint(args[1], 10, 64)
if err != nil {
return fmt.Errorf("invalid failure ID %s: expected a uint64: %v", args[1], err)
}

clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}

queryClient := contractmanagertypes.NewQueryClient(clientCtx)
if _, err = queryClient.AddressFailure(
cmd.Context(),
pr0n00gler marked this conversation as resolved.
Show resolved Hide resolved
&contractmanagertypes.QueryFailuresRequest{Address: address, FailureId: failureID},
); err != nil {
return err
}

searchEvents := []string{
fmt.Sprintf("%s.%s='%s'", wasmtypes.EventTypeSudo, wasmtypes.AttributeKeyContractAddr, address),
fmt.Sprintf("%s.%s='%d'", wasmtypes.EventTypeSudo, contractmanagertypes.AttributeKeySudoFailureID, failureID),
}
result, err := tx.QueryTxsByEvents(clientCtx, searchEvents, 1, 1, "") // only a single tx for a pair address+failure_id is expected
if err != nil {
return err
}

for _, tx := range result.Txs {
for _, event := range tx.Events {
if event.Type == wasmtypes.EventTypeSudo {
for _, attr := range event.Attributes {
if attr.Key == contractmanagertypes.AttributeKeySudoError {
return clientCtx.PrintString(attr.Value)
}
}
}
}
}
return fmt.Errorf("detailed failure error message not found in node events")
},
}

flags.AddQueryFlagsToCmd(cmd)

return cmd
}
2 changes: 1 addition & 1 deletion x/contractmanager/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) {
// Set all the failure
for _, elem := range genState.FailuresList {
k.AddContractFailure(ctx, elem.Address, elem.SudoPayload)
k.AddContractFailure(ctx, elem.Address, elem.SudoPayload, elem.Error)
}
// this line is used by starport scaffolding # genesis/module/init
err := k.SetParams(ctx, genState.Params)
Expand Down
39 changes: 34 additions & 5 deletions x/contractmanager/ibc_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package contractmanager
import (
"fmt"

"cosmossdk.io/errors"
errorsmod "cosmossdk.io/errors"

wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types"
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
"github.com/cometbft/cometbft/libs/log"
sdk "github.com/cosmos/cosmos-sdk/types"
contractmanagertypes "github.com/neutron-org/neutron/x/contractmanager/types"
Expand Down Expand Up @@ -34,9 +36,16 @@ func (k SudoLimitWrapper) Sudo(ctx sdk.Context, contractAddress sdk.AccAddress,
// maybe later we'll retrieve actual errors from events
resp, err = k.WasmKeeper.Sudo(cacheCtx, contractAddress, msg)
}()
if err != nil {
// the contract either returned an error or panicked with `out of gas`
k.contractManager.AddContractFailure(ctx, contractAddress.String(), msg)
if err != nil { // the contract either returned an error or panicked with `out of gas`
failure := k.contractManager.AddContractFailure(ctx, contractAddress.String(), msg, redactError(err).Error())
pr0n00gler marked this conversation as resolved.
Show resolved Hide resolved
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
wasmtypes.EventTypeSudo,
sdk.NewAttribute(wasmtypes.AttributeKeyContractAddr, contractAddress.String()),
sdk.NewAttribute(contractmanagertypes.AttributeKeySudoFailureID, fmt.Sprintf("%d", failure.Id)),
sdk.NewAttribute(contractmanagertypes.AttributeKeySudoError, err.Error()),
),
})
} else {
writeFn()
}
Expand All @@ -60,7 +69,7 @@ func outOfGasRecovery(
if !ok || !gasMeter.IsOutOfGas() {
panic(r)
}
*err = errors.Wrapf(errors.ErrPanic, "%v", r)
*err = errorsmod.Wrapf(errorsmod.ErrPanic, "%v", r)
}
}

Expand All @@ -71,3 +80,23 @@ func createCachedContext(ctx sdk.Context, gasLimit uint64) (sdk.Context, func())
cacheCtx = cacheCtx.WithGasMeter(gasMeter)
return cacheCtx, writeFn
}

// redactError removes non-determenistic details from the error returning just codespace and core
// of the error. Returns full error for system errors.
//
// Copy+paste from https://github.com/neutron-org/wasmd/blob/5b59886e41ed55a7a4a9ae196e34b0852285503d/x/wasm/keeper/msg_dispatcher.go#L175-L190
func redactError(err error) error {
// Do not redact system errors
// SystemErrors must be created in x/wasm and we can ensure determinism
if wasmvmtypes.ToSystemError(err) != nil {
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)
return fmt.Errorf("codespace: %s, code: %d", codespace, code)
}
12 changes: 7 additions & 5 deletions x/contractmanager/keeper/failure.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
package keeper

import (
"cosmossdk.io/errors"
errorsmod "cosmossdk.io/errors"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/neutron-org/neutron/x/contractmanager/types"
)

// AddContractFailure adds a specific failure to the store using address as the key
func (k Keeper) AddContractFailure(ctx sdk.Context, address string, sudoPayload []byte) {
func (k Keeper) AddContractFailure(ctx sdk.Context, address string, sudoPayload []byte, errMsg string) types.Failure {
failure := types.Failure{
Address: address,
SudoPayload: sudoPayload,
Error: errMsg,
}
nextFailureID := k.GetNextFailureIDKey(ctx, failure.GetAddress())
failure.Id = nextFailureID

store := ctx.KVStore(k.storeKey)
bz := k.cdc.MustMarshal(&failure)
store.Set(types.GetFailureKey(failure.GetAddress(), nextFailureID), bz)
return failure
}

func (k Keeper) GetNextFailureIDKey(ctx sdk.Context, address string) uint64 {
Expand Down Expand Up @@ -58,7 +60,7 @@ func (k Keeper) GetFailure(ctx sdk.Context, contractAddr sdk.AccAddress, id uint

bz := store.Get(key)
if bz == nil {
return nil, errors.Wrapf(sdkerrors.ErrKeyNotFound, "no failure found for contractAddress = %s and failureId = %d", contractAddr.String(), id)
return nil, errorsmod.Wrapf(sdkerrors.ErrKeyNotFound, "no failure found for contractAddress = %s and failureId = %d", contractAddr.String(), id)
}
var res types.Failure
k.cdc.MustUnmarshal(bz, &res)
Expand All @@ -69,11 +71,11 @@ func (k Keeper) GetFailure(ctx sdk.Context, contractAddr sdk.AccAddress, id uint
// ResubmitFailure tries to call sudo handler for contract with same parameters as initially.
func (k Keeper) ResubmitFailure(ctx sdk.Context, contractAddr sdk.AccAddress, failure *types.Failure) error {
if failure.SudoPayload == nil {
return errors.Wrapf(types.IncorrectFailureToResubmit, "cannot resubmit failure without sudo payload; failureId = %d", failure.Id)
return errorsmod.Wrapf(types.IncorrectFailureToResubmit, "cannot resubmit failure without sudo payload; failureId = %d", failure.Id)
}

if _, err := k.wasmKeeper.Sudo(ctx, contractAddr, failure.SudoPayload); err != nil {
return errors.Wrapf(types.FailedToResubmitFailure, "cannot resubmit failure; failureId = %d; err = %s", failure.Id, err)
return errorsmod.Wrapf(types.FailedToResubmitFailure, "cannot resubmit failure; failureId = %d; err = %s", failure.Id, err)
}

// Cleanup failure since we resubmitted it successfully
Expand Down
21 changes: 19 additions & 2 deletions x/contractmanager/keeper/grpc_query_failure.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import (
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/query"
"github.com/neutron-org/neutron/x/contractmanager/types"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/neutron-org/neutron/x/contractmanager/types"
)

const FailuresQueryMaxLimit uint64 = query.DefaultLimit
Expand Down Expand Up @@ -58,3 +57,21 @@ func (k Keeper) AddressFailures(c context.Context, req *types.QueryFailuresReque

return &types.QueryFailuresResponse{Failures: failures, Pagination: pageRes}, nil
}

func (k Keeper) AddressFailure(c context.Context, req *types.QueryFailuresRequest) (*types.QueryFailuresResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "request field must not be empty")
}

addr, err := sdk.AccAddressFromBech32(req.Address)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid address: %v", err)
}

resp, err := k.GetFailure(sdk.UnwrapSDKContext(c), addr, req.GetFailureId())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}

return &types.QueryFailuresResponse{Failures: []types.Failure{*resp}}, nil
}
10 changes: 10 additions & 0 deletions x/contractmanager/types/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package types

// Contractmanager events
const (
// AttributeKeySudoError indicates an attribute containing detailed Sudo call error.
AttributeKeySudoError = "error"
// AttributeKeySudoFailureID indicates attribute containing ID of the failure related to an
// error Sudo call.
AttributeKeySudoFailureID = "failure_id"
)
2 changes: 1 addition & 1 deletion x/contractmanager/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ type WasmKeeper interface {
}

type ContractManagerKeeper interface {
AddContractFailure(ctx sdk.Context, address string, sudoPayload []byte)
AddContractFailure(ctx sdk.Context, address string, sudoPayload []byte, errMsg string) Failure
GetParams(ctx sdk.Context) (params Params)
}
Loading
Loading