From 1a8ec4bc0856c47c2bd55abe4d2f7ff33129256b Mon Sep 17 00:00:00 2001 From: Dave Crighton Date: Fri, 27 Feb 2026 13:43:55 +0000 Subject: [PATCH 1/8] Best effort to decode nester error strings from reverts Signed-off-by: Dave Crighton --- internal/ethereum/exec_query.go | 70 ++++- internal/ethereum/exec_query_test.go | 365 +++++++++++++++++++++++++++ 2 files changed, 434 insertions(+), 1 deletion(-) diff --git a/internal/ethereum/exec_query.go b/internal/ethereum/exec_query.go index 701c20f2..a8f2627d 100644 --- a/internal/ethereum/exec_query.go +++ b/internal/ethereum/exec_query.go @@ -19,6 +19,7 @@ package ethereum import ( "bytes" "context" + "encoding/hex" "encoding/json" "fmt" @@ -169,7 +170,7 @@ func processRevertReason(ctx context.Context, outputData ethtypes.HexBytes0xPref errorInfo, err := defaultError.DecodeCallDataCtx(ctx, outputData) if err == nil && len(errorInfo.Children) == 1 { if strError, ok := errorInfo.Children[0].Value.(string); ok { - return strError + return unwrapNestedRevertReasons(ctx, strError, 0, errorAbis) } } log.L(ctx).Warnf("Invalid revert data: %s", outputData) @@ -195,6 +196,73 @@ func processRevertReason(ctx context.Context, outputData ethtypes.HexBytes0xPref return "" } +const maxNestedRevertDepth = 10 + +// unwrapNestedRevertReasons handles Solidity contracts that catch a revert's raw bytes +// and re-throw them inside a new Error(string) by doing string(reason). This produces +// an Error(string) whose decoded "string" contains raw ABI-encoded error data +// (including null bytes from ABI padding). We scan for all known error selectors +// (Error(string) plus any custom errors from the ABI), decode the earliest match, +// and recurse for nested Error(string) chains. +func unwrapNestedRevertReasons(ctx context.Context, s string, depth int, errorAbis []*abi.Entry) string { + if depth >= maxNestedRevertDepth { + return sanitizeBinaryString([]byte(s)) + } + + raw := []byte(s) + + // Find the earliest occurrence of any known error selector + bestIdx := -1 + var bestEntry *abi.Entry + if idx := bytes.Index(raw, defaultErrorID); idx >= 0 { + bestIdx = idx + bestEntry = defaultError + } + for _, e := range errorAbis { + sel := e.FunctionSelectorBytes() + if idx := bytes.Index(raw, sel); idx >= 0 && (bestIdx < 0 || idx < bestIdx) { + bestIdx = idx + bestEntry = e + } + } + + if bestIdx < 0 { + return sanitizeBinaryString(raw) + } + + prefix := sanitizeBinaryString(raw[:bestIdx]) + embedded := raw[bestIdx:] + + if bestEntry == defaultError { + errorInfo, err := defaultError.DecodeCallDataCtx(ctx, embedded) + if err == nil && len(errorInfo.Children) == 1 { + if nested, ok := errorInfo.Children[0].Value.(string); ok { + return prefix + unwrapNestedRevertReasons(ctx, nested, depth+1, errorAbis) + } + } + } else { + formatted := formatCustomError(ctx, bestEntry, embedded) + if formatted != "" { + return prefix + formatted + } + } + + log.L(ctx).Debugf("Could not decode nested revert at depth %d, hex-encoding remaining %d bytes", depth, len(embedded)) + return prefix + "0x" + hex.EncodeToString(embedded) +} + +// sanitizeBinaryString returns the input as a text string if it is entirely +// printable ASCII, or hex-encodes the entire input otherwise. This all-or-nothing +// approach avoids guessing where "readable" ends in an ambiguous binary blob. +func sanitizeBinaryString(raw []byte) string { + for _, b := range raw { + if b < 32 || b >= 127 { + return "0x" + hex.EncodeToString(raw) + } + } + return string(raw) +} + func formatCustomError(ctx context.Context, e *abi.Entry, outputData ethtypes.HexBytes0xPrefix) string { errorInfo, err := e.DecodeCallDataCtx(ctx, outputData) if err == nil { diff --git a/internal/ethereum/exec_query_test.go b/internal/ethereum/exec_query_test.go index 04521112..83536498 100644 --- a/internal/ethereum/exec_query_test.go +++ b/internal/ethereum/exec_query_test.go @@ -17,12 +17,17 @@ package ethereum import ( + "context" + "encoding/binary" + "encoding/hex" "encoding/json" + "strings" "testing" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-evmconnect/internal/msgs" + "github.com/hyperledger/firefly-signer/pkg/abi" "github.com/hyperledger/firefly-signer/pkg/ethsigner" "github.com/hyperledger/firefly-signer/pkg/ethtypes" "github.com/hyperledger/firefly-signer/pkg/rpcbackend" @@ -31,6 +36,25 @@ import ( "github.com/stretchr/testify/mock" ) +// buildErrorStringABI builds the raw ABI encoding for Error(string) with the given message bytes. +// The message can contain arbitrary bytes (including null bytes and nested ABI encodings). +func buildErrorStringABI(msgBytes []byte) []byte { + offset := make([]byte, 32) + binary.BigEndian.PutUint64(offset[24:], 0x20) + length := make([]byte, 32) + binary.BigEndian.PutUint64(length[24:], uint64(len(msgBytes))) + paddedLen := ((len(msgBytes) + 31) / 32) * 32 + data := make([]byte, paddedLen) + copy(data, msgBytes) + + result := make([]byte, 0, 4+32+32+paddedLen) + result = append(result, defaultErrorID...) + result = append(result, offset...) + result = append(result, length...) + result = append(result, data...) + return result +} + const sampleExecQuery = `{ "ffcapi": { "version": "v1.0.0", @@ -390,6 +414,347 @@ func TestExecQueryFailBadToAddress(t *testing.T) { } +func TestProcessRevertReasonNestedErrorString(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() + + // Outer Error(string) wrapping "outer: " + raw inner Error(string) ABI bytes. + // Simulates: catch (bytes memory reason) { revert(string.concat("outer: ", string(reason))); } + revertData := ethtypes.MustNewHexBytes0xPrefix( + "0x08c379a00000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000000000006b" + + "6f757465723a20" + + "08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000013" + + "696e6e6572206572726f72206d65737361676500000000000000000000000000" + + "000000000000000000000000000000000000000000") + + result := processRevertReason(ctx, revertData, nil) + assert.Equal(t, "outer: inner error message", result) +} + +func TestProcessRevertReasonDoubleNestedErrorString(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() + + // Three levels: Error("level1: " + Error("level2: " + Error("deepest error"))) + revertData := ethtypes.MustNewHexBytes0xPrefix( + "0x08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "00000000000000000000000000000000000000000000000000000000000000cc" + + "6c6576656c313a20" + // "level1: " + "08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000000000006c" + + "6c6576656c323a20" + // "level2: " + "08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000000000000d" + + "64656570657374206572726f720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + + result := processRevertReason(ctx, revertData, nil) + assert.Equal(t, "level1: level2: deepest error", result) +} + +func TestProcessRevertReasonNestedCustomError(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() + + // Define the custom error ABI first so we can use its real selector + customErr := &abi.Entry{ + Type: abi.Error, + Name: "MyCustomError", + Inputs: abi.ParameterArray{ + {Type: "bytes"}, + }, + } + customSelector := hex.EncodeToString(customErr.FunctionSelectorBytes()) + + // Error("[404]01d - caught bytes:" + MyCustomError(0xdeadbeef) raw ABI bytes) + revertData := ethtypes.MustNewHexBytes0xPrefix( + "0x08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000000000007c" + + "5b3430345d303164202d206361756768742062797465733a" + // "[404]01d - caught bytes:" + customSelector + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000004" + + "deadbeef00000000000000000000000000000000000000000000000000000000" + + "00000000") + + // With no error ABIs, the custom error can't be decoded — + // the entire nested section is hex-encoded + result := processRevertReason(ctx, revertData, nil) + assert.True(t, strings.HasPrefix(result, "0x")) + assert.NotContains(t, result, "\x00") + + // Now provide the custom error ABI so it CAN be decoded + result = processRevertReason(ctx, revertData, []*abi.Entry{customErr}) + assert.Equal(t, `[404]01d - caught bytes:MyCustomError("deadbeef")`, result) +} + +func TestProcessRevertReasonUnknownNestedBinaryFallback(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() + + // Error("[404]01d - caught bytes:" + unknown error selector + binary payload) + // The embedded selector ac8ae0 is NOT in our error ABIs, so the function + // falls back to readable prefix + hex-encoded binary remainder. + revertData := ethtypes.MustNewHexBytes0xPrefix("0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000007b5b3430345d303164202d2063617567687420627974" + + "65733aac8ae000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004deadbeef000000000000000000000000000000000000000000000000000000000000000000") + + result := processRevertReason(ctx, revertData, nil) + + // Entire nested section is hex-encoded since no selector could be decoded + assert.True(t, strings.HasPrefix(result, "0x")) + assert.Contains(t, result, "deadbeef") + assert.NotContains(t, result, "\x00") +} + +func TestProcessRevertReasonPlainStringUnchanged(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() + + // A normal Error(string) with no nested binary data should pass through unchanged + revertData := ethtypes.MustNewHexBytes0xPrefix( + "0x08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000000000001a" + + "4e6f7420656e6f7567682045746865722070726f76696465642e000000000000") + + result := processRevertReason(ctx, revertData, nil) + assert.Equal(t, "Not enough Ether provided.", result) +} + +// ---- sanitizeBinaryString unit tests ---- + +func TestSanitizeBinaryStringEmpty(t *testing.T) { + assert.Equal(t, "", sanitizeBinaryString(nil)) + assert.Equal(t, "", sanitizeBinaryString([]byte{})) +} + +func TestSanitizeBinaryStringPureASCII(t *testing.T) { + assert.Equal(t, "hello world", sanitizeBinaryString([]byte("hello world"))) +} + +func TestSanitizeBinaryStringTrailingNulls(t *testing.T) { + // Any non-printable byte → entire input is hex-encoded + assert.Equal(t, "0x736f6d65206572726f72000000", sanitizeBinaryString([]byte("some error\x00\x00\x00"))) +} + +func TestSanitizeBinaryStringPureBinary(t *testing.T) { + assert.Equal(t, "0xdeadbeef", sanitizeBinaryString([]byte{0xde, 0xad, 0xbe, 0xef})) +} + +func TestSanitizeBinaryStringPureNulls(t *testing.T) { + assert.Equal(t, "0x000000", sanitizeBinaryString([]byte{0x00, 0x00, 0x00})) +} + +func TestSanitizeBinaryStringSingleNullByte(t *testing.T) { + assert.Equal(t, "0x00", sanitizeBinaryString([]byte{0x00})) +} + +func TestSanitizeBinaryStringTextThenBinary(t *testing.T) { + input := append([]byte("error: "), 0xde, 0xad, 0xbe, 0xef) + assert.Equal(t, "0x6572726f723a20deadbeef", sanitizeBinaryString(input)) +} + +func TestSanitizeBinaryStringTextThenNulls(t *testing.T) { + input := append([]byte("error: "), 0x00, 0x00) + assert.Equal(t, "0x6572726f723a200000", sanitizeBinaryString(input)) +} + +func TestSanitizeBinaryStringControlCharAtStart(t *testing.T) { + input := []byte{0x01, 'h', 'e', 'l', 'l', 'o'} + assert.Equal(t, "0x0168656c6c6f", sanitizeBinaryString(input)) +} + +// ---- unwrapNestedRevertReasons unit tests ---- + +func TestUnwrapEmptyString(t *testing.T) { + ctx := context.Background() + assert.Equal(t, "", unwrapNestedRevertReasons(ctx, "", 0, nil)) +} + +func TestUnwrapPlainASCII(t *testing.T) { + ctx := context.Background() + assert.Equal(t, "simple revert", unwrapNestedRevertReasons(ctx, "simple revert", 0, nil)) +} + +func TestUnwrapTrailingNulls(t *testing.T) { + ctx := context.Background() + // "some error" + trailing nulls → entire thing hex-encoded since it contains non-printable bytes + result := unwrapNestedRevertReasons(ctx, "some error\x00\x00\x00", 0, nil) + assert.Equal(t, "0x736f6d65206572726f72000000", result) +} + +func TestUnwrapNestedErrorStringMalformedABI(t *testing.T) { + ctx := context.Background() + + // Error(string) selector followed by garbage — can't ABI-decode, falls back to hex + badData := "prefix:" + string(defaultErrorID) + "truncated" + result := unwrapNestedRevertReasons(ctx, badData, 0, nil) + // "prefix:" is pure ASCII so stays as text; the embedded section is hex-encoded + assert.Equal(t, "prefix:0x08c379a07472756e6361746564", result) +} + +func TestUnwrapDepthLimitReached(t *testing.T) { + ctx := context.Background() + + innerABI := buildErrorStringABI([]byte("should not decode")) + s := "prefix:" + string(innerABI) + + // At maxNestedRevertDepth, no further decoding happens — entire string hex-encoded + result := unwrapNestedRevertReasons(ctx, s, maxNestedRevertDepth, nil) + assert.True(t, strings.HasPrefix(result, "0x")) + assert.NotEqual(t, "prefix:should not decode", result) + assert.NotContains(t, result, "\x00") +} + +func TestUnwrapDepthLimitMinusOneStillDecodes(t *testing.T) { + ctx := context.Background() + + innerABI := buildErrorStringABI([]byte("decoded at limit")) + s := "prefix:" + string(innerABI) + + result := unwrapNestedRevertReasons(ctx, s, maxNestedRevertDepth-1, nil) + assert.Equal(t, "prefix:decoded at limit", result) +} + +func TestUnwrapErrorStringSelectorPickedOverCustomWhenEarlier(t *testing.T) { + ctx := context.Background() + + customErr := &abi.Entry{ + Type: abi.Error, + Name: "CustomErr", + Inputs: abi.ParameterArray{{Type: "uint256"}}, + } + customSel := customErr.FunctionSelectorBytes() + + innerErrorABI := buildErrorStringABI([]byte("decoded-inner")) + // Error(string) appears first, then the custom selector later + s := "first:" + string(innerErrorABI) + "\x00\x00" + string(customSel) + "\x00\x00" + result := unwrapNestedRevertReasons(ctx, s, 0, []*abi.Entry{customErr}) + assert.Equal(t, "first:decoded-inner", result) +} + +func TestUnwrapCustomSelectorPickedOverErrorStringWhenEarlier(t *testing.T) { + ctx := context.Background() + + customErr := &abi.Entry{ + Type: abi.Error, + Name: "EarlyErr", + Inputs: abi.ParameterArray{{Type: "uint256"}}, + } + customSel := customErr.FunctionSelectorBytes() + + // Build a custom error encoding: selector + one uint256 word (value=42) + arg := make([]byte, 32) + binary.BigEndian.PutUint64(arg[24:], 42) + customEncoded := append(customSel, arg...) + + innerErrorABI := buildErrorStringABI([]byte("late-error")) + // Custom selector appears before the Error(string) selector + s := "head:" + string(customEncoded) + "middle:" + string(innerErrorABI) + result := unwrapNestedRevertReasons(ctx, s, 0, []*abi.Entry{customErr}) + assert.Equal(t, `head:EarlyErr("42")`, result) +} + +func TestUnwrapCustomErrorMultipleParams(t *testing.T) { + ctx := context.Background() + + customErr := &abi.Entry{ + Type: abi.Error, + Name: "DetailedError", + Inputs: abi.ParameterArray{ + {Type: "uint256", Name: "code"}, + {Type: "uint256", Name: "extra"}, + }, + } + customSel := customErr.FunctionSelectorBytes() + + arg1 := make([]byte, 32) + binary.BigEndian.PutUint64(arg1[24:], 404) + arg2 := make([]byte, 32) + binary.BigEndian.PutUint64(arg2[24:], 999) + customEncoded := append(customSel, arg1...) + customEncoded = append(customEncoded, arg2...) + + s := "err:" + string(customEncoded) + result := unwrapNestedRevertReasons(ctx, s, 0, []*abi.Entry{customErr}) + assert.Equal(t, `err:DetailedError("404", "999")`, result) +} + +func TestUnwrapCustomErrorDecodeFails(t *testing.T) { + ctx := context.Background() + + customErr := &abi.Entry{ + Type: abi.Error, + Name: "BadErr", + Inputs: abi.ParameterArray{{Type: "uint256"}, {Type: "uint256"}}, + } + customSel := customErr.FunctionSelectorBytes() + + // Only 1 word of data but the error needs 2 — decode will fail + arg := make([]byte, 32) + binary.BigEndian.PutUint64(arg[24:], 1) + truncated := append(customSel, arg...) + + s := "prefix:" + string(truncated) + result := unwrapNestedRevertReasons(ctx, s, 0, []*abi.Entry{customErr}) + // "prefix:" is clean ASCII, embedded section is hex-encoded + assert.True(t, strings.HasPrefix(result, "prefix:0x")) + assert.Contains(t, result, hex.EncodeToString(customSel)) +} + +func TestUnwrapPureBinaryNoSelector(t *testing.T) { + ctx := context.Background() + + s := string([]byte{0xde, 0xad, 0xbe, 0xef, 0x00, 0x00}) + result := unwrapNestedRevertReasons(ctx, s, 0, nil) + assert.Equal(t, "0xdeadbeef0000", result) +} + +func TestUnwrapNilErrorAbis(t *testing.T) { + ctx := context.Background() + + innerABI := buildErrorStringABI([]byte("works with nil abis")) + s := "check:" + string(innerABI) + result := unwrapNestedRevertReasons(ctx, s, 0, nil) + assert.Equal(t, "check:works with nil abis", result) +} + +func TestUnwrapEmptyErrorAbis(t *testing.T) { + ctx := context.Background() + + innerABI := buildErrorStringABI([]byte("works with empty abis")) + s := "check:" + string(innerABI) + result := unwrapNestedRevertReasons(ctx, s, 0, []*abi.Entry{}) + assert.Equal(t, "check:works with empty abis", result) +} + +func TestUnwrapNullBytesBetweenTextAndSelector(t *testing.T) { + ctx := context.Background() + + innerABI := buildErrorStringABI([]byte("inner")) + s := "text\x00\x00\x00" + string(innerABI) + result := unwrapNestedRevertReasons(ctx, s, 0, nil) + // Prefix "text\x00\x00\x00" has non-printable bytes → entirely hex-encoded + assert.True(t, strings.HasPrefix(result, "0x")) + assert.True(t, strings.HasSuffix(result, "inner")) +} + +func TestUnwrapNestedWithTrailingGarbage(t *testing.T) { + ctx := context.Background() + + // Inner Error(string) followed by trailing null padding (simulates ABI padding from outer encoding) + innerABI := buildErrorStringABI([]byte("real message")) + s := "prefix:" + string(innerABI) + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + result := unwrapNestedRevertReasons(ctx, s, 0, nil) + assert.Equal(t, "prefix:real message", result) +} + func TestExecQueryFailBadToParams(t *testing.T) { ctx, c, _, done := newTestConnector(t) From 74f7a9554808fac5b154895c9c2222dac602c821 Mon Sep 17 00:00:00 2001 From: Dave Crighton Date: Mon, 2 Mar 2026 11:26:49 +0000 Subject: [PATCH 2/8] Refactor decode to use ff-signer Signed-off-by: Dave Crighton --- internal/ethereum/estimate_gas_test.go | 16 +- internal/ethereum/exec_query.go | 141 +--------- internal/ethereum/exec_query_test.go | 366 ++++++++++++------------- internal/ethereum/get_receipt.go | 11 +- 4 files changed, 193 insertions(+), 341 deletions(-) diff --git a/internal/ethereum/estimate_gas_test.go b/internal/ethereum/estimate_gas_test.go index d0060b16..fe5b4131 100644 --- a/internal/ethereum/estimate_gas_test.go +++ b/internal/ethereum/estimate_gas_test.go @@ -17,7 +17,6 @@ package ethereum import ( - "context" "encoding/hex" "encoding/json" "testing" @@ -32,6 +31,12 @@ import ( "github.com/stretchr/testify/mock" ) +var testDefaultError = &abi.Entry{ + Type: abi.Error, + Name: "Error", + Inputs: abi.ParameterArray{{Type: "string"}}, +} + const sampleGasEstimate = `{ "ffcapi": { "version": "v1.0.0", @@ -150,7 +155,7 @@ func TestGasEstimateFailRevertReasonInData(t *testing.T) { ctx, c, mRPC, done := newTestConnector(t) defer done() - errData, err := defaultError.EncodeCallDataValues([]string{"this reason"}) + errData, err := testDefaultError.EncodeCallDataValues([]string{"this reason"}) assert.NoError(t, err) mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_estimateGas", mock.MatchedBy(func(tx *ethsigner.Transaction) bool { @@ -205,7 +210,7 @@ func TestGasEstimateFailThenRevertDataFromCall(t *testing.T) { ctx, c, mRPC, done := newTestConnector(t) defer done() - errData, err := defaultError.EncodeCallDataValues([]string{"this reason"}) + errData, err := testDefaultError.EncodeCallDataValues([]string{"this reason"}) assert.NoError(t, err) mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_estimateGas", mock.MatchedBy(func(tx *ethsigner.Transaction) bool { @@ -261,7 +266,7 @@ func TestGasEstimateFailCustomErrorCannotParse(t *testing.T) { ctx, c, mRPC, done := newTestConnector(t) defer done() - errData, err := defaultError.EncodeCallDataValues([]string{"this reason"}) + errData, err := testDefaultError.EncodeCallDataValues([]string{"this reason"}) assert.NoError(t, err) mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_estimateGas", mock.MatchedBy(func(tx *ethsigner.Transaction) bool { @@ -281,6 +286,3 @@ func TestGasEstimateFailCustomErrorCannotParse(t *testing.T) { } -func TestFormatErrorComponentBadCV(t *testing.T) { - assert.Equal(t, "?", formatErrorComponent(context.Background(), &abi.ComponentValue{})) -} diff --git a/internal/ethereum/exec_query.go b/internal/ethereum/exec_query.go index a8f2627d..99943f72 100644 --- a/internal/ethereum/exec_query.go +++ b/internal/ethereum/exec_query.go @@ -17,11 +17,8 @@ package ethereum import ( - "bytes" "context" - "encoding/hex" "encoding/json" - "fmt" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" @@ -35,21 +32,6 @@ import ( "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) -var ( - // See https://docs.soliditylang.org/en/v0.8.14/control-structures.html#revert - // There default error for `revert("some error")` is a function Error(string) - defaultError = &abi.Entry{ - Type: abi.Error, - Name: "Error", - Inputs: abi.ParameterArray{ - { - Type: "string", - }, - }, - } - defaultErrorID = defaultError.FunctionSelectorBytes() -) - func (c *ethConnector) QueryInvoke(ctx context.Context, req *ffcapi.QueryInvokeRequest) (*ffcapi.QueryInvokeResponse, ffcapi.ErrorReason, error) { // Parse the input JSON data, to build the call data callData, method, err := c.prepareCallData(ctx, &req.TransactionInput) @@ -165,126 +147,15 @@ func processRevertReason(ctx context.Context, outputData ethtypes.HexBytes0xPref // result in a multiple of 32 bytes) and has exactly 4 extra bytes for a function // signature if len(outputData)%32 == 4 { - signature := outputData[0:4] - if bytes.Equal(signature, defaultErrorID) { - errorInfo, err := defaultError.DecodeCallDataCtx(ctx, outputData) - if err == nil && len(errorInfo.Children) == 1 { - if strError, ok := errorInfo.Children[0].Value.(string); ok { - return unwrapNestedRevertReasons(ctx, strError, 0, errorAbis) - } - } - log.L(ctx).Warnf("Invalid revert data: %s", outputData) - } else if len(errorAbis) > 0 { - // check if the signature matches any of the declared custom error definitions - for _, e := range errorAbis { - idBytes := e.FunctionSelectorBytes() - if bytes.Equal(signature, idBytes) { - err := formatCustomError(ctx, e, outputData) - if err == "" { - log.L(ctx).Warnf("Invalid revert data: %s", outputData) - break - } - return err - } - } + var errors abi.ABI + for _, e := range errorAbis { + errors = append(errors, e) + } + if result, ok := errors.UnwrapErrorStringCtx(ctx, outputData); ok { + return result } - // we call this "transient error" because it signals to the caller of the case - // that the raw revert data is returned, then it gets thrown away. so no need to translate log.L(ctx).Debugf("Directly returning revert reason: %s", outputData) return outputData.String() } return "" } - -const maxNestedRevertDepth = 10 - -// unwrapNestedRevertReasons handles Solidity contracts that catch a revert's raw bytes -// and re-throw them inside a new Error(string) by doing string(reason). This produces -// an Error(string) whose decoded "string" contains raw ABI-encoded error data -// (including null bytes from ABI padding). We scan for all known error selectors -// (Error(string) plus any custom errors from the ABI), decode the earliest match, -// and recurse for nested Error(string) chains. -func unwrapNestedRevertReasons(ctx context.Context, s string, depth int, errorAbis []*abi.Entry) string { - if depth >= maxNestedRevertDepth { - return sanitizeBinaryString([]byte(s)) - } - - raw := []byte(s) - - // Find the earliest occurrence of any known error selector - bestIdx := -1 - var bestEntry *abi.Entry - if idx := bytes.Index(raw, defaultErrorID); idx >= 0 { - bestIdx = idx - bestEntry = defaultError - } - for _, e := range errorAbis { - sel := e.FunctionSelectorBytes() - if idx := bytes.Index(raw, sel); idx >= 0 && (bestIdx < 0 || idx < bestIdx) { - bestIdx = idx - bestEntry = e - } - } - - if bestIdx < 0 { - return sanitizeBinaryString(raw) - } - - prefix := sanitizeBinaryString(raw[:bestIdx]) - embedded := raw[bestIdx:] - - if bestEntry == defaultError { - errorInfo, err := defaultError.DecodeCallDataCtx(ctx, embedded) - if err == nil && len(errorInfo.Children) == 1 { - if nested, ok := errorInfo.Children[0].Value.(string); ok { - return prefix + unwrapNestedRevertReasons(ctx, nested, depth+1, errorAbis) - } - } - } else { - formatted := formatCustomError(ctx, bestEntry, embedded) - if formatted != "" { - return prefix + formatted - } - } - - log.L(ctx).Debugf("Could not decode nested revert at depth %d, hex-encoding remaining %d bytes", depth, len(embedded)) - return prefix + "0x" + hex.EncodeToString(embedded) -} - -// sanitizeBinaryString returns the input as a text string if it is entirely -// printable ASCII, or hex-encodes the entire input otherwise. This all-or-nothing -// approach avoids guessing where "readable" ends in an ambiguous binary blob. -func sanitizeBinaryString(raw []byte) string { - for _, b := range raw { - if b < 32 || b >= 127 { - return "0x" + hex.EncodeToString(raw) - } - } - return string(raw) -} - -func formatCustomError(ctx context.Context, e *abi.Entry, outputData ethtypes.HexBytes0xPrefix) string { - errorInfo, err := e.DecodeCallDataCtx(ctx, outputData) - if err == nil { - strError := fmt.Sprintf("%s(", e.Name) - for i, child := range errorInfo.Children { - strError += formatErrorComponent(ctx, child) - if i < len(errorInfo.Children)-1 { - strError += ", " - } - } - strError += ")" - return strError - } - return "" -} - -func formatErrorComponent(ctx context.Context, child *abi.ComponentValue) string { - value, err := child.JSON() - if err != nil { - // if this part of the error structure failed to parse, simply append "?" - log.L(ctx).Warnf("Failed to parse component value in error: %+v", child) - return "?" - } - return string(value) -} diff --git a/internal/ethereum/exec_query_test.go b/internal/ethereum/exec_query_test.go index 83536498..e89d3a4f 100644 --- a/internal/ethereum/exec_query_test.go +++ b/internal/ethereum/exec_query_test.go @@ -17,8 +17,6 @@ package ethereum import ( - "context" - "encoding/binary" "encoding/hex" "encoding/json" "strings" @@ -36,25 +34,6 @@ import ( "github.com/stretchr/testify/mock" ) -// buildErrorStringABI builds the raw ABI encoding for Error(string) with the given message bytes. -// The message can contain arbitrary bytes (including null bytes and nested ABI encodings). -func buildErrorStringABI(msgBytes []byte) []byte { - offset := make([]byte, 32) - binary.BigEndian.PutUint64(offset[24:], 0x20) - length := make([]byte, 32) - binary.BigEndian.PutUint64(length[24:], uint64(len(msgBytes))) - paddedLen := ((len(msgBytes) + 31) / 32) * 32 - data := make([]byte, paddedLen) - copy(data, msgBytes) - - result := make([]byte, 0, 4+32+32+paddedLen) - result = append(result, defaultErrorID...) - result = append(result, offset...) - result = append(result, length...) - result = append(result, data...) - return result -} - const sampleExecQuery = `{ "ffcapi": { "version": "v1.0.0", @@ -188,7 +167,7 @@ func TestExecQueryCustomErrorRevertData(t *testing.T) { assert.NoError(t, err) _, reason, err := c.QueryInvoke(ctx, &req) assert.Equal(t, ffcapi.ErrorReasonTransactionReverted, reason) - expectedError := i18n.NewError(ctx, msgs.MsgReverted, `GreaterThanTen("20", "20")`) + expectedError := i18n.NewError(ctx, msgs.MsgReverted, `GreaterThanTen("20","20")`) assert.Equal(t, expectedError.Error(), err.Error()) } @@ -491,7 +470,7 @@ func TestProcessRevertReasonNestedCustomError(t *testing.T) { // Now provide the custom error ABI so it CAN be decoded result = processRevertReason(ctx, revertData, []*abi.Entry{customErr}) - assert.Equal(t, `[404]01d - caught bytes:MyCustomError("deadbeef")`, result) + assert.Equal(t, `[404]01d - caught bytes:MyCustomError("0xdeadbeef")`, result) } func TestProcessRevertReasonUnknownNestedBinaryFallback(t *testing.T) { @@ -527,232 +506,237 @@ func TestProcessRevertReasonPlainStringUnchanged(t *testing.T) { assert.Equal(t, "Not enough Ether provided.", result) } -// ---- sanitizeBinaryString unit tests ---- - -func TestSanitizeBinaryStringEmpty(t *testing.T) { - assert.Equal(t, "", sanitizeBinaryString(nil)) - assert.Equal(t, "", sanitizeBinaryString([]byte{})) -} +// ---- processRevertReason behavioral tests ---- -func TestSanitizeBinaryStringPureASCII(t *testing.T) { - assert.Equal(t, "hello world", sanitizeBinaryString([]byte("hello world"))) -} - -func TestSanitizeBinaryStringTrailingNulls(t *testing.T) { - // Any non-printable byte → entire input is hex-encoded - assert.Equal(t, "0x736f6d65206572726f72000000", sanitizeBinaryString([]byte("some error\x00\x00\x00"))) -} +func TestProcessRevertReasonNonRevertData(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() -func TestSanitizeBinaryStringPureBinary(t *testing.T) { - assert.Equal(t, "0xdeadbeef", sanitizeBinaryString([]byte{0xde, 0xad, 0xbe, 0xef})) -} + // Data whose length is a multiple of 32 is NOT revert data + data32 := ethtypes.MustNewHexBytes0xPrefix("0x" + strings.Repeat("ab", 32)) + assert.Equal(t, "", processRevertReason(ctx, data32, nil)) -func TestSanitizeBinaryStringPureNulls(t *testing.T) { - assert.Equal(t, "0x000000", sanitizeBinaryString([]byte{0x00, 0x00, 0x00})) + data64 := ethtypes.MustNewHexBytes0xPrefix("0x" + strings.Repeat("ab", 64)) + assert.Equal(t, "", processRevertReason(ctx, data64, nil)) } -func TestSanitizeBinaryStringSingleNullByte(t *testing.T) { - assert.Equal(t, "0x00", sanitizeBinaryString([]byte{0x00})) -} +func TestProcessRevertReasonEmptyData(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() -func TestSanitizeBinaryStringTextThenBinary(t *testing.T) { - input := append([]byte("error: "), 0xde, 0xad, 0xbe, 0xef) - assert.Equal(t, "0x6572726f723a20deadbeef", sanitizeBinaryString(input)) + assert.Equal(t, "", processRevertReason(ctx, ethtypes.HexBytes0xPrefix{}, nil)) + assert.Equal(t, "", processRevertReason(ctx, nil, nil)) } -func TestSanitizeBinaryStringTextThenNulls(t *testing.T) { - input := append([]byte("error: "), 0x00, 0x00) - assert.Equal(t, "0x6572726f723a200000", sanitizeBinaryString(input)) -} +func TestProcessRevertReasonBareSelector(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() -func TestSanitizeBinaryStringControlCharAtStart(t *testing.T) { - input := []byte{0x01, 'h', 'e', 'l', 'l', 'o'} - assert.Equal(t, "0x0168656c6c6f", sanitizeBinaryString(input)) + // Just 4 bytes — valid error selector but no params to decode + data := ethtypes.MustNewHexBytes0xPrefix("0x08c379a0") + result := processRevertReason(ctx, data, nil) + assert.Equal(t, "0x08c379a0", result) } -// ---- unwrapNestedRevertReasons unit tests ---- +func TestProcessRevertReasonCustomErrorStringParam(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() -func TestUnwrapEmptyString(t *testing.T) { - ctx := context.Background() - assert.Equal(t, "", unwrapNestedRevertReasons(ctx, "", 0, nil)) -} + customErr := &abi.Entry{ + Type: abi.Error, + Name: "LessThanOne", + Inputs: abi.ParameterArray{{Name: "x", Type: "string"}}, + } + errData, err := customErr.EncodeCallDataValues([]string{"bad value"}) + assert.NoError(t, err) -func TestUnwrapPlainASCII(t *testing.T) { - ctx := context.Background() - assert.Equal(t, "simple revert", unwrapNestedRevertReasons(ctx, "simple revert", 0, nil)) + result := processRevertReason(ctx, errData, []*abi.Entry{customErr}) + assert.Equal(t, `LessThanOne("bad value")`, result) } -func TestUnwrapTrailingNulls(t *testing.T) { - ctx := context.Background() - // "some error" + trailing nulls → entire thing hex-encoded since it contains non-printable bytes - result := unwrapNestedRevertReasons(ctx, "some error\x00\x00\x00", 0, nil) - assert.Equal(t, "0x736f6d65206572726f72000000", result) -} +func TestProcessRevertReasonCustomErrorAddressParam(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() -func TestUnwrapNestedErrorStringMalformedABI(t *testing.T) { - ctx := context.Background() + customErr := &abi.Entry{ + Type: abi.Error, + Name: "Unauthorized", + Inputs: abi.ParameterArray{{Name: "caller", Type: "address"}}, + } + errData, err := customErr.EncodeCallDataJSON([]byte(`{"caller":"0x03706Ff580119B130E7D26C5e816913123C24d89"}`)) + assert.NoError(t, err) - // Error(string) selector followed by garbage — can't ABI-decode, falls back to hex - badData := "prefix:" + string(defaultErrorID) + "truncated" - result := unwrapNestedRevertReasons(ctx, badData, 0, nil) - // "prefix:" is pure ASCII so stays as text; the embedded section is hex-encoded - assert.Equal(t, "prefix:0x08c379a07472756e6361746564", result) + result := processRevertReason(ctx, errData, []*abi.Entry{customErr}) + assert.Equal(t, `Unauthorized("0x03706ff580119b130e7d26c5e816913123c24d89")`, result) } -func TestUnwrapDepthLimitReached(t *testing.T) { - ctx := context.Background() +func TestProcessRevertReasonMultipleCustomErrorsCorrectMatch(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() - innerABI := buildErrorStringABI([]byte("should not decode")) - s := "prefix:" + string(innerABI) + errA := &abi.Entry{ + Type: abi.Error, + Name: "ErrAlpha", + Inputs: abi.ParameterArray{{Type: "uint256"}}, + } + errB := &abi.Entry{ + Type: abi.Error, + Name: "ErrBeta", + Inputs: abi.ParameterArray{{Type: "string"}}, + } - // At maxNestedRevertDepth, no further decoding happens — entire string hex-encoded - result := unwrapNestedRevertReasons(ctx, s, maxNestedRevertDepth, nil) - assert.True(t, strings.HasPrefix(result, "0x")) - assert.NotEqual(t, "prefix:should not decode", result) - assert.NotContains(t, result, "\x00") + // Encode errB and verify errA doesn't accidentally match + errData, err := errB.EncodeCallDataValues([]string{"beta triggered"}) + assert.NoError(t, err) + result := processRevertReason(ctx, errData, []*abi.Entry{errA, errB}) + assert.Equal(t, `ErrBeta("beta triggered")`, result) + + // Now encode errA and verify it matches + errData, err = errA.EncodeCallDataValues([]string{"42"}) + assert.NoError(t, err) + result = processRevertReason(ctx, errData, []*abi.Entry{errA, errB}) + assert.Equal(t, `ErrAlpha("42")`, result) } -func TestUnwrapDepthLimitMinusOneStillDecodes(t *testing.T) { - ctx := context.Background() +func TestProcessRevertReasonUnknownSelectorFallsThrough(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() - innerABI := buildErrorStringABI([]byte("decoded at limit")) - s := "prefix:" + string(innerABI) + // Selector that doesn't match any known error — returns raw hex + data := ethtypes.MustNewHexBytes0xPrefix( + "0xdeadbeef" + strings.Repeat("00", 32)) - result := unwrapNestedRevertReasons(ctx, s, maxNestedRevertDepth-1, nil) - assert.Equal(t, "prefix:decoded at limit", result) + result := processRevertReason(ctx, data, nil) + assert.Equal(t, "0xdeadbeef"+strings.Repeat("00", 32), result) } -func TestUnwrapErrorStringSelectorPickedOverCustomWhenEarlier(t *testing.T) { - ctx := context.Background() +func TestProcessRevertReasonErrorSelectorMalformedData(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() - customErr := &abi.Entry{ - Type: abi.Error, - Name: "CustomErr", - Inputs: abi.ParameterArray{{Type: "uint256"}}, - } - customSel := customErr.FunctionSelectorBytes() + // Error(string) selector but ABI data is garbage — should fall through to hex + data := ethtypes.MustNewHexBytes0xPrefix( + "0x08c379a0" + + "00000000000000000000000000000000000000000000000000000000baadf00d") - innerErrorABI := buildErrorStringABI([]byte("decoded-inner")) - // Error(string) appears first, then the custom selector later - s := "first:" + string(innerErrorABI) + "\x00\x00" + string(customSel) + "\x00\x00" - result := unwrapNestedRevertReasons(ctx, s, 0, []*abi.Entry{customErr}) - assert.Equal(t, "first:decoded-inner", result) + result := processRevertReason(ctx, data, nil) + assert.Equal(t, "0x08c379a000000000000000000000000000000000000000000000000000000000baadf00d", result) } -func TestUnwrapCustomSelectorPickedOverErrorStringWhenEarlier(t *testing.T) { - ctx := context.Background() +func TestProcessRevertReasonCustomErrorTruncatedData(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() customErr := &abi.Entry{ Type: abi.Error, - Name: "EarlyErr", - Inputs: abi.ParameterArray{{Type: "uint256"}}, + Name: "NeedsTwoWords", + Inputs: abi.ParameterArray{{Type: "uint256"}, {Type: "uint256"}}, } - customSel := customErr.FunctionSelectorBytes() - // Build a custom error encoding: selector + one uint256 word (value=42) - arg := make([]byte, 32) - binary.BigEndian.PutUint64(arg[24:], 42) - customEncoded := append(customSel, arg...) + // Only 1 word of data — not enough for the error's 2 params + data := ethtypes.MustNewHexBytes0xPrefix( + "0x" + hex.EncodeToString(customErr.FunctionSelectorBytes()) + + strings.Repeat("00", 32)) - innerErrorABI := buildErrorStringABI([]byte("late-error")) - // Custom selector appears before the Error(string) selector - s := "head:" + string(customEncoded) + "middle:" + string(innerErrorABI) - result := unwrapNestedRevertReasons(ctx, s, 0, []*abi.Entry{customErr}) - assert.Equal(t, `head:EarlyErr("42")`, result) + result := processRevertReason(ctx, data, []*abi.Entry{customErr}) + assert.True(t, strings.HasPrefix(result, "0x")) } -func TestUnwrapCustomErrorMultipleParams(t *testing.T) { - ctx := context.Background() - - customErr := &abi.Entry{ - Type: abi.Error, - Name: "DetailedError", - Inputs: abi.ParameterArray{ - {Type: "uint256", Name: "code"}, - {Type: "uint256", Name: "extra"}, - }, - } - customSel := customErr.FunctionSelectorBytes() +func TestProcessRevertReasonNilErrorAbis(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() - arg1 := make([]byte, 32) - binary.BigEndian.PutUint64(arg1[24:], 404) - arg2 := make([]byte, 32) - binary.BigEndian.PutUint64(arg2[24:], 999) - customEncoded := append(customSel, arg1...) - customEncoded = append(customEncoded, arg2...) + // "hello world!" = 12 bytes, padded to 32. + // Total: 4 (sel) + 32 (offset) + 32 (len) + 32 (data) = 100. 100%32 = 4 ✓ + revertData := ethtypes.MustNewHexBytes0xPrefix( + "0x08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000000000000c" + + "68656c6c6f20776f726c64210000000000000000000000000000000000000000") - s := "err:" + string(customEncoded) - result := unwrapNestedRevertReasons(ctx, s, 0, []*abi.Entry{customErr}) - assert.Equal(t, `err:DetailedError("404", "999")`, result) + assert.Equal(t, "hello world!", processRevertReason(ctx, revertData, nil)) } -func TestUnwrapCustomErrorDecodeFails(t *testing.T) { - ctx := context.Background() - - customErr := &abi.Entry{ - Type: abi.Error, - Name: "BadErr", - Inputs: abi.ParameterArray{{Type: "uint256"}, {Type: "uint256"}}, - } - customSel := customErr.FunctionSelectorBytes() +func TestProcessRevertReasonEmptyErrorAbis(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() - // Only 1 word of data but the error needs 2 — decode will fail - arg := make([]byte, 32) - binary.BigEndian.PutUint64(arg[24:], 1) - truncated := append(customSel, arg...) + revertData := ethtypes.MustNewHexBytes0xPrefix( + "0x08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000000000000c" + + "68656c6c6f20776f726c64210000000000000000000000000000000000000000") - s := "prefix:" + string(truncated) - result := unwrapNestedRevertReasons(ctx, s, 0, []*abi.Entry{customErr}) - // "prefix:" is clean ASCII, embedded section is hex-encoded - assert.True(t, strings.HasPrefix(result, "prefix:0x")) - assert.Contains(t, result, hex.EncodeToString(customSel)) + assert.Equal(t, "hello world!", processRevertReason(ctx, revertData, []*abi.Entry{})) } -func TestUnwrapPureBinaryNoSelector(t *testing.T) { - ctx := context.Background() +func TestProcessRevertReasonRealWorldNestedData(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() - s := string([]byte{0xde, 0xad, 0xbe, 0xef, 0x00, 0x00}) - result := unwrapNestedRevertReasons(ctx, s, 0, nil) - assert.Equal(t, "0xdeadbeef0000", result) -} + // Real revert data from the original bug report (the hex from logs). + // This contains deeply nested Error(string) chains from Solidity catch-and-rethrow + // with string(reason). The outer Error(string) declares string length 0x73=115 bytes, + // which captures the first-level prefix "[OCPE]404/98 - " plus the START of the + // inner Error(string) ABI encoding. The inner encoding declares length 0x212=530 but + // only ~32 bytes fit in the outer string, so the inner decode correctly fails and + // the remainder is hex-encoded. + // + // The critical requirement: the output must NOT contain null bytes (\x00) which + // was the root cause of the PostgreSQL "invalid byte sequence" bug. + revertData := ethtypes.MustNewHexBytes0xPrefix("0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000735b4f4350455d3430342f3938202d2008c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002125b544d4d5d3430342f3136653a2008c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001b45b4c544d4d525d3430342f3236713a2008c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001435b4b44574c5d3430342f313061202d205b4350485d3430342f5b3078616638333233336638626462323834333235386235653234663261326464636133356666323738625d3339613a205b4c4f43435d3430342f3137613a205b4350485d3430342f5b3078393638343634366535383033313539623061396338623163396538646237316361373062643236615d3239633a5b4c4f43535d3430342f32333a08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000555b44574c5d3430342f3737633a205b4b4841415d3430342f303161202d205b4350485d3430332f30343a2030786633363438306137643036356137366666623366366531633939613137313232353464656538353500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") -func TestUnwrapNilErrorAbis(t *testing.T) { - ctx := context.Background() + result := processRevertReason(ctx, revertData, nil) - innerABI := buildErrorStringABI([]byte("works with nil abis")) - s := "check:" + string(innerABI) - result := unwrapNestedRevertReasons(ctx, s, 0, nil) - assert.Equal(t, "check:works with nil abis", result) -} + // Critical: must not contain null bytes (the original bug) + assert.NotContains(t, result, "\x00") -func TestUnwrapEmptyErrorAbis(t *testing.T) { - ctx := context.Background() + // First level is decoded to readable text + assert.Contains(t, result, "[OCPE]404/98") - innerABI := buildErrorStringABI([]byte("works with empty abis")) - s := "check:" + string(innerABI) - result := unwrapNestedRevertReasons(ctx, s, 0, []*abi.Entry{}) - assert.Equal(t, "check:works with empty abis", result) + // The inner nested data (which the ABI decoder can't fully decode because + // the outer encoding truncates it) is hex-encoded as a safe fallback + assert.True(t, strings.Contains(result, "0x08c379a0")) } -func TestUnwrapNullBytesBetweenTextAndSelector(t *testing.T) { - ctx := context.Background() +func TestProcessRevertReasonCustomErrorWithMultipleParams(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() - innerABI := buildErrorStringABI([]byte("inner")) - s := "text\x00\x00\x00" + string(innerABI) - result := unwrapNestedRevertReasons(ctx, s, 0, nil) - // Prefix "text\x00\x00\x00" has non-printable bytes → entirely hex-encoded - assert.True(t, strings.HasPrefix(result, "0x")) - assert.True(t, strings.HasSuffix(result, "inner")) + customErr := &abi.Entry{ + Type: abi.Error, + Name: "DetailedError", + Inputs: abi.ParameterArray{ + {Name: "code", Type: "uint256"}, + {Name: "message", Type: "string"}, + }, + } + errData, err := customErr.EncodeCallDataJSON([]byte(`{"code":404,"message":"not found"}`)) + assert.NoError(t, err) + + result := processRevertReason(ctx, errData, []*abi.Entry{customErr}) + assert.Equal(t, `DetailedError("404","not found")`, result) } -func TestUnwrapNestedWithTrailingGarbage(t *testing.T) { - ctx := context.Background() +func TestProcessRevertReasonDefaultErrorTakesPriorityOverCustom(t *testing.T) { + ctx, _, _, done := newTestConnector(t) + defer done() - // Inner Error(string) followed by trailing null padding (simulates ABI padding from outer encoding) - innerABI := buildErrorStringABI([]byte("real message")) - s := "prefix:" + string(innerABI) + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - result := unwrapNestedRevertReasons(ctx, s, 0, nil) - assert.Equal(t, "prefix:real message", result) + // "default error msg" = 17 bytes, padded to 32. + // Total: 4 + 32 + 32 + 32 = 100. 100%32 = 4 ✓ + revertData := ethtypes.MustNewHexBytes0xPrefix( + "0x08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000011" + + "64656661756c74206572726f72206d7367000000000000000000000000000000") + + customErr := &abi.Entry{ + Type: abi.Error, + Name: "SomeOtherError", + Inputs: abi.ParameterArray{{Type: "uint256"}}, + } + result := processRevertReason(ctx, revertData, []*abi.Entry{customErr}) + assert.Equal(t, "default error msg", result) } func TestExecQueryFailBadToParams(t *testing.T) { diff --git a/internal/ethereum/get_receipt.go b/internal/ethereum/get_receipt.go index 1247a078..67abac55 100644 --- a/internal/ethereum/get_receipt.go +++ b/internal/ethereum/get_receipt.go @@ -17,7 +17,6 @@ package ethereum import ( - "bytes" "context" "encoding/hex" "encoding/json" @@ -132,17 +131,13 @@ func (c *ethConnector) getErrorInfo(ctx context.Context, transactionHash string, revertReason = revertFromReceipt.String() } - // See if the return value is using the default error you get from "revert" var errorMessage string returnDataBytes, _ := hex.DecodeString(padHexData(revertReason)) - if len(returnDataBytes) > 4 && bytes.Equal(returnDataBytes[0:4], defaultErrorID) { - value, err := defaultError.DecodeCallDataCtx(ctx, returnDataBytes) - if err == nil { - errorMessage = value.Children[0].Value.(string) - } + var errors abi.ABI + if decoded, ok := errors.UnwrapErrorStringCtx(ctx, returnDataBytes); ok { + errorMessage = decoded } - // Otherwise we can't decode it, so put it directly in the error if errorMessage == "" { if len(returnDataBytes) > 0 { errorMessage = i18n.NewError(ctx, msgs.MsgReturnValueNotDecoded, revertReason).Error() From 97830d09d4d9b6dc56f72a7e833e222c736642bf Mon Sep 17 00:00:00 2001 From: Dave Crighton Date: Thu, 16 Apr 2026 12:45:18 +0100 Subject: [PATCH 3/8] Expect type identifier in error string representation Signed-off-by: Dave Crighton --- internal/ethereum/exec_query_test.go | 36 +++++++++++++--------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/internal/ethereum/exec_query_test.go b/internal/ethereum/exec_query_test.go index e89d3a4f..57bdef58 100644 --- a/internal/ethereum/exec_query_test.go +++ b/internal/ethereum/exec_query_test.go @@ -188,7 +188,7 @@ func TestExecQueryCustomErrorRevertDataExceedsBalance(t *testing.T) { assert.NoError(t, err) _, reason, err := c.QueryInvoke(ctx, &req) assert.Equal(t, ffcapi.ErrorReasonTransactionReverted, reason) - expectedError := i18n.NewError(ctx, msgs.MsgReverted, `ERC20: transfer amount exceeds balance`) + expectedError := i18n.NewError(ctx, msgs.MsgReverted, `Error("ERC20: transfer amount exceeds balance")`) assert.Equal(t, expectedError.Error(), err.Error()) } @@ -209,7 +209,7 @@ func TestExecQueryCustomErrorRevertDataNotEnoughEther(t *testing.T) { assert.NoError(t, err) _, reason, err := c.QueryInvoke(ctx, &req) assert.Equal(t, ffcapi.ErrorReasonTransactionReverted, reason) - expectedError := i18n.NewError(ctx, msgs.MsgReverted, `Not enough Ether provided.`) + expectedError := i18n.NewError(ctx, msgs.MsgReverted, `Error("Not enough Ether provided.")`) assert.Equal(t, expectedError.Error(), err.Error()) } @@ -230,7 +230,7 @@ func TestExecQueryCustomErrorRevertDataTransferFromZeroAddress(t *testing.T) { assert.NoError(t, err) _, reason, err := c.QueryInvoke(ctx, &req) assert.Equal(t, ffcapi.ErrorReasonTransactionReverted, reason) - expectedError := i18n.NewError(ctx, msgs.MsgReverted, `ERC20: transfer from the zero address`) + expectedError := i18n.NewError(ctx, msgs.MsgReverted, `Error("ERC20: transfer from the zero address")`) assert.Equal(t, expectedError.Error(), err.Error()) } @@ -410,7 +410,7 @@ func TestProcessRevertReasonNestedErrorString(t *testing.T) { "000000000000000000000000000000000000000000") result := processRevertReason(ctx, revertData, nil) - assert.Equal(t, "outer: inner error message", result) + assert.Equal(t, `outer: Error("inner error message")`, result) } func TestProcessRevertReasonDoubleNestedErrorString(t *testing.T) { @@ -433,7 +433,7 @@ func TestProcessRevertReasonDoubleNestedErrorString(t *testing.T) { "64656570657374206572726f720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") result := processRevertReason(ctx, revertData, nil) - assert.Equal(t, "level1: level2: deepest error", result) + assert.Equal(t, `level1: level2: Error("deepest error")`, result) } func TestProcessRevertReasonNestedCustomError(t *testing.T) { @@ -462,10 +462,10 @@ func TestProcessRevertReasonNestedCustomError(t *testing.T) { "deadbeef00000000000000000000000000000000000000000000000000000000" + "00000000") - // With no error ABIs, the custom error can't be decoded — - // the entire nested section is hex-encoded + // With no error ABIs, the custom error can't be decoded — the outer Error(string) + // is formatted directly (binary content JSON-escaped inside the string). result := processRevertReason(ctx, revertData, nil) - assert.True(t, strings.HasPrefix(result, "0x")) + assert.True(t, strings.HasPrefix(result, `Error("[404]01d`)) assert.NotContains(t, result, "\x00") // Now provide the custom error ABI so it CAN be decoded @@ -485,9 +485,8 @@ func TestProcessRevertReasonUnknownNestedBinaryFallback(t *testing.T) { result := processRevertReason(ctx, revertData, nil) - // Entire nested section is hex-encoded since no selector could be decoded - assert.True(t, strings.HasPrefix(result, "0x")) - assert.Contains(t, result, "deadbeef") + // Unknown nested selector: outer Error(string) is still decoded; binary tail is JSON-escaped. + assert.True(t, strings.HasPrefix(result, `Error("[404]01d`)) assert.NotContains(t, result, "\x00") } @@ -503,7 +502,7 @@ func TestProcessRevertReasonPlainStringUnchanged(t *testing.T) { "4e6f7420656e6f7567682045746865722070726f76696465642e000000000000") result := processRevertReason(ctx, revertData, nil) - assert.Equal(t, "Not enough Ether provided.", result) + assert.Equal(t, `Error("Not enough Ether provided.")`, result) } // ---- processRevertReason behavioral tests ---- @@ -654,7 +653,7 @@ func TestProcessRevertReasonNilErrorAbis(t *testing.T) { "000000000000000000000000000000000000000000000000000000000000000c" + "68656c6c6f20776f726c64210000000000000000000000000000000000000000") - assert.Equal(t, "hello world!", processRevertReason(ctx, revertData, nil)) + assert.Equal(t, `Error("hello world!")`, processRevertReason(ctx, revertData, nil)) } func TestProcessRevertReasonEmptyErrorAbis(t *testing.T) { @@ -667,7 +666,7 @@ func TestProcessRevertReasonEmptyErrorAbis(t *testing.T) { "000000000000000000000000000000000000000000000000000000000000000c" + "68656c6c6f20776f726c64210000000000000000000000000000000000000000") - assert.Equal(t, "hello world!", processRevertReason(ctx, revertData, []*abi.Entry{})) + assert.Equal(t, `Error("hello world!")`, processRevertReason(ctx, revertData, []*abi.Entry{})) } func TestProcessRevertReasonRealWorldNestedData(t *testing.T) { @@ -680,7 +679,7 @@ func TestProcessRevertReasonRealWorldNestedData(t *testing.T) { // which captures the first-level prefix "[OCPE]404/98 - " plus the START of the // inner Error(string) ABI encoding. The inner encoding declares length 0x212=530 but // only ~32 bytes fit in the outer string, so the inner decode correctly fails and - // the remainder is hex-encoded. + // the remainder is represented as JSON-escaped bytes inside Error(string). // // The critical requirement: the output must NOT contain null bytes (\x00) which // was the root cause of the PostgreSQL "invalid byte sequence" bug. @@ -694,9 +693,8 @@ func TestProcessRevertReasonRealWorldNestedData(t *testing.T) { // First level is decoded to readable text assert.Contains(t, result, "[OCPE]404/98") - // The inner nested data (which the ABI decoder can't fully decode because - // the outer encoding truncates it) is hex-encoded as a safe fallback - assert.True(t, strings.Contains(result, "0x08c379a0")) + // Inner nested fragments appear inside the formatted Error(string) (binary JSON-escaped). + assert.Contains(t, result, "[TMM]404") } func TestProcessRevertReasonCustomErrorWithMultipleParams(t *testing.T) { @@ -736,7 +734,7 @@ func TestProcessRevertReasonDefaultErrorTakesPriorityOverCustom(t *testing.T) { Inputs: abi.ParameterArray{{Type: "uint256"}}, } result := processRevertReason(ctx, revertData, []*abi.Entry{customErr}) - assert.Equal(t, "default error msg", result) + assert.Equal(t, `Error("default error msg")`, result) } func TestExecQueryFailBadToParams(t *testing.T) { From 255f37fa031429e1e2b5d1ef4342ad49ea662127 Mon Sep 17 00:00:00 2001 From: Dave Crighton Date: Thu, 16 Apr 2026 12:31:01 +0100 Subject: [PATCH 4/8] go mod updates Signed-off-by: Dave Crighton Made-with: Cursor --- .gitignore | 6 +++++- go.mod | 6 ++++-- go.sum | 8 ++++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 1a0a2839..698b465f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,8 @@ __debug* .leveldb .vscode/*.log *.iml -.idea/ \ No newline at end of file +.idea/ + +# Local Go workspace (not part of the module release) +go.work +go.work.sum \ No newline at end of file diff --git a/go.mod b/go.mod index 11cf7e22..c5220142 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/hashicorp/golang-lru v1.0.2 github.com/hyperledger/firefly-common v1.5.9 - github.com/hyperledger/firefly-signer v1.1.21 + github.com/hyperledger/firefly-signer v1.1.23-0.20260414131220-8bfd443d115f github.com/hyperledger/firefly-transaction-manager v1.4.4 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 @@ -24,7 +24,7 @@ require ( github.com/Masterminds/squirrel v1.5.4 // indirect github.com/aidarkhanov/nanoid v1.0.8 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.5 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect @@ -99,3 +99,5 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/hyperledger/firefly-signer => github.com/davecrighton/firefly-signer v1.1.23-0.20260414131220-8bfd443d115f diff --git a/go.sum b/go.sum index f7696511..0b938b37 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,8 @@ github.com/aidarkhanov/nanoid v1.0.8 h1:yxyJkgsEDFXP7+97vc6JevMcjyb03Zw+/9fqhlVX github.com/aidarkhanov/nanoid v1.0.8/go.mod h1:vadfZHT+m4uDhttg0yY4wW3GKtl2T6i4d2Age+45pYk= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= -github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU= +github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -28,6 +28,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecrighton/firefly-signer v1.1.23-0.20260414131220-8bfd443d115f h1:EOkkhB8terNaEXWqY1IpGln75mnwPaJluqlijdYqZAQ= +github.com/davecrighton/firefly-signer v1.1.23-0.20260414131220-8bfd443d115f/go.mod h1:cb40Xkm/t2+KH+V1q3/zxZPohBNEA0iOA7mcr9wyfzI= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= @@ -102,8 +104,6 @@ github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/hyperledger/firefly-common v1.5.9 h1:Z1+SuKNYJ8hPKQ5CvcsMg6r/E4RyW6wb08nGwtcc8Ww= github.com/hyperledger/firefly-common v1.5.9/go.mod h1:1Xawm5PUhxT7k+CL/Kr3i1LE3cTTzoQwZMLimvlW8rs= -github.com/hyperledger/firefly-signer v1.1.21 h1:r7cTOw6e/6AtiXLf84wZy6Z7zppzlc191HokW2hv4N4= -github.com/hyperledger/firefly-signer v1.1.21/go.mod h1:axrlSQeKrd124UdHF5L3MkTjb5DeTcbJxJNCZ3JmcWM= github.com/hyperledger/firefly-transaction-manager v1.4.4 h1:cbG9FkQWriOcc1MMGaMqU7OpOwLloSV+PImOoaN0ckU= github.com/hyperledger/firefly-transaction-manager v1.4.4/go.mod h1:1kbYt8ofDXqfwC02vwV/HoOjmiv0IuT9UkJ//bbrliE= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= From 2d888ab5214de75ef722b30e520e5f43569caa6f Mon Sep 17 00:00:00 2001 From: Dave Crighton Date: Tue, 21 Apr 2026 11:35:17 +0100 Subject: [PATCH 5/8] Update as a result of feedback in firefly-signer PR #98 Signed-off-by: Dave Crighton --- go.mod | 4 ++-- go.sum | 4 ++-- internal/ethereum/exec_query.go | 2 +- internal/ethereum/get_receipt.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index c5220142..4c6ee164 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/hashicorp/golang-lru v1.0.2 github.com/hyperledger/firefly-common v1.5.9 - github.com/hyperledger/firefly-signer v1.1.23-0.20260414131220-8bfd443d115f + github.com/hyperledger/firefly-signer v1.1.23-0.20260421094944-fdf091eea4db github.com/hyperledger/firefly-transaction-manager v1.4.4 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 @@ -100,4 +100,4 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace github.com/hyperledger/firefly-signer => github.com/davecrighton/firefly-signer v1.1.23-0.20260414131220-8bfd443d115f +replace github.com/hyperledger/firefly-signer => github.com/davecrighton/firefly-signer v1.1.23-0.20260421094944-fdf091eea4db diff --git a/go.sum b/go.sum index 0b938b37..2dea921e 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecrighton/firefly-signer v1.1.23-0.20260414131220-8bfd443d115f h1:EOkkhB8terNaEXWqY1IpGln75mnwPaJluqlijdYqZAQ= -github.com/davecrighton/firefly-signer v1.1.23-0.20260414131220-8bfd443d115f/go.mod h1:cb40Xkm/t2+KH+V1q3/zxZPohBNEA0iOA7mcr9wyfzI= +github.com/davecrighton/firefly-signer v1.1.23-0.20260421094944-fdf091eea4db h1:/HglxFZkhRlJxpw4qg9quly+1fzPfIKDywH0FsbnXGE= +github.com/davecrighton/firefly-signer v1.1.23-0.20260421094944-fdf091eea4db/go.mod h1:cb40Xkm/t2+KH+V1q3/zxZPohBNEA0iOA7mcr9wyfzI= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= diff --git a/internal/ethereum/exec_query.go b/internal/ethereum/exec_query.go index 99943f72..9da6860d 100644 --- a/internal/ethereum/exec_query.go +++ b/internal/ethereum/exec_query.go @@ -151,7 +151,7 @@ func processRevertReason(ctx context.Context, outputData ethtypes.HexBytes0xPref for _, e := range errorAbis { errors = append(errors, e) } - if result, ok := errors.UnwrapErrorStringCtx(ctx, outputData); ok { + if result, ok := errors.ErrorStringCtx(ctx, outputData, abi.ErrorFormatOption{Unwrap: true}); ok { return result } log.L(ctx).Debugf("Directly returning revert reason: %s", outputData) diff --git a/internal/ethereum/get_receipt.go b/internal/ethereum/get_receipt.go index 67abac55..97b1c801 100644 --- a/internal/ethereum/get_receipt.go +++ b/internal/ethereum/get_receipt.go @@ -134,7 +134,7 @@ func (c *ethConnector) getErrorInfo(ctx context.Context, transactionHash string, var errorMessage string returnDataBytes, _ := hex.DecodeString(padHexData(revertReason)) var errors abi.ABI - if decoded, ok := errors.UnwrapErrorStringCtx(ctx, returnDataBytes); ok { + if decoded, ok := errors.ErrorStringCtx(ctx, returnDataBytes, abi.ErrorFormatOption{Unwrap: true}); ok { errorMessage = decoded } From 6deb6e0fc535c50d9eb425c8aed2a5808232777a Mon Sep 17 00:00:00 2001 From: Dave Crighton Date: Tue, 21 Apr 2026 14:31:03 +0100 Subject: [PATCH 6/8] update go mod Signed-off-by: Dave Crighton --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4c6ee164..7bf9bc5c 100644 --- a/go.mod +++ b/go.mod @@ -100,4 +100,4 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace github.com/hyperledger/firefly-signer => github.com/davecrighton/firefly-signer v1.1.23-0.20260421094944-fdf091eea4db +replace github.com/hyperledger/firefly-signer => github.com/davecrighton/firefly-signer v1.1.23-0.20260421131225-77652c2eeb43 diff --git a/go.sum b/go.sum index 2dea921e..c96dbb09 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecrighton/firefly-signer v1.1.23-0.20260421094944-fdf091eea4db h1:/HglxFZkhRlJxpw4qg9quly+1fzPfIKDywH0FsbnXGE= -github.com/davecrighton/firefly-signer v1.1.23-0.20260421094944-fdf091eea4db/go.mod h1:cb40Xkm/t2+KH+V1q3/zxZPohBNEA0iOA7mcr9wyfzI= +github.com/davecrighton/firefly-signer v1.1.23-0.20260421131225-77652c2eeb43 h1:mx5D/eyZtXm+TgECTGM3fbEDWb71t6A2WqlWlhoeUyQ= +github.com/davecrighton/firefly-signer v1.1.23-0.20260421131225-77652c2eeb43/go.mod h1:cb40Xkm/t2+KH+V1q3/zxZPohBNEA0iOA7mcr9wyfzI= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= From 437a796e7ed9aa832da7f44204c1c27e08c738fb Mon Sep 17 00:00:00 2001 From: Dave Crighton Date: Tue, 21 Apr 2026 16:53:57 +0100 Subject: [PATCH 7/8] Update to use new option name Signed-off-by: Dave Crighton --- internal/ethereum/exec_query.go | 2 +- internal/ethereum/get_receipt.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ethereum/exec_query.go b/internal/ethereum/exec_query.go index 9da6860d..e56ce6ed 100644 --- a/internal/ethereum/exec_query.go +++ b/internal/ethereum/exec_query.go @@ -151,7 +151,7 @@ func processRevertReason(ctx context.Context, outputData ethtypes.HexBytes0xPref for _, e := range errorAbis { errors = append(errors, e) } - if result, ok := errors.ErrorStringCtx(ctx, outputData, abi.ErrorFormatOption{Unwrap: true}); ok { + if result, ok := errors.ErrorStringCtx(ctx, outputData, abi.ErrorFormatOption{SearchForWrappedBinaryErrors: true}); ok { return result } log.L(ctx).Debugf("Directly returning revert reason: %s", outputData) diff --git a/internal/ethereum/get_receipt.go b/internal/ethereum/get_receipt.go index 97b1c801..fd812ad6 100644 --- a/internal/ethereum/get_receipt.go +++ b/internal/ethereum/get_receipt.go @@ -134,7 +134,7 @@ func (c *ethConnector) getErrorInfo(ctx context.Context, transactionHash string, var errorMessage string returnDataBytes, _ := hex.DecodeString(padHexData(revertReason)) var errors abi.ABI - if decoded, ok := errors.ErrorStringCtx(ctx, returnDataBytes, abi.ErrorFormatOption{Unwrap: true}); ok { + if decoded, ok := errors.ErrorStringCtx(ctx, returnDataBytes, abi.ErrorFormatOption{SearchForWrappedBinaryErrors: true}); ok { errorMessage = decoded } From 2343975edda503e0df738f9f84f1a87b14aefe9f Mon Sep 17 00:00:00 2001 From: Dave Crighton Date: Wed, 22 Apr 2026 09:22:02 +0100 Subject: [PATCH 8/8] Remove psuedoversion and update to point to real commit now ff-signer PR 98 is merged Signed-off-by: Dave Crighton --- go.mod | 4 +--- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 7bf9bc5c..c57bdcb3 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/hashicorp/golang-lru v1.0.2 github.com/hyperledger/firefly-common v1.5.9 - github.com/hyperledger/firefly-signer v1.1.23-0.20260421094944-fdf091eea4db + github.com/hyperledger/firefly-signer v1.1.23-0.20260422080826-42345c6c6b85 github.com/hyperledger/firefly-transaction-manager v1.4.4 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 @@ -99,5 +99,3 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace github.com/hyperledger/firefly-signer => github.com/davecrighton/firefly-signer v1.1.23-0.20260421131225-77652c2eeb43 diff --git a/go.sum b/go.sum index c96dbb09..9bba1581 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecrighton/firefly-signer v1.1.23-0.20260421131225-77652c2eeb43 h1:mx5D/eyZtXm+TgECTGM3fbEDWb71t6A2WqlWlhoeUyQ= -github.com/davecrighton/firefly-signer v1.1.23-0.20260421131225-77652c2eeb43/go.mod h1:cb40Xkm/t2+KH+V1q3/zxZPohBNEA0iOA7mcr9wyfzI= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= @@ -104,6 +102,8 @@ github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/hyperledger/firefly-common v1.5.9 h1:Z1+SuKNYJ8hPKQ5CvcsMg6r/E4RyW6wb08nGwtcc8Ww= github.com/hyperledger/firefly-common v1.5.9/go.mod h1:1Xawm5PUhxT7k+CL/Kr3i1LE3cTTzoQwZMLimvlW8rs= +github.com/hyperledger/firefly-signer v1.1.23-0.20260422080826-42345c6c6b85 h1:gh3YhxUYYwOfBCsEJXFmWO7SFzFrNuNulXftOam2JRI= +github.com/hyperledger/firefly-signer v1.1.23-0.20260422080826-42345c6c6b85/go.mod h1:cb40Xkm/t2+KH+V1q3/zxZPohBNEA0iOA7mcr9wyfzI= github.com/hyperledger/firefly-transaction-manager v1.4.4 h1:cbG9FkQWriOcc1MMGaMqU7OpOwLloSV+PImOoaN0ckU= github.com/hyperledger/firefly-transaction-manager v1.4.4/go.mod h1:1kbYt8ofDXqfwC02vwV/HoOjmiv0IuT9UkJ//bbrliE= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=