Skip to content

Commit 6a70c6b

Browse files
feat(eth): return consistent error for null rounds from RPC methods (#12655)
1 parent d388bdf commit 6a70c6b

File tree

5 files changed

+156
-14
lines changed

5 files changed

+156
-14
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# UNRELEASED
44

55
## New features
6+
- Return a consistent error when encountering null rounds in ETH RPC method calls. ([filecoin-project/lotus#12655](https://github.com/filecoin-project/lotus/pull/12655))
67
- Reduce size of embedded genesis CAR files by removing WASM actor blocks and compressing with zstd. This reduces the `lotus` binary size by approximately 10 MiB. ([filecoin-project/lotus#12439](https://github.com/filecoin-project/lotus/pull/12439))
78
- Add ChainSafe operated Calibration archival node to the bootstrap list ([filecoin-project/lotus#12517](https://github.com/filecoin-project/lotus/pull/12517))
89
- `lotus chain head` now supports a `--height` flag to print just the epoch number of the current chain head ([filecoin-project/lotus#12609](https://github.com/filecoin-project/lotus/pull/12609))

api/api_errors.go

+52
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package api
22

33
import (
44
"errors"
5+
"fmt"
56
"reflect"
67

78
"golang.org/x/xerrors"
89

910
"github.com/filecoin-project/go-jsonrpc"
11+
"github.com/filecoin-project/go-state-types/abi"
1012
)
1113

1214
var invalidExecutionRevertedMsg = xerrors.New("invalid execution reverted error")
@@ -22,6 +24,7 @@ const (
2224
EF3ParticipationTicketStartBeforeExisting
2325
EF3NotReady
2426
EExecutionReverted
27+
ENullRound
2528
)
2629

2730
var (
@@ -54,6 +57,8 @@ var (
5457
_ error = (*errF3NotReady)(nil)
5558
_ error = (*ErrExecutionReverted)(nil)
5659
_ jsonrpc.RPCErrorCodec = (*ErrExecutionReverted)(nil)
60+
_ error = (*ErrNullRound)(nil)
61+
_ jsonrpc.RPCErrorCodec = (*ErrNullRound)(nil)
5762
)
5863

5964
func init() {
@@ -67,6 +72,7 @@ func init() {
6772
RPCErrors.Register(EF3ParticipationTicketStartBeforeExisting, new(*errF3ParticipationTicketStartBeforeExisting))
6873
RPCErrors.Register(EF3NotReady, new(*errF3NotReady))
6974
RPCErrors.Register(EExecutionReverted, new(*ErrExecutionReverted))
75+
RPCErrors.Register(ENullRound, new(*ErrNullRound))
7076
}
7177

7278
func ErrorIsIn(err error, errorTypes []error) bool {
@@ -160,3 +166,49 @@ func NewErrExecutionReverted(reason string) *ErrExecutionReverted {
160166
Data: reason,
161167
}
162168
}
169+
170+
type ErrNullRound struct {
171+
Epoch abi.ChainEpoch
172+
Message string
173+
}
174+
175+
func NewErrNullRound(epoch abi.ChainEpoch) *ErrNullRound {
176+
return &ErrNullRound{
177+
Epoch: epoch,
178+
Message: fmt.Sprintf("requested epoch was a null round (%d)", epoch),
179+
}
180+
}
181+
182+
func (e *ErrNullRound) Error() string {
183+
return e.Message
184+
}
185+
186+
func (e *ErrNullRound) FromJSONRPCError(jerr jsonrpc.JSONRPCError) error {
187+
if jerr.Code != ENullRound {
188+
return fmt.Errorf("unexpected error code: %d", jerr.Code)
189+
}
190+
191+
epoch, ok := jerr.Data.(float64)
192+
if !ok {
193+
return fmt.Errorf("expected number data in null round error, got %T", jerr.Data)
194+
}
195+
196+
e.Epoch = abi.ChainEpoch(epoch)
197+
e.Message = jerr.Message
198+
return nil
199+
}
200+
201+
func (e *ErrNullRound) ToJSONRPCError() (jsonrpc.JSONRPCError, error) {
202+
return jsonrpc.JSONRPCError{
203+
Code: ENullRound,
204+
Message: e.Message,
205+
Data: e.Epoch,
206+
}, nil
207+
}
208+
209+
// Is performs a non-strict type check, we only care if the target is an ErrNullRound
210+
// and will ignore the contents (specifically there is no matching on Epoch).
211+
func (e *ErrNullRound) Is(target error) bool {
212+
_, ok := target.(*ErrNullRound)
213+
return ok
214+
}

itests/fevm_test.go

+94-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/stretchr/testify/require"
1616

1717
"github.com/filecoin-project/go-address"
18+
"github.com/filecoin-project/go-jsonrpc"
1819
"github.com/filecoin-project/go-state-types/abi"
1920
"github.com/filecoin-project/go-state-types/big"
2021
builtintypes "github.com/filecoin-project/go-state-types/builtin"
@@ -1546,7 +1547,7 @@ func TestEthGetTransactionByBlockHashAndIndexAndNumber(t *testing.T) {
15461547
// 2. Invalid block number
15471548
_, err = client.EthGetTransactionByBlockNumberAndIndex(ctx, (blockNumber + 1000).Hex(), ethtypes.EthUint64(0))
15481549
require.Error(t, err)
1549-
require.ErrorContains(t, err, "failed to get tipset")
1550+
require.ErrorContains(t, err, "requested a future epoch")
15501551

15511552
// 3. Index out of range
15521553
_, err = client.EthGetTransactionByBlockHashAndIndex(ctx, blockHash, ethtypes.EthUint64(100))
@@ -1659,3 +1660,95 @@ func TestEthEstimateGas(t *testing.T) {
16591660
})
16601661
}
16611662
}
1663+
1664+
func TestEthNullRoundHandling(t *testing.T) {
1665+
blockTime := 100 * time.Millisecond
1666+
client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC())
1667+
1668+
bms := ens.InterconnectAll().BeginMining(blockTime)
1669+
1670+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
1671+
defer cancel()
1672+
1673+
client.WaitTillChain(ctx, kit.HeightAtLeast(10))
1674+
1675+
bms[0].InjectNulls(10)
1676+
1677+
tctx, cancel := context.WithTimeout(ctx, 30*time.Second)
1678+
defer cancel()
1679+
ch, err := client.ChainNotify(tctx)
1680+
require.NoError(t, err)
1681+
<-ch
1682+
hc := <-ch
1683+
require.Equal(t, store.HCApply, hc[0].Type)
1684+
1685+
afterNullHeight := hc[0].Val.Height()
1686+
1687+
nullHeight := afterNullHeight - 1
1688+
for nullHeight > 0 {
1689+
ts, err := client.ChainGetTipSetByHeight(ctx, nullHeight, types.EmptyTSK)
1690+
require.NoError(t, err)
1691+
if ts.Height() == nullHeight {
1692+
nullHeight--
1693+
} else {
1694+
break
1695+
}
1696+
}
1697+
1698+
nullBlockHex := fmt.Sprintf("0x%x", int(nullHeight))
1699+
client.WaitTillChain(ctx, kit.HeightAtLeast(nullHeight+2))
1700+
testCases := []struct {
1701+
name string
1702+
testFunc func() error
1703+
}{
1704+
{
1705+
name: "EthGetBlockByNumber",
1706+
testFunc: func() error {
1707+
_, err := client.EthGetBlockByNumber(ctx, nullBlockHex, true)
1708+
return err
1709+
},
1710+
},
1711+
{
1712+
name: "EthFeeHistory",
1713+
testFunc: func() error {
1714+
_, err := client.EthFeeHistory(ctx, jsonrpc.RawParams([]byte(`[1,"`+nullBlockHex+`",[]]`)))
1715+
return err
1716+
},
1717+
},
1718+
{
1719+
name: "EthTraceBlock",
1720+
testFunc: func() error {
1721+
_, err := client.EthTraceBlock(ctx, nullBlockHex)
1722+
return err
1723+
},
1724+
},
1725+
{
1726+
name: "EthTraceReplayBlockTransactions",
1727+
testFunc: func() error {
1728+
_, err := client.EthTraceReplayBlockTransactions(ctx, nullBlockHex, []string{"trace"})
1729+
return err
1730+
},
1731+
},
1732+
}
1733+
1734+
for _, tc := range testCases {
1735+
t.Run(tc.name, func(t *testing.T) {
1736+
err := tc.testFunc()
1737+
if err == nil {
1738+
return
1739+
}
1740+
require.Error(t, err)
1741+
1742+
// Test errors.Is
1743+
require.ErrorIs(t, err, new(api.ErrNullRound), "error should be or wrap ErrNullRound")
1744+
1745+
// Test errors.As and verify message
1746+
var nullRoundErr *api.ErrNullRound
1747+
require.ErrorAs(t, err, &nullRoundErr, "error should be convertible to ErrNullRound")
1748+
1749+
expectedMsg := fmt.Sprintf("requested epoch was a null round (%d)", nullHeight)
1750+
require.Equal(t, expectedMsg, nullRoundErr.Error())
1751+
require.Equal(t, nullHeight, nullRoundErr.Epoch)
1752+
})
1753+
}
1754+
}

node/impl/full/eth.go

+8-12
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,6 @@ type EthAPI struct {
177177
EthEventAPI
178178
}
179179

180-
var ErrNullRound = errors.New("requested epoch was a null round")
181-
182180
func (a *EthModule) StateNetworkName(ctx context.Context) (dtypes.NetworkName, error) {
183181
return stmgr.GetNetworkName(ctx, a.StateManager, a.Chain.GetHeaviestTipSet().ParentState())
184182
}
@@ -243,10 +241,9 @@ func (a *EthAPI) FilecoinAddressToEthAddress(ctx context.Context, p jsonrpc.RawP
243241
blkParam = *params.BlkParam
244242
}
245243

246-
// Get the tipset for the specified block
247-
ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkParam, true)
244+
ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkParam, false)
248245
if err != nil {
249-
return ethtypes.EthAddress{}, xerrors.Errorf("failed to get tipset for block %s: %w", blkParam, err)
246+
return ethtypes.EthAddress{}, err
250247
}
251248

252249
// Lookup the ID address
@@ -571,7 +568,7 @@ func (a *EthAPI) EthGetTransactionByBlockHashAndIndex(ctx context.Context, blkHa
571568
func (a *EthAPI) EthGetTransactionByBlockNumberAndIndex(ctx context.Context, blkParam string, index ethtypes.EthUint64) (*ethtypes.EthTx, error) {
572569
ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkParam, true)
573570
if err != nil {
574-
return nil, xerrors.Errorf("failed to get tipset for block %s: %w", blkParam, err)
571+
return nil, err
575572
}
576573

577574
if ts == nil {
@@ -942,7 +939,7 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth
942939

943940
ts, err := getTipsetByBlockNumber(ctx, a.Chain, params.NewestBlkNum, false)
944941
if err != nil {
945-
return ethtypes.EthFeeHistory{}, fmt.Errorf("bad block parameter %s: %s", params.NewestBlkNum, err)
942+
return ethtypes.EthFeeHistory{}, err
946943
}
947944

948945
var (
@@ -1099,9 +1096,9 @@ func (a *EthModule) Web3ClientVersion(ctx context.Context) (string, error) {
10991096
}
11001097

11011098
func (a *EthModule) EthTraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.EthTraceBlock, error) {
1102-
ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkNum, false)
1099+
ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkNum, true)
11031100
if err != nil {
1104-
return nil, xerrors.Errorf("failed to get tipset: %w", err)
1101+
return nil, err
11051102
}
11061103

11071104
stRoot, trace, err := a.StateManager.ExecutionTrace(ctx, ts)
@@ -1170,10 +1167,9 @@ func (a *EthModule) EthTraceReplayBlockTransactions(ctx context.Context, blkNum
11701167
if len(traceTypes) != 1 || traceTypes[0] != "trace" {
11711168
return nil, fmt.Errorf("only 'trace' is supported")
11721169
}
1173-
1174-
ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkNum, false)
1170+
ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkNum, true)
11751171
if err != nil {
1176-
return nil, xerrors.Errorf("failed to get tipset: %w", err)
1172+
return nil, err
11771173
}
11781174

11791175
stRoot, trace, err := a.StateManager.ExecutionTrace(ctx, ts)

node/impl/full/eth_utils.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func getTipsetByBlockNumber(ctx context.Context, chain *store.ChainStore, blkPar
8888
return nil, fmt.Errorf("cannot get tipset at height: %v", num)
8989
}
9090
if strict && ts.Height() != abi.ChainEpoch(num) {
91-
return nil, ErrNullRound
91+
return nil, api.NewErrNullRound(abi.ChainEpoch(num))
9292
}
9393
return ts, nil
9494
}

0 commit comments

Comments
 (0)