Skip to content

Commit

Permalink
feat: eth: parse revert data (#10295)
Browse files Browse the repository at this point in the history
We don't really want to do this in the FVM because it's Ethereum
specific, but this makes sense to do in the Ethereum API.

See:

See https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require
  • Loading branch information
Stebalien authored Feb 17, 2023
1 parent 5854d72 commit 00b6d06
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 9 deletions.
33 changes: 26 additions & 7 deletions chain/types/ethtypes/eth_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ func EthUint64FromHex(s string) (EthUint64, error) {
return EthUint64(parsedInt), nil
}

// Parse a uint64 from big-endian encoded bytes.
func EthUint64FromBytes(b []byte) (EthUint64, error) {
if len(b) != 32 {
return 0, xerrors.Errorf("eth int must be 32 bytes long")
}
var zeros [32 - 8]byte
if !bytes.Equal(b[:len(zeros)], zeros[:]) {
return 0, xerrors.Errorf("eth int overflows 64 bits")
}
return EthUint64(binary.BigEndian.Uint64(b[len(zeros):])), nil
}

func (e EthUint64) Hex() string {
if e == 0 {
return "0x0"
Expand All @@ -78,11 +90,15 @@ type EthBigInt big.Int

var EthBigIntZero = EthBigInt{Int: big.Zero().Int}

func (e EthBigInt) MarshalJSON() ([]byte, error) {
func (e EthBigInt) String() string {
if e.Int == nil || e.Int.BitLen() == 0 {
return json.Marshal("0x0")
return "0x0"
}
return json.Marshal(fmt.Sprintf("0x%x", e.Int))
return fmt.Sprintf("0x%x", e.Int)
}

func (e EthBigInt) MarshalJSON() ([]byte, error) {
return json.Marshal(e.String())
}

func (e *EthBigInt) UnmarshalJSON(b []byte) error {
Expand All @@ -106,12 +122,15 @@ func (e *EthBigInt) UnmarshalJSON(b []byte) error {
// EthBytes represent arbitrary bytes. A nil or empty slice serializes to "0x".
type EthBytes []byte

func (e EthBytes) MarshalJSON() ([]byte, error) {
func (e EthBytes) String() string {
if len(e) == 0 {
return json.Marshal("0x")
return "0x"
}
s := hex.EncodeToString(e)
return json.Marshal("0x" + s)
return "0x" + hex.EncodeToString(e)
}

func (e EthBytes) MarshalJSON() ([]byte, error) {
return json.Marshal(e.String())
}

func (e *EthBytes) UnmarshalJSON(b []byte) error {
Expand Down
1 change: 1 addition & 0 deletions itests/contracts/Errors.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
608060405234801561001057600080fd5b506102de806100206000396000f3fe608060405234801561001057600080fd5b50600436106100575760003560e01c80630abe88b61461005c57806358d4cbce1461006657806359be8c55146100705780638791bd331461007a578063c6dbcf2e14610084575b600080fd5b61006461008e565b005b61006e61009f565b005b6100786100a4565b005b6100826100df565b005b61008c610111565b005b600061009d5761009c61012a565b5b565b600080fd5b6040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100d6906101b6565b60405180910390fd5b6040517f09caebf300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60006001905060008082610125919061023e565b505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fd5b600082825260208201905092915050565b7f6d7920726561736f6e0000000000000000000000000000000000000000000000600082015250565b60006101a0600983610159565b91506101ab8261016a565b602082019050919050565b600060208201905081810360008301526101cf81610193565b9050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610249826101d6565b9150610254836101d6565b925082610264576102636101e0565b5b600160000383147f80000000000000000000000000000000000000000000000000000000000000008314161561029d5761029c61020f565b5b82820590509291505056fea26469706673582212207815355e9e7ced2b8168a953c364e82871c0fe326602bbb9106e6551aea673ed64736f6c63430008120033
24 changes: 24 additions & 0 deletions itests/contracts/Errors.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Errors {
error CustomError();

function failRevertEmpty() public {
revert();
}
function failRevertReason() public {
revert("my reason");
}
function failAssert() public {
assert(false);
}
function failDivZero() public {
int a = 1;
int b = 0;
a / b;
}
function failCustom() public {
revert CustomError();
}
}
30 changes: 30 additions & 0 deletions itests/fevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -882,3 +882,33 @@ func TestFEVMGetBlockDifficulty(t *testing.T) {
require.NoError(t, err)
require.Equal(t, len(ret), 32)
}

func TestFEVMErrorParsing(t *testing.T) {
ctx, cancel, client := kit.SetupFEVMTest(t)
defer cancel()

e := client.EVM()

_, contractAddr := e.DeployContractFromFilename(ctx, "contracts/Errors.hex")
contractAddrEth, err := ethtypes.EthAddressFromFilecoinAddress(contractAddr)
require.NoError(t, err)
customError := ethtypes.EthBytes(kit.CalcFuncSignature("CustomError()")).String()
for sig, expected := range map[string]string{
"failRevertEmpty()": "none",
"failRevertReason()": "Error(my reason)",
"failAssert()": "Assert()",
"failDivZero()": "DivideByZero()",
"failCustom()": customError,
} {
sig := sig
expected := expected
t.Run(sig, func(t *testing.T) {
entryPoint := kit.CalcFuncSignature(sig)
_, err := e.EthCall(ctx, ethtypes.EthCall{
To: &contractAddrEth,
Data: entryPoint,
}, "latest")
require.ErrorContains(t, err, fmt.Sprintf("exit 33, revert reason: %s, vm error", expected))
})
}
}
84 changes: 82 additions & 2 deletions node/impl/full/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,8 @@ func (a *EthModule) applyMessage(ctx context.Context, msg *types.Message, tsk ty
return nil, xerrors.Errorf("CallWithGas failed: %w", err)
}
if res.MsgRct.ExitCode.IsError() {
return nil, xerrors.Errorf("message execution failed: exit %s, msg receipt: %s, reason: %s", res.MsgRct.ExitCode, res.MsgRct.Return, res.Error)
reason := parseEthRevert(res.MsgRct.Return)
return nil, xerrors.Errorf("message execution failed: exit %s, revert reason: %s, vm error: %s", res.MsgRct.ExitCode, reason, res.Error)
}
return res, nil
}
Expand Down Expand Up @@ -1032,7 +1033,7 @@ func (a *EthModule) EthCall(ctx context.Context, tx ethtypes.EthCall, blkParam s

invokeResult, err := a.applyMessage(ctx, msg, ts.Key())
if err != nil {
return nil, xerrors.Errorf("failed to apply message: %w", err)
return nil, err
}

if msg.To == builtintypes.EthereumAddressManagerActorAddr {
Expand Down Expand Up @@ -2211,6 +2212,85 @@ func parseEthTopics(topics ethtypes.EthTopicSpec) (map[string][][]byte, error) {
return keys, nil
}

const errorFunctionSelector = "\x08\xc3\x79\xa0" // Error(string)
const panicFunctionSelector = "\x4e\x48\x7b\x71" // Panic(uint256)
// Eth ABI (solidity) panic codes.
var panicErrorCodes map[uint64]string = map[uint64]string{
0x00: "Panic()",
0x01: "Assert()",
0x11: "ArithmeticOverflow()",
0x12: "DivideByZero()",
0x21: "InvalidEnumVariant()",
0x22: "InvalidStorageArray()",
0x31: "PopEmptyArray()",
0x32: "ArrayIndexOutOfBounds()",
0x41: "OutOfMemory()",
0x51: "CalledUninitializedFunction()",
}

// Parse an ABI encoded revert reason. This reason should be encoded as if it were the parameters to
// an `Error(string)` function call.
//
// See https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require
func parseEthRevert(ret []byte) string {
if len(ret) == 0 {
return "none"
}
var cbytes abi.CborBytes
if err := cbytes.UnmarshalCBOR(bytes.NewReader(ret)); err != nil {
return "ERROR: revert reason is not cbor encoded bytes"
}
if len(cbytes) == 0 {
return "none"
}
// If it's not long enough to contain an ABI encoded response, return immediately.
if len(cbytes) < 4+32 {
return ethtypes.EthBytes(cbytes).String()
}
switch string(cbytes[:4]) {
case panicFunctionSelector:
cbytes := cbytes[4 : 4+32]
// Read the and check the code.
code, err := ethtypes.EthUint64FromBytes(cbytes)
if err != nil {
// If it's too big, just return the raw value.
codeInt := big.PositiveFromUnsignedBytes(cbytes)
return fmt.Sprintf("Panic(%s)", ethtypes.EthBigInt(codeInt).String())
}
if s, ok := panicErrorCodes[uint64(code)]; ok {
return s
}
return fmt.Sprintf("Panic(0x%x)", code)
case errorFunctionSelector:
cbytes := cbytes[4:]
cbytesLen := ethtypes.EthUint64(len(cbytes))
// Read the and check the offset.
offset, err := ethtypes.EthUint64FromBytes(cbytes[:32])
if err != nil {
break
}
if cbytesLen < offset {
break
}

// Read and check the length.
if cbytesLen-offset < 32 {
break
}
start := offset + 32
length, err := ethtypes.EthUint64FromBytes(cbytes[offset : offset+32])
if err != nil {
break
}
if cbytesLen-start < length {
break
}
// Slice the error message.
return fmt.Sprintf("Error(%s)", cbytes[start:start+length])
}
return ethtypes.EthBytes(cbytes).String()
}

func calculateRewardsAndGasUsed(rewardPercentiles []float64, txGasRewards gasRewardSorter) ([]ethtypes.EthBigInt, uint64) {
var totalGasUsed uint64
for _, tx := range txGasRewards {
Expand Down

0 comments on commit 00b6d06

Please sign in to comment.