Skip to content

Represent nested structure for revert errors when solidity catches and rethrows an error#98

Merged
EnriqueL8 merged 22 commits intohyperledger:mainfrom
davecrighton:djc/nestedErrors
Apr 22, 2026
Merged

Represent nested structure for revert errors when solidity catches and rethrows an error#98
EnriqueL8 merged 22 commits intohyperledger:mainfrom
davecrighton:djc/nestedErrors

Conversation

@davecrighton
Copy link
Copy Markdown
Contributor

@davecrighton davecrighton commented Feb 27, 2026

Proposed changes

This PR introduces a new type to represent an EVM RevertError, along with parsing and serialization support for revert payloads that contain nested errors. These show up when Solidity catches and re-throws to wrap the original revert with extra context, for example:

revert(string.concat("[404] caught bytes:", string(reason)));

See hyperledger/firefly#1717 for more on the scenario and how the related pull requests fit together.

The type exposes String() for a sensible default serialization, while still acting as a structured representation so callers can apply a more sophisticated serialization scheme if they want.

While parsing, we scan error data in 4-byte chunks and compare against known ABI selectors for error types. When a selector matches, existing parsing decodes the following bytes as an ABI-encoded error. Unwrapping stops at a maximum depth of 10.

If we cannot decode, or if any error string contains non-printable characters, output is hex-encoded instead—avoiding cases where non-printable bytes are passed through to callers that may persist them (for example, into a database).

For additional protection in the main Firefly repo against inserting null bytes from error messages into the database, see hyperledger/firefly#1716.


Types of changes

  • Bug fix
  • New feature added
  • Documentation Update

Please make sure to follow these points

  • I have read the contributing guidelines.
  • I have performed a self-review of my own code or work.
  • I have commented my code, particularly in hard-to-understand areas.
  • My changes generates no new warnings.
  • I have added tests that prove my fix is effective or that my feature works.
  • My changes have sufficient code coverage (unit, integration, e2e tests).

Screenshots (If Applicable)


Other Information

Any message for the reviewer or kick off the discussion by explaining why you considered this particular solution, any alternatives etc.

Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
@davecrighton davecrighton requested a review from a team as a code owner February 27, 2026 16:07
@davecrighton davecrighton changed the title Djc/nested errors Best effort for decoding nested errors in reverts Feb 27, 2026
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
…compatibility

Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
@davecrighton davecrighton changed the title Best effort for decoding nested errors in reverts Represent nested structure for revert errors when solidity catches and rethrows an error Apr 16, 2026
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
@davecrighton
Copy link
Copy Markdown
Contributor Author

davecrighton commented Apr 16, 2026

Some e2e examples of before / after serializations:

Before

--- invoke revertNestedWrapPanicAssert HTTP 500 ---
{
  "error": "FF10111: Error from ethereum connector: FF23021: EVM reverted: caught-panic:NH{q\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001"
}

--- invoke revertNestedSingleWrapPlain HTTP 500 ---
{
  "error": "FF10111: Error from ethereum connector: FF23021: EVM reverted: outer: \b�y�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 \u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0013plain revert string\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
}

--- invoke revertNestedTripleErrorString HTTP 500 ---
{
  "error": "FF10111: Error from ethereum connector: FF23021: EVM reverted: level1: \b�y�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 \u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000llevel2: \b�y�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 \u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\rdeepest error\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
}

--- invoke _nestedMid HTTP 500 ---
{
  "error": "FF10111: Error from ethereum connector: FF23021: EVM reverted: level2: \b�y�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 \u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\rdeepest error\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
}

--- invoke revertNestedWrapCustomUint256String HTTP 500 ---
{
  "error": "FF10111: Error from ethereum connector: FF23021: EVM reverted: [wrap] �%��\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000@\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnot found\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
}

After

--- invoke revertNestedWrapPanicAssert HTTP 500 ---
{
  "error": "FF10111: Error from ethereum connector: FF23021: EVM reverted: caught-panic:Panic(\"1\")"
}

--- invoke revertNestedSingleWrapPlain HTTP 500 ---
{
  "error": "FF10111: Error from ethereum connector: FF23021: EVM reverted: outer: Error(\"plain revert string\")"
}

--- invoke revertNestedTripleErrorString HTTP 500 ---
{
  "error": "FF10111: Error from ethereum connector: FF23021: EVM reverted: level1: level2: Error(\"deepest error\")"
}

--- invoke _nestedMid HTTP 500 ---
{
  "error": "FF10111: Error from ethereum connector: FF23021: EVM reverted: level2: Error(\"deepest error\")"
}

--- invoke revertNestedWrapCustomUint256String HTTP 500 ---
{
  "error": "FF10111: Error from ethereum connector: FF23021: EVM reverted: [wrap] MyCustomErrorUint256(\"404\",\"not found\")"
}

Associated solidity snippet:

 /// @notice Inner Panic(uint256) caught and re-thrown as Error(string) (decoder should unwrap Panic).
    function revertNestedWrapPanicAssert() external {
        try this.revertPanicAssert() {} catch (bytes memory reason) {
            revert(string.concat("caught-panic:", string(reason)));
        }
    }

    /// @notice Inner plain Error(string) wrapped once (two-level chain).
    function revertNestedSingleWrapPlain() external {
        try this.revertPlainString() {} catch (bytes memory reason) {
            revert(string.concat("outer: ", string(reason)));
        }
    }

    /// @notice Three-level Error(string) chain (see signer `TestUnwrapErrorStringDoubleNested` style).
    function revertNestedTripleErrorString() external {
        try this._nestedMid() {} catch (bytes memory r1) {
            revert(string.concat("level1: ", string(r1)));
        }
    }

    function _nestedMid() external {
        try this._nestedLeaf() {} catch (bytes memory r2) {
            revert(string.concat("level2: ", string(r2)));
        }
    }

    function _nestedLeaf() external pure {
        revert("deepest error");
    }

    /// @notice Inner is another custom error (multi-arg) to exercise non-bytes custom decoding.
    function revertNestedWrapCustomUint256String() external {
        try this.revertCustomUint256String() {} catch (bytes memory reason) {
            revert(string.concat("[wrap] ", string(reason)));
        }
    }

Copy link
Copy Markdown
Contributor

@peterbroadhurst peterbroadhurst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @davecrighton - this looks like a great piece of work.

I do think we need to go through my questions/comments in the PR and close out on some opinions about this pattern before we merge.

Comment thread pkg/abi/reverterror.go
// tried when decoding revert data, even if the caller's ABI is empty.
var defaultErrorEntries = ABI{
{Type: Error, Name: "Error", Inputs: ParameterArray{{Name: "reason", Type: "string"}}},
{Type: Error, Name: "Panic", Inputs: ParameterArray{{Name: "code", Type: "uint256"}}},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!
Found the detail on the difference and read up on hit here: https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require

Comment thread pkg/abi/abi.go Outdated
Comment thread pkg/abi/reverterror.go Outdated
Comment thread pkg/abi/reverterror.go Outdated
Comment thread pkg/abi/reverterror.go Outdated
Comment thread pkg/abi/reverterror.go Outdated
Comment thread pkg/abi/abi_test.go
Comment thread pkg/abi/reverterror.go Outdated
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
…an ABI after the matched selector

Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Comment thread pkg/abi/abi.go Outdated
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
Copy link
Copy Markdown
Contributor

@EnriqueL8 EnriqueL8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome @davecrighton , thanks for all the back and forth on this one

@EnriqueL8 EnriqueL8 merged commit 42345c6 into hyperledger:main Apr 22, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants