diff --git a/CHANGELOG.md b/CHANGELOG.md index f00ca73a7f1..d78831fd9b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - chore(deps): update of quic-go to v0.54.1 and go-libp2p to v0.43.0 ([filecoin-project/lotus#13361](https://github.com/filecoin-project/lotus/pull/13361)) - feat(spcli): add a `deposit-margin-factor` option to `lotus-miner actor new` and `lotus-shed miner create` so the sent deposit still covers the on-chain requirement if it rises between lookup and execution - feat(cli): lotus evm deploy prints message CID ([filecoin-project/lotus#13378](https://github.com/filecoin-project/lotus/pull/13378)) +- fix(eth): properly return vm error in all gas estimation methods ([filecoin-project/lotus#13389](https://github.com/filecoin-project/lotus/pull/13389)) # Node and Miner v1.34.1 / 2025-09-15 diff --git a/api/api_errors.go b/api/api_errors.go index 38371e09e34..d464933c8cb 100644 --- a/api/api_errors.go +++ b/api/api_errors.go @@ -1,6 +1,7 @@ package api import ( + "bytes" "errors" "fmt" "reflect" @@ -10,6 +11,8 @@ import ( "github.com/filecoin-project/go-jsonrpc" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/exitcode" + + "github.com/filecoin-project/lotus/chain/types/ethtypes" ) var invalidExecutionRevertedMsg = xerrors.New("invalid execution reverted error") @@ -173,6 +176,19 @@ func NewErrExecutionReverted(exitCode exitcode.ExitCode, error, reason string, d } } +// NewErrExecutionRevertedFromResult creates an ErrExecutionReverted from an InvocResult. +// It decodes the CBOR-encoded return data and parses any Ethereum revert reason. +func NewErrExecutionRevertedFromResult(res *InvocResult) error { + reason := "none" + var cbytes abi.CborBytes + if err := cbytes.UnmarshalCBOR(bytes.NewReader(res.MsgRct.Return)); err != nil { + reason = "ERROR: revert reason is not cbor encoded bytes" + } else if len(cbytes) > 0 { + reason = ethtypes.ParseEthRevert(cbytes) + } + return NewErrExecutionReverted(res.MsgRct.ExitCode, res.Error, reason, cbytes) +} + type ErrNullRound struct { Epoch abi.ChainEpoch Message string diff --git a/chain/types/ethtypes/eth_types.go b/chain/types/ethtypes/eth_types.go index 77998215978..4cf80e48bc1 100644 --- a/chain/types/ethtypes/eth_types.go +++ b/chain/types/ethtypes/eth_types.go @@ -1278,3 +1278,73 @@ type EthTxReceipt struct { Logs []EthLog `json:"logs"` Type EthUint64 `json:"type"` } + +const errorFunctionSelector = "\x08\xc3\x79\xa0" // Error(string) +const panicFunctionSelector = "\x4e\x48\x7b\x71" // Panic(uint256) + +// panicErrorCodes maps Solidity panic codes to human-readable descriptions. +var panicErrorCodes = map[uint64]string{ + 0x00: "Panic()", + 0x01: "Assert()", + 0x11: "ArithmeticOverflow()", + 0x12: "DivideByZero()", + 0x21: "InvalidEnumVariant()", + 0x22: "InvalidStorageArray()", + 0x31: "PopEmptyArray()", + 0x32: "ArrayIndexOutOfBounds()", + 0x41: "OutOfMemory()", + 0x51: "CalledUninitializedFunction()", +} + +// ParseEthRevert decodes an Ethereum ABI-encoded revert reason. +// This handles both Error(string) and Panic(uint256) revert types. +// +// See https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require +func ParseEthRevert(ret []byte) string { + // If it's not long enough to contain an ABI encoded response, return immediately. + if len(ret) < 4+32 { + return EthBytes(ret).String() + } + switch string(ret[:4]) { + case panicFunctionSelector: + ret := ret[4 : 4+32] + // Read and check the code. + code, err := EthUint64FromBytes(ret) + if err != nil { + // If it's too big, just return the raw value. + codeInt := big.PositiveFromUnsignedBytes(ret) + return fmt.Sprintf("Panic(%s)", EthBigInt(codeInt).String()) + } + if s, ok := panicErrorCodes[uint64(code)]; ok { + return s + } + return fmt.Sprintf("Panic(0x%x)", code) + case errorFunctionSelector: + ret := ret[4:] + retLen := EthUint64(len(ret)) + // Read and check the offset. + offset, err := EthUint64FromBytes(ret[:32]) + if err != nil { + break + } + if retLen < offset { + break + } + + // Read and check the length. + if retLen-offset < 32 { + break + } + start := offset + 32 + length, err := EthUint64FromBytes(ret[offset : offset+32]) + if err != nil { + break + } + if retLen-start < length { + break + } + // Slice the error message. + return fmt.Sprintf("Error(%s)", ret[start:start+length]) + } + return EthBytes(ret).String() +} diff --git a/node/impl/eth/gas.go b/node/impl/eth/gas.go index 67d0a0e9cee..a3f4fa3aeb0 100644 --- a/node/impl/eth/gas.go +++ b/node/impl/eth/gas.go @@ -289,16 +289,7 @@ func (e *ethGas) applyMessage(ctx context.Context, msg *types.Message, tsk types } if res.MsgRct.ExitCode.IsError() { - reason := "none" - var cbytes abi.CborBytes - if err := cbytes.UnmarshalCBOR(bytes.NewReader(res.MsgRct.Return)); err != nil { - log.Warnw("failed to unmarshal cbor bytes from message receipt return", "error", err) - reason = "ERROR: revert reason is not cbor encoded bytes" - } // else leave as empty bytes - if len(cbytes) > 0 { - reason = parseEthRevert(cbytes) - } - return nil, api.NewErrExecutionReverted(res.MsgRct.ExitCode, reason, res.Error, cbytes) + return nil, api.NewErrExecutionRevertedFromResult(res) } return res, nil @@ -337,7 +328,7 @@ func ethGasSearch( return ret, nil } - return -1, xerrors.Errorf("message execution failed: exit %s, reason: %s", res.MsgRct.ExitCode, res.Error) + return -1, api.NewErrExecutionRevertedFromResult(res) } func traceContainsExitCode(et types.ExecutionTrace, ex exitcode.ExitCode) bool { diff --git a/node/impl/eth/utils.go b/node/impl/eth/utils.go index c40575f003d..dbe154d3eeb 100644 --- a/node/impl/eth/utils.go +++ b/node/impl/eth/utils.go @@ -142,75 +142,6 @@ func executeTipset(ctx context.Context, ts *types.TipSet, cs ChainStore, sm Stat return stRoot, msgs, rcpts, 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{ - 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 it's not long enough to contain an ABI encoded response, return immediately. - if len(ret) < 4+32 { - return ethtypes.EthBytes(ret).String() - } - switch string(ret[:4]) { - case panicFunctionSelector: - ret := ret[4 : 4+32] - // Read the and check the code. - code, err := ethtypes.EthUint64FromBytes(ret) - if err != nil { - // If it's too big, just return the raw value. - codeInt := big.PositiveFromUnsignedBytes(ret) - 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: - ret := ret[4:] - retLen := ethtypes.EthUint64(len(ret)) - // Read the and check the offset. - offset, err := ethtypes.EthUint64FromBytes(ret[:32]) - if err != nil { - break - } - if retLen < offset { - break - } - - // Read and check the length. - if retLen-offset < 32 { - break - } - start := offset + 32 - length, err := ethtypes.EthUint64FromBytes(ret[offset : offset+32]) - if err != nil { - break - } - if retLen-start < length { - break - } - // Slice the error message. - return fmt.Sprintf("Error(%s)", ret[start:start+length]) - } - return ethtypes.EthBytes(ret).String() -} - // lookupEthAddress makes its best effort at finding the Ethereum address for a // Filecoin address. It does the following: // diff --git a/node/impl/gasutils/gasutils.go b/node/impl/gasutils/gasutils.go index f35903e22a6..107db7037eb 100644 --- a/node/impl/gasutils/gasutils.go +++ b/node/impl/gasutils/gasutils.go @@ -167,7 +167,7 @@ func GasEstimateGasLimit( } if res.MsgRct.ExitCode != exitcode.Ok { - return -1, xerrors.Errorf("message execution failed: exit %s, reason: %s", res.MsgRct.ExitCode, res.Error) + return -1, api.NewErrExecutionRevertedFromResult(res) } ret := res.MsgRct.GasUsed