Skip to content

Commit c0b7401

Browse files
ecPabloCopilot
andauthored
feat: add execution error decode command (#604)
Adds execution error decode command. This command takes a JSON for an mcms-tools results json and attempts to decode the error revert reason using the ABI registries injected by the proposal context. TODO: Remove local replace once mcmslib is released with smartcontractkit/mcms#547 . ## AI Summary This pull request introduces significant improvements to error decoding for EVM transaction failures, adds a new CLI command for error decoding, and includes dependency updates. The most notable changes are the addition of robust helpers for decoding EVM execution errors, comprehensive tests for these helpers, and a new CLI command to decode errors from JSON files using the ABI registry. Several dependencies in `go.mod` have also been updated. **EVM Error Decoding Enhancements** * Added `DecodedExecutionError` struct and `tryDecodeExecutionError` function in `err_decode_helpers.go` to extract and decode revert reasons and underlying reasons from EVM execution errors, handling both custom and standard errors, and supporting decoding with or without an ABI registry. * Introduced `decodeRevertData` and `decodeRevertDataFromBytes` helpers to convert hex or byte revert data into human-readable error messages, falling back to custom error selectors if no decoder is available. **CLI Improvements** * Added a new CLI command `error-decode-evm` in `mcms_v2.go` that reads a JSON file containing transaction error data, loads the ABI registry from the specified environment, decodes the error using the new helpers, and prints the decoded revert and underlying reasons. [[1]](diffhunk://#diff-726dd799d05204c24b69b8bda1f4ede5393c5955b360b076db8d11bc01f56a1bR115) [[2]](diffhunk://#diff-726dd799d05204c24b69b8bda1f4ede5393c5955b360b076db8d11bc01f56a1bR150-R261) **Testing** * Added a comprehensive test `Test_tryDecodeExecutionError` in `err_decode_helpers_test.go` that covers decoding custom errors, standard errors, selectors, underlying reasons, and nil/empty cases for the new error decoding logic. **Dependency Updates** * Updated several dependencies in `go.mod`, including `github.com/smartcontractkit/chain-selectors`, `github.com/smartcontractkit/chainlink-testing-framework/framework`, `golang.org/x/oauth2`, and others. Also, replaced the `github.com/smartcontractkit/mcms` dependency to use a local path. [[1]](diffhunk://#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6R7-R8) [[2]](diffhunk://#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6L30-R39) [[3]](diffhunk://#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6L54-R56) [[4]](diffhunk://#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6L84-R86) [[5]](diffhunk://#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6L123-R131) [[6]](diffhunk://#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6L269-R271) [[7]](diffhunk://#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6L297) **Minor Improvements** * Improved error logging and error joining in the `MultiClient.retryWithBackups` method for more informative logs when retryable errors occur. * Cleaned up unnecessary comments in `maybeDataErr` for code clarity. These changes collectively improve the developer experience when diagnosing and understanding EVM transaction failures, both programmatically and via the CLI. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 8c67cb6 commit c0b7401

File tree

7 files changed

+470
-32
lines changed

7 files changed

+470
-32
lines changed

.changeset/whole-symbols-vanish.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
add error decode command

chain/evm/provider/rpcclient/multiclient.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,10 @@ func (mc *MultiClient) retryWithBackups(ctx context.Context, opName string, op f
401401

402402
err = op(timeoutCtx, client)
403403
if err != nil {
404-
mc.lggr.Warnf("traceID %q: chain %q: op: %q: client index %d: failed execution - retryable error '%s'", traceID.String(), mc.chainName, opName, rpcIndex, maybeDataErr(err))
404+
detailedErr := maybeDataErr(err)
405+
mc.lggr.Warnf("traceID %q: chain %q: op: %q: client index %d: failed execution - retryable error '%s'", traceID.String(), mc.chainName, opName, rpcIndex, detailedErr)
406+
err = errors.Join(err, detailedErr)
407+
405408
return err
406409
}
407410

engine/cld/legacy/cli/mcmsv2/err_decode_helpers.go

Lines changed: 163 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ import (
3030

3131
const noRevertData = "(no revert data)"
3232

33+
type errorSelector [4]byte
34+
35+
var emptySelector = errorSelector{}
36+
3337
type traceConfig struct {
3438
DisableStorage bool `json:"disableStorage,omitempty"`
3539
DisableMemory bool `json:"disableMemory,omitempty"`
@@ -57,25 +61,25 @@ type ErrSig struct {
5761
TypeVer string
5862
Name string
5963
Inputs abi.Arguments
60-
id [4]byte
64+
id errorSelector
6165
}
6266

6367
// ErrDecoder indexes custom-error selectors across many ABIs.
6468
type ErrDecoder struct {
65-
bySelector map[[4]byte][]ErrSig
69+
bySelector map[errorSelector][]ErrSig
6670
registry analyzer.EVMABIRegistry
6771
}
6872

6973
// NewErrDecoder builds an index from EVM ABI registry.
7074
func NewErrDecoder(registry analyzer.EVMABIRegistry) (*ErrDecoder, error) {
71-
idx := make(map[[4]byte][]ErrSig)
75+
idx := make(map[errorSelector][]ErrSig)
7276
for tv, jsonABI := range registry.GetAllABIs() {
7377
a, err := abi.JSON(strings.NewReader(jsonABI))
7478
if err != nil {
7579
return nil, fmt.Errorf("parse ABI for %s: %w", tv, err)
7680
}
7781
for name, e := range a.Errors {
78-
var key [4]byte
82+
var key errorSelector
7983
copy(key[:], e.ID[:4]) // selector is first 4 bytes of the keccak(sig)
8084
idx[key] = append(idx[key], ErrSig{
8185
TypeVer: tv,
@@ -181,7 +185,7 @@ func (d *ErrDecoder) decodeRecursive(revertData []byte, preferredABIJSON string)
181185
}
182186

183187
// --- B) Registry lookup
184-
var key [4]byte
188+
var key errorSelector
185189
copy(key[:], sel)
186190
cands, ok := d.bySelector[key]
187191
if !ok {
@@ -619,6 +623,160 @@ func prettyRevertFromError(err error, preferredABIJSON string, dec *ErrDecoder)
619623
return "", false
620624
}
621625

626+
// DecodedExecutionError contains the decoded revert reasons from an ExecutionError.
627+
type DecodedExecutionError struct {
628+
RevertReason string
629+
RevertReasonDecoded bool
630+
UnderlyingReason string
631+
UnderlyingReasonDecoded bool
632+
}
633+
634+
// tryDecodeExecutionError decodes an evm.ExecutionError into human-readable strings.
635+
// It first checks for RevertReasonDecoded and UnderlyingReasonDecoded fields.
636+
// If those are not available, it extracts RevertReasonRaw and UnderlyingReasonRaw from the struct
637+
// and decodes them using the provided ErrDecoder to match error selectors against the ABI registry.
638+
func tryDecodeExecutionError(execError *evm.ExecutionError, dec *ErrDecoder) DecodedExecutionError {
639+
if execError == nil {
640+
return DecodedExecutionError{}
641+
}
642+
643+
revertReason, revertDecoded := decodeRevertReasonWithStatus(execError, dec)
644+
underlyingReason, underlyingDecoded := decodeUnderlyingReasonWithStatus(execError, dec)
645+
646+
return DecodedExecutionError{
647+
RevertReason: revertReason,
648+
RevertReasonDecoded: revertDecoded,
649+
UnderlyingReason: underlyingReason,
650+
UnderlyingReasonDecoded: underlyingDecoded,
651+
}
652+
}
653+
654+
// decodeRevertReasonWithStatus decodes the revert reason and returns both the reason and decoded status.
655+
func decodeRevertReasonWithStatus(execError *evm.ExecutionError, dec *ErrDecoder) (string, bool) {
656+
if execError.RevertReasonDecoded != "" {
657+
return execError.RevertReasonDecoded, true
658+
}
659+
660+
if execError.RevertReasonRaw == nil {
661+
return "", false
662+
}
663+
664+
hasData := len(execError.RevertReasonRaw.Data) > 0
665+
hasSelector := execError.RevertReasonRaw.Selector != emptySelector
666+
667+
if hasData {
668+
if reason, decoded := tryDecodeFromData(execError.RevertReasonRaw, dec); decoded {
669+
return reason, true
670+
}
671+
}
672+
673+
if hasSelector && !hasData {
674+
reason := decodeSelectorOnly(execError.RevertReasonRaw.Selector, dec)
675+
return reason, reason != ""
676+
}
677+
678+
return "", false
679+
}
680+
681+
// tryDecodeFromData attempts to decode revert data from the CustomErrorData.
682+
func tryDecodeFromData(raw *evm.CustomErrorData, dec *ErrDecoder) (string, bool) {
683+
if len(raw.Data) >= 4 {
684+
if reason, decoded := decodeRevertDataFromBytes(raw.Data, dec, ""); decoded {
685+
return reason, true
686+
}
687+
}
688+
689+
if raw.Selector != emptySelector {
690+
if combined := raw.Combined(); len(combined) > 4 {
691+
return decodeRevertDataFromBytes(combined, dec, "")
692+
}
693+
}
694+
695+
return "", false
696+
}
697+
698+
// decodeSelectorOnly decodes an error when only the selector is available.
699+
func decodeSelectorOnly(selector errorSelector, dec *ErrDecoder) string {
700+
if dec == nil {
701+
return formatSelectorHex(selector)
702+
}
703+
704+
if matched, ok := dec.matchErrorSelector(selector); ok {
705+
return matched
706+
}
707+
708+
return formatSelectorHex(selector)
709+
}
710+
711+
// formatSelectorHex formats a selector as a hex string.
712+
func formatSelectorHex(selector errorSelector) string {
713+
return "custom error 0x" + hex.EncodeToString(selector[:])
714+
}
715+
716+
// decodeUnderlyingReasonWithStatus decodes the underlying reason and returns both the reason and decoded status.
717+
func decodeUnderlyingReasonWithStatus(execError *evm.ExecutionError, dec *ErrDecoder) (string, bool) {
718+
if execError.UnderlyingReasonDecoded != "" {
719+
return execError.UnderlyingReasonDecoded, true
720+
}
721+
722+
if execError.UnderlyingReasonRaw == "" {
723+
return "", false
724+
}
725+
726+
reason, decoded := decodeRevertData(execError.UnderlyingReasonRaw, dec, "")
727+
728+
return reason, decoded
729+
}
730+
731+
// decodeRevertData decodes a hex string containing revert data into a human-readable error message.
732+
func decodeRevertData(hexStr string, dec *ErrDecoder, preferredABIJSON string) (string, bool) {
733+
if hexStr == "" {
734+
return "", false
735+
}
736+
737+
data, err := hexutil.Decode(hexStr)
738+
if err != nil || len(data) == 0 {
739+
return "", false
740+
}
741+
742+
return decodeRevertDataFromBytes(data, dec, preferredABIJSON)
743+
}
744+
745+
// matchErrorSelector tries to resolve a 4-byte selector to an error name.
746+
// Returns "ErrorName(...) @Type@Version" if found in registry, or empty string if not found.
747+
func (d *ErrDecoder) matchErrorSelector(sel4 errorSelector) (string, bool) {
748+
if d == nil || d.bySelector == nil {
749+
return "", false
750+
}
751+
752+
cands, ok := d.bySelector[sel4]
753+
if !ok || len(cands) == 0 {
754+
return "", false
755+
}
756+
757+
// If multiple ABIs define the same selector, pick the first.
758+
c := cands[0]
759+
760+
return fmt.Sprintf("%s(...) @%s", c.Name, c.TypeVer), true
761+
}
762+
763+
// decodeRevertDataFromBytes decodes revert data bytes into a human-readable error message.
764+
func decodeRevertDataFromBytes(data []byte, dec *ErrDecoder, preferredABIJSON string) (string, bool) {
765+
if len(data) == 0 {
766+
return "", false
767+
}
768+
769+
if dec == nil {
770+
if len(data) >= 4 {
771+
return "custom error 0x" + hex.EncodeToString(data[:4]), true
772+
}
773+
774+
return "", false
775+
}
776+
777+
return prettyFromBytes(data, preferredABIJSON, dec)
778+
}
779+
622780
type callContractClient interface {
623781
CallContract(context.Context, ethereum.CallMsg, *big.Int) ([]byte, error)
624782
}

0 commit comments

Comments
 (0)