From ef87651143cb02ec4fda1e06e383e5e0c84ca02c Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 16 Feb 2023 11:19:07 -0800 Subject: [PATCH 1/7] feat: eth: parse revert data 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 --- chain/types/ethtypes/eth_types.go | 33 +++++++++--- itests/contracts/Errors.hex | 1 + itests/contracts/Errors.sol | 24 +++++++++ itests/fevm_test.go | 27 ++++++++++ node/impl/full/eth.go | 90 ++++++++++++++++++++++++++++++- 5 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 itests/contracts/Errors.hex create mode 100644 itests/contracts/Errors.sol diff --git a/chain/types/ethtypes/eth_types.go b/chain/types/ethtypes/eth_types.go index 1539a638b60..006ffffdc0a 100644 --- a/chain/types/ethtypes/eth_types.go +++ b/chain/types/ethtypes/eth_types.go @@ -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 { diff --git a/itests/contracts/Errors.hex b/itests/contracts/Errors.hex new file mode 100644 index 00000000000..64e4ea6df60 --- /dev/null +++ b/itests/contracts/Errors.hex @@ -0,0 +1 @@ +608060405234801561001057600080fd5b506102de806100206000396000f3fe608060405234801561001057600080fd5b50600436106100575760003560e01c80630abe88b61461005c57806358d4cbce1461006657806359be8c55146100705780638791bd331461007a578063c6dbcf2e14610084575b600080fd5b61006461008e565b005b61006e61009f565b005b6100786100a4565b005b6100826100df565b005b61008c610111565b005b600061009d5761009c61012a565b5b565b600080fd5b6040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100d6906101b6565b60405180910390fd5b6040517f09caebf300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60006001905060008082610125919061023e565b505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fd5b600082825260208201905092915050565b7f6d7920726561736f6e0000000000000000000000000000000000000000000000600082015250565b60006101a0600983610159565b91506101ab8261016a565b602082019050919050565b600060208201905081810360008301526101cf81610193565b9050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610249826101d6565b9150610254836101d6565b925082610264576102636101e0565b5b600160000383147f80000000000000000000000000000000000000000000000000000000000000008314161561029d5761029c61020f565b5b82820590509291505056fea26469706673582212207815355e9e7ced2b8168a953c364e82871c0fe326602bbb9106e6551aea673ed64736f6c63430008120033 \ No newline at end of file diff --git a/itests/contracts/Errors.sol b/itests/contracts/Errors.sol new file mode 100644 index 00000000000..f9bcfbce2dd --- /dev/null +++ b/itests/contracts/Errors.sol @@ -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(); + } +} diff --git a/itests/fevm_test.go b/itests/fevm_test.go index 3018bf63ddd..6c79f27f487 100644 --- a/itests/fevm_test.go +++ b/itests/fevm_test.go @@ -868,3 +868,30 @@ 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) + for sig, expected := range map[string]string{ + "failRevertEmpty()": "none", + "failRevertReason()": "Error(my reason)", + "failAssert()": "Assert()", + "failDivZero()": "DivideByZero()", + "failCustom()": "0x09caebf3", + } { + 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)) + }) + } +} diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index d41a15c883c..1be546bc695 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -840,7 +840,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 } @@ -1001,7 +1002,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 { @@ -2180,6 +2181,91 @@ 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) + +// Parse an ABI encoded revert reason. This reason should be encoded as if it were the parameters to +// an `Error(string)` function call. +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()) + } + // Otherwise, switch. + switch code { + case 0x00: + return "Panic()" + case 0x01: + return "Assert()" + case 0x11: + return "ArithmaticOverflow()" + case 0x12: + return "DivideByZero()" + case 0x21: + return "InvalidEnumVariant()" + case 0x22: + return "InvalidStorageArray()" + case 0x31: + return "PopEmptyArray()" + case 0x32: + return "ArrayIndexOutOfBounds()" + case 0x41: + return "OutOfMemory()" + case 0x51: + return "CalledUninitializedFunction()" + default: + 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 { From 2c83264c0267df43a86b59ba808585de1fccf510 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 16 Feb 2023 13:17:09 -0800 Subject: [PATCH 2/7] more docs --- node/impl/full/eth.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 1be546bc695..cc2fbbf9f84 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -2186,6 +2186,8 @@ const panicFunctionSelector = "\x4e\x48\x7b\x71" // Panic(uint256) // 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" @@ -2211,7 +2213,6 @@ func parseEthRevert(ret []byte) string { codeInt := big.PositiveFromUnsignedBytes(cbytes) return fmt.Sprintf("Panic(%s)", ethtypes.EthBigInt(codeInt).String()) } - // Otherwise, switch. switch code { case 0x00: return "Panic()" From bc77c62e8243baaed44c6a039558ecf83e1838fd Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 16 Feb 2023 13:24:57 -0800 Subject: [PATCH 3/7] over-enthusiastic linter --- itests/fevm_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/itests/fevm_test.go b/itests/fevm_test.go index 6c79f27f487..579683303fa 100644 --- a/itests/fevm_test.go +++ b/itests/fevm_test.go @@ -885,6 +885,8 @@ func TestFEVMErrorParsing(t *testing.T) { "failDivZero()": "DivideByZero()", "failCustom()": "0x09caebf3", } { + sig := sig + expected := expected t.Run(sig, func(t *testing.T) { entryPoint := kit.CalcFuncSignature(sig) _, err := e.EthCall(ctx, ethtypes.EthCall{ From 6a003df8695bddccb7b3eab02f262c9c15c603a9 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 16 Feb 2023 16:48:47 -0800 Subject: [PATCH 4/7] fix spelling --- node/impl/full/eth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index cc2fbbf9f84..2480138877b 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -2219,7 +2219,7 @@ func parseEthRevert(ret []byte) string { case 0x01: return "Assert()" case 0x11: - return "ArithmaticOverflow()" + return "ArithmeticOverflow()" case 0x12: return "DivideByZero()" case 0x21: From 56bbe28ff998b9571042afc8bd30e244ec544d6e Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 16 Feb 2023 16:52:27 -0800 Subject: [PATCH 5/7] pull panic error codes into a table --- node/impl/full/eth.go | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 2480138877b..b3e2e777c40 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -2183,6 +2183,19 @@ func parseEthTopics(topics ethtypes.EthTopicSpec) (map[string][][]byte, error) { 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. @@ -2213,28 +2226,9 @@ func parseEthRevert(ret []byte) string { codeInt := big.PositiveFromUnsignedBytes(cbytes) return fmt.Sprintf("Panic(%s)", ethtypes.EthBigInt(codeInt).String()) } - switch code { - case 0x00: - return "Panic()" - case 0x01: - return "Assert()" - case 0x11: - return "ArithmeticOverflow()" - case 0x12: - return "DivideByZero()" - case 0x21: - return "InvalidEnumVariant()" - case 0x22: - return "InvalidStorageArray()" - case 0x31: - return "PopEmptyArray()" - case 0x32: - return "ArrayIndexOutOfBounds()" - case 0x41: - return "OutOfMemory()" - case 0x51: - return "CalledUninitializedFunction()" - default: + if s, ok := panicErrorCodes[uint64(code)]; ok { + return s + } else { return fmt.Sprintf("Panic(0x%x)", code) } case errorFunctionSelector: From 631219c148b7ad147ceefdbcc9dd7ede99172b77 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 16 Feb 2023 16:57:59 -0800 Subject: [PATCH 6/7] derive custom error selector --- itests/fevm_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/itests/fevm_test.go b/itests/fevm_test.go index 579683303fa..296334b37e6 100644 --- a/itests/fevm_test.go +++ b/itests/fevm_test.go @@ -878,12 +878,13 @@ func TestFEVMErrorParsing(t *testing.T) { _, 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()": "0x09caebf3", + "failCustom()": customError, } { sig := sig expected := expected From c218fc0a4edd674c20c66551f46e83aafdfc4ae4 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 16 Feb 2023 17:00:49 -0800 Subject: [PATCH 7/7] lint --- node/impl/full/eth.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index b3e2e777c40..6e964501c06 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -2228,9 +2228,8 @@ func parseEthRevert(ret []byte) string { } if s, ok := panicErrorCodes[uint64(code)]; ok { return s - } else { - return fmt.Sprintf("Panic(0x%x)", code) } + return fmt.Sprintf("Panic(0x%x)", code) case errorFunctionSelector: cbytes := cbytes[4:] cbytesLen := ethtypes.EthUint64(len(cbytes))