feat: eth: parse revert data (#10295)

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
This commit is contained in:
Steven Allen 2023-02-16 17:18:03 -08:00 committed by GitHub
parent 5854d72784
commit 00b6d06041
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 163 additions and 9 deletions

View File

@ -66,6 +66,18 @@ func EthUint64FromHex(s string) (EthUint64, error) {
return EthUint64(parsedInt), nil 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 { func (e EthUint64) Hex() string {
if e == 0 { if e == 0 {
return "0x0" return "0x0"
@ -78,11 +90,15 @@ type EthBigInt big.Int
var EthBigIntZero = EthBigInt{Int: big.Zero().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 { 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 { func (e *EthBigInt) UnmarshalJSON(b []byte) error {
@ -106,12 +122,15 @@ func (e *EthBigInt) UnmarshalJSON(b []byte) error {
// EthBytes represent arbitrary bytes. A nil or empty slice serializes to "0x". // EthBytes represent arbitrary bytes. A nil or empty slice serializes to "0x".
type EthBytes []byte type EthBytes []byte
func (e EthBytes) MarshalJSON() ([]byte, error) { func (e EthBytes) String() string {
if len(e) == 0 { if len(e) == 0 {
return json.Marshal("0x") return "0x"
} }
s := hex.EncodeToString(e) return "0x" + hex.EncodeToString(e)
return json.Marshal("0x" + s) }
func (e EthBytes) MarshalJSON() ([]byte, error) {
return json.Marshal(e.String())
} }
func (e *EthBytes) UnmarshalJSON(b []byte) error { func (e *EthBytes) UnmarshalJSON(b []byte) error {

View File

@ -0,0 +1 @@
608060405234801561001057600080fd5b506102de806100206000396000f3fe608060405234801561001057600080fd5b50600436106100575760003560e01c80630abe88b61461005c57806358d4cbce1461006657806359be8c55146100705780638791bd331461007a578063c6dbcf2e14610084575b600080fd5b61006461008e565b005b61006e61009f565b005b6100786100a4565b005b6100826100df565b005b61008c610111565b005b600061009d5761009c61012a565b5b565b600080fd5b6040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100d6906101b6565b60405180910390fd5b6040517f09caebf300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60006001905060008082610125919061023e565b505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fd5b600082825260208201905092915050565b7f6d7920726561736f6e0000000000000000000000000000000000000000000000600082015250565b60006101a0600983610159565b91506101ab8261016a565b602082019050919050565b600060208201905081810360008301526101cf81610193565b9050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610249826101d6565b9150610254836101d6565b925082610264576102636101e0565b5b600160000383147f80000000000000000000000000000000000000000000000000000000000000008314161561029d5761029c61020f565b5b82820590509291505056fea26469706673582212207815355e9e7ced2b8168a953c364e82871c0fe326602bbb9106e6551aea673ed64736f6c63430008120033

View File

@ -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();
}
}

View File

@ -882,3 +882,33 @@ func TestFEVMGetBlockDifficulty(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, len(ret), 32) 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))
})
}
}

View File

@ -871,7 +871,8 @@ func (a *EthModule) applyMessage(ctx context.Context, msg *types.Message, tsk ty
return nil, xerrors.Errorf("CallWithGas failed: %w", err) return nil, xerrors.Errorf("CallWithGas failed: %w", err)
} }
if res.MsgRct.ExitCode.IsError() { 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 return res, nil
} }
@ -1032,7 +1033,7 @@ func (a *EthModule) EthCall(ctx context.Context, tx ethtypes.EthCall, blkParam s
invokeResult, err := a.applyMessage(ctx, msg, ts.Key()) invokeResult, err := a.applyMessage(ctx, msg, ts.Key())
if err != nil { if err != nil {
return nil, xerrors.Errorf("failed to apply message: %w", err) return nil, err
} }
if msg.To == builtintypes.EthereumAddressManagerActorAddr { if msg.To == builtintypes.EthereumAddressManagerActorAddr {
@ -2211,6 +2212,85 @@ func parseEthTopics(topics ethtypes.EthTopicSpec) (map[string][][]byte, error) {
return keys, nil 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) { func calculateRewardsAndGasUsed(rewardPercentiles []float64, txGasRewards gasRewardSorter) ([]ethtypes.EthBigInt, uint64) {
var totalGasUsed uint64 var totalGasUsed uint64
for _, tx := range txGasRewards { for _, tx := range txGasRewards {