github.com/fibonacci-chain/fbc@v0.0.0-20231124064014-c7636198c1e9/x/wasm/keeper/msg_dispatcher.go (about)

     1  package keeper
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"sort"
     7  
     8  	"github.com/fibonacci-chain/fbc/libs/tendermint/libs/kv"
     9  
    10  	wasmvmtypes "github.com/CosmWasm/wasmvm/types"
    11  	sdk "github.com/fibonacci-chain/fbc/libs/cosmos-sdk/types"
    12  	sdkerrors "github.com/fibonacci-chain/fbc/libs/cosmos-sdk/types/errors"
    13  	"github.com/fibonacci-chain/fbc/x/wasm/types"
    14  )
    15  
    16  // Messenger is an extension point for custom wasmd message handling
    17  type Messenger interface {
    18  	// DispatchMsg encodes the wasmVM message and dispatches it.
    19  	DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error)
    20  }
    21  
    22  // replyer is a subset of keeper that can handle replies to submessages
    23  type replyer interface {
    24  	reply(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error)
    25  }
    26  
    27  // MessageDispatcher coordinates message sending and submessage reply/ state commits
    28  type MessageDispatcher struct {
    29  	messenger Messenger
    30  	keeper    replyer
    31  }
    32  
    33  // NewMessageDispatcher constructor
    34  func NewMessageDispatcher(messenger Messenger, keeper replyer) *MessageDispatcher {
    35  	return &MessageDispatcher{messenger: messenger, keeper: keeper}
    36  }
    37  
    38  // DispatchMessages sends all messages.
    39  func (d MessageDispatcher) DispatchMessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.CosmosMsg) error {
    40  	for _, msg := range msgs {
    41  		events, _, err := d.messenger.DispatchMsg(ctx, contractAddr, ibcPort, msg)
    42  		if err != nil {
    43  			return err
    44  		}
    45  		// redispatch all events, (type sdk.EventTypeMessage will be filtered out in the handler)
    46  		ctx.EventManager().EmitEvents(events)
    47  	}
    48  	return nil
    49  }
    50  
    51  // dispatchMsgWithGasLimit sends a message with gas limit applied
    52  func (d MessageDispatcher) dispatchMsgWithGasLimit(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msg wasmvmtypes.CosmosMsg, gasLimit uint64) (events []sdk.Event, data [][]byte, err error) {
    53  	limitedMeter := sdk.NewGasMeter(gasLimit)
    54  	subCtx := ctx
    55  	subCtx.SetGasMeter(limitedMeter)
    56  
    57  	// catch out of gas panic and just charge the entire gas limit
    58  	defer func() {
    59  		if r := recover(); r != nil {
    60  			// if it's not an OutOfGas error, raise it again
    61  			if _, ok := r.(sdk.ErrorOutOfGas); !ok {
    62  				// log it to get the original stack trace somewhere (as panic(r) keeps message but stacktrace to here
    63  				moduleLogger(ctx).Info("SubMsg rethrowing panic: %#v", r)
    64  				panic(r)
    65  			}
    66  			ctx.GasMeter().ConsumeGas(gasLimit, "Sub-Message OutOfGas panic")
    67  			err = sdkerrors.Wrap(sdkerrors.ErrOutOfGas, "SubMsg hit gas limit")
    68  		}
    69  	}()
    70  	events, data, err = d.messenger.DispatchMsg(subCtx, contractAddr, ibcPort, msg)
    71  
    72  	// make sure we charge the parent what was spent
    73  	spent := subCtx.GasMeter().GasConsumed()
    74  	ctx.GasMeter().ConsumeGas(spent, "From limited Sub-Message")
    75  
    76  	return events, data, err
    77  }
    78  
    79  // DispatchSubmessages builds a sandbox to execute these messages and returns the execution result to the contract
    80  // that dispatched them, both on success as well as failure
    81  func (d MessageDispatcher) DispatchSubmessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) {
    82  	var rsp []byte
    83  	for _, msg := range msgs {
    84  		switch msg.ReplyOn {
    85  		case wasmvmtypes.ReplySuccess, wasmvmtypes.ReplyError, wasmvmtypes.ReplyAlways, wasmvmtypes.ReplyNever:
    86  		default:
    87  			return nil, sdkerrors.Wrap(types.ErrInvalid, "replyOn value")
    88  		}
    89  		// first, we build a sub-context which we can use inside the submessages
    90  		subCtx, commit := ctx.CacheContext()
    91  		em := sdk.NewEventManager()
    92  		subCtx.SetEventManager(em)
    93  
    94  		// check how much gas left locally, optionally wrap the gas meter
    95  		gasRemaining := ctx.GasMeter().Limit() - ctx.GasMeter().GasConsumed()
    96  		limitGas := msg.GasLimit != nil && (*msg.GasLimit < gasRemaining)
    97  
    98  		var err error
    99  		var events []sdk.Event
   100  		var data [][]byte
   101  		if limitGas {
   102  			events, data, err = d.dispatchMsgWithGasLimit(subCtx, contractAddr, ibcPort, msg.Msg, *msg.GasLimit)
   103  		} else {
   104  			events, data, err = d.messenger.DispatchMsg(subCtx, contractAddr, ibcPort, msg.Msg)
   105  		}
   106  
   107  		// if it succeeds, commit state changes from submessage, and pass on events to Event Manager
   108  		var filteredEvents []sdk.Event
   109  		if err == nil {
   110  			commit()
   111  			filteredEvents = filterEvents(append(em.Events(), events...))
   112  			ctx.EventManager().EmitEvents(filteredEvents)
   113  			if msg.Msg.Wasm == nil {
   114  				filteredEvents = []sdk.Event{}
   115  			} else {
   116  				for _, e := range filteredEvents {
   117  					attributes := e.Attributes
   118  					sort.SliceStable(attributes, func(i, j int) bool {
   119  						return bytes.Compare(attributes[i].Key, attributes[j].Key) < 0
   120  					})
   121  				}
   122  			}
   123  		} // on failure, revert state from sandbox, and ignore events (just skip doing the above)
   124  
   125  		// we only callback if requested. Short-circuit here the cases we don't want to
   126  		if (msg.ReplyOn == wasmvmtypes.ReplySuccess || msg.ReplyOn == wasmvmtypes.ReplyNever) && err != nil {
   127  			return nil, err
   128  		}
   129  		if msg.ReplyOn == wasmvmtypes.ReplyNever || (msg.ReplyOn == wasmvmtypes.ReplyError && err == nil) {
   130  			continue
   131  		}
   132  
   133  		// otherwise, we create a SubMsgResult and pass it into the calling contract
   134  		var result wasmvmtypes.SubMsgResult
   135  		if err == nil {
   136  			// just take the first one for now if there are multiple sub-sdk messages
   137  			// and safely return nothing if no data
   138  			var responseData []byte
   139  			if len(data) > 0 {
   140  				responseData = data[0]
   141  			}
   142  			result = wasmvmtypes.SubMsgResult{
   143  				Ok: &wasmvmtypes.SubMsgResponse{
   144  					Events: sdkEventsToWasmVMEvents(filteredEvents),
   145  					Data:   responseData,
   146  				},
   147  			}
   148  		} else {
   149  			// Issue #759 - we don't return error string for worries of non-determinism
   150  			moduleLogger(ctx).Info("Redacting submessage error", "cause", err)
   151  			result = wasmvmtypes.SubMsgResult{
   152  				Err: redactError(err).Error(),
   153  			}
   154  		}
   155  
   156  		// now handle the reply, we use the parent context, and abort on error
   157  		reply := wasmvmtypes.Reply{
   158  			ID:     msg.ID,
   159  			Result: result,
   160  		}
   161  
   162  		// we can ignore any result returned as there is nothing to do with the data
   163  		// and the events are already in the ctx.EventManager()
   164  		rspData, err := d.keeper.reply(ctx, contractAddr, reply)
   165  		switch {
   166  		case err != nil:
   167  			return nil, sdkerrors.Wrap(err, "reply")
   168  		case rspData != nil:
   169  			rsp = rspData
   170  		}
   171  	}
   172  	return rsp, nil
   173  }
   174  
   175  // Issue #759 - we don't return error string for worries of non-determinism
   176  func redactError(err error) error {
   177  	// Do not redact system errors
   178  	// SystemErrors must be created in x/wasm and we can ensure determinism
   179  	if wasmvmtypes.ToSystemError(err) != nil {
   180  		return err
   181  	}
   182  
   183  	// FIXME: do we want to hardcode some constant string mappings here as well?
   184  	// Or better document them? (SDK error string may change on a patch release to fix wording)
   185  	// sdk/11 is out of gas
   186  	// sdk/5 is insufficient funds (on bank send)
   187  	// (we can theoretically redact less in the future, but this is a first step to safety)
   188  	codespace, code, _ := sdkerrors.ABCIInfo(err, false)
   189  	return fmt.Errorf("codespace: %s, code: %d", codespace, code)
   190  }
   191  
   192  func filterEvents(events []sdk.Event) []sdk.Event {
   193  	// pre-allocate space for efficiency
   194  	res := make([]sdk.Event, 0, len(events))
   195  	for _, ev := range events {
   196  		if ev.Type != "message" {
   197  			res = append(res, ev)
   198  		}
   199  	}
   200  	return res
   201  }
   202  
   203  func sdkEventsToWasmVMEvents(events []sdk.Event) []wasmvmtypes.Event {
   204  	res := make([]wasmvmtypes.Event, len(events))
   205  	for i, ev := range events {
   206  		res[i] = wasmvmtypes.Event{
   207  			Type:       ev.Type,
   208  			Attributes: sdkAttributesToWasmVMAttributes(ev.Attributes),
   209  		}
   210  	}
   211  	return res
   212  }
   213  
   214  func sdkAttributesToWasmVMAttributes(attrs []kv.Pair) []wasmvmtypes.EventAttribute {
   215  	res := make([]wasmvmtypes.EventAttribute, len(attrs))
   216  	for i, attr := range attrs {
   217  		res[i] = wasmvmtypes.EventAttribute{
   218  			Key:   string(attr.Key),
   219  			Value: string(attr.Value),
   220  		}
   221  	}
   222  	return res
   223  }