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:
parent
5854d72784
commit
00b6d06041
@ -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"
|
||||
@ -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 {
|
||||
@ -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 {
|
||||
|
1
itests/contracts/Errors.hex
Normal file
1
itests/contracts/Errors.hex
Normal file
@ -0,0 +1 @@
|
||||
608060405234801561001057600080fd5b506102de806100206000396000f3fe608060405234801561001057600080fd5b50600436106100575760003560e01c80630abe88b61461005c57806358d4cbce1461006657806359be8c55146100705780638791bd331461007a578063c6dbcf2e14610084575b600080fd5b61006461008e565b005b61006e61009f565b005b6100786100a4565b005b6100826100df565b005b61008c610111565b005b600061009d5761009c61012a565b5b565b600080fd5b6040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100d6906101b6565b60405180910390fd5b6040517f09caebf300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60006001905060008082610125919061023e565b505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fd5b600082825260208201905092915050565b7f6d7920726561736f6e0000000000000000000000000000000000000000000000600082015250565b60006101a0600983610159565b91506101ab8261016a565b602082019050919050565b600060208201905081810360008301526101cf81610193565b9050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610249826101d6565b9150610254836101d6565b925082610264576102636101e0565b5b600160000383147f80000000000000000000000000000000000000000000000000000000000000008314161561029d5761029c61020f565b5b82820590509291505056fea26469706673582212207815355e9e7ced2b8168a953c364e82871c0fe326602bbb9106e6551aea673ed64736f6c63430008120033
|
24
itests/contracts/Errors.sol
Normal file
24
itests/contracts/Errors.sol
Normal 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();
|
||||
}
|
||||
}
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
@ -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 {
|
||||
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user