Skip to content

Commit 26d78d5

Browse files
authored
Add finalized state check as optional interfaces to avoid breaking shared code (#100)
* Add finalized state check as optional interfaces to avoid breaking shared code # Conflicts: # multinode/node_test.go * added mockery * Remove FinalizedStateCheckFailureThreshold from shared MultiNode config Move this field out of the shared MultiNode struct to avoid imposing it on chains that don't use finalized state checking (e.g. Solana). The EVM side provides this value through its own NodePool config, and the framework's optional FinalizedStateCheckConfig interface handles the type assertion in node_lifecycle.go. * multinode: cache finalized-state checker, tighten mocks/logs, drop base no-op, gate threshold on poll interval * lint fix
1 parent 3ed6f62 commit 26d78d5

11 files changed

Lines changed: 527 additions & 21 deletions

multinode/.mockery.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ packages:
2222
nodeMetrics:
2323
sendOnlyNodeMetrics:
2424
transactionSenderMetrics:
25+
FinalizedStateChecker:

multinode/mock_finalized_state_checker_test.go

Lines changed: 82 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

multinode/mock_node_metrics_test.go

Lines changed: 68 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package multinode
2+
3+
import (
4+
"context"
5+
6+
mock "github.com/stretchr/testify/mock"
7+
)
8+
9+
// mockRPCClientCheckFinalizedStateAvailabilityCall mirrors mockery-generated *Call types for
10+
// CheckFinalizedStateAvailability (added manually on mockRPCClient, not on RPCClient interface).
11+
// Named without underscores to satisfy revive var-naming (mockery-generated code uses underscores).
12+
type mockRPCClientCheckFinalizedStateAvailabilityCall[CHAIN_ID ID, HEAD Head] struct {
13+
*mock.Call
14+
}
15+
16+
// CheckFinalizedStateAvailability is a helper to define EXPECT().CheckFinalizedStateAvailability(...).
17+
func (_e *mockRPCClient_Expecter[CHAIN_ID, HEAD]) CheckFinalizedStateAvailability(ctx interface{}) *mockRPCClientCheckFinalizedStateAvailabilityCall[CHAIN_ID, HEAD] {
18+
return &mockRPCClientCheckFinalizedStateAvailabilityCall[CHAIN_ID, HEAD]{Call: _e.mock.On("CheckFinalizedStateAvailability", ctx)}
19+
}
20+
21+
func (_c *mockRPCClientCheckFinalizedStateAvailabilityCall[CHAIN_ID, HEAD]) Return(err error) *mockRPCClientCheckFinalizedStateAvailabilityCall[CHAIN_ID, HEAD] {
22+
_c.Call.Return(err)
23+
return _c
24+
}
25+
26+
// CheckFinalizedStateAvailability extends mockRPCClient to also satisfy FinalizedStateChecker,
27+
// so NewNode populates finalizedStateChecker for tests that exercise finalized-state checks.
28+
func (_m *mockRPCClient[CHAIN_ID, HEAD]) CheckFinalizedStateAvailability(ctx context.Context) error {
29+
ret := _m.Called(ctx)
30+
31+
if len(ret) == 0 {
32+
panic("no return value specified for CheckFinalizedStateAvailability")
33+
}
34+
35+
var r0 error
36+
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
37+
r0 = rf(ctx)
38+
} else {
39+
r0 = ret.Error(0)
40+
}
41+
42+
return r0
43+
}

multinode/node.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ type ChainConfig interface {
3838
FinalizedBlockOffset() uint32
3939
}
4040

41+
// FinalizedStateCheckConfig is an optional interface for enabling finalized state availability checking.
42+
// It is optional (not part of NodeConfig) so non-EVM multinode consumers are not forced to extend their config types; see aliveLoop in node_lifecycle.go.
43+
type FinalizedStateCheckConfig interface {
44+
FinalizedStateCheckFailureThreshold() uint32
45+
}
46+
47+
// FinalizedStateChecker is an optional interface for RPCClients that support finalized state checks.
48+
// It is optional (not part of RPCClient) so non-EVM multinode consumers avoid boilerplate and review churn; see aliveLoop in node_lifecycle.go.
49+
type FinalizedStateChecker interface {
50+
CheckFinalizedStateAvailability(ctx context.Context) error
51+
}
52+
4153
type nodeMetrics interface {
4254
IncrementNodeVerifies(ctx context.Context, nodeName string)
4355
IncrementNodeVerifiesFailed(ctx context.Context, nodeName string)
@@ -49,13 +61,15 @@ type nodeMetrics interface {
4961
IncrementNodeTransitionsToInvalidChainID(ctx context.Context, nodeName string)
5062
IncrementNodeTransitionsToUnusable(ctx context.Context, nodeName string)
5163
IncrementNodeTransitionsToSyncing(ctx context.Context, nodeName string)
64+
IncrementNodeTransitionsToFinalizedStateNotAvailable(ctx context.Context, nodeName string)
5265
RecordNodeClientVersion(ctx context.Context, nodeName string, version string)
5366
SetHighestSeenBlock(ctx context.Context, nodeName string, blockNumber int64)
5467
SetHighestFinalizedBlock(ctx context.Context, nodeName string, blockNumber int64)
5568
IncrementSeenBlocks(ctx context.Context, nodeName string)
5669
IncrementPolls(ctx context.Context, nodeName string)
5770
IncrementPollsFailed(ctx context.Context, nodeName string)
5871
IncrementPollsSuccess(ctx context.Context, nodeName string)
72+
IncrementFinalizedStateFailed(ctx context.Context, nodeName string)
5973
}
6074

6175
type Node[
@@ -106,8 +120,9 @@ type node[
106120
ws *url.URL
107121
http *url.URL
108122

109-
rpc RPC
110-
isLoadBalancedRPC bool
123+
rpc RPC
124+
finalizedStateChecker FinalizedStateChecker // set in NewNode when rpc implements FinalizedStateChecker
125+
isLoadBalancedRPC bool
111126

112127
stateMu sync.RWMutex // protects state* fields
113128
state nodeState
@@ -165,6 +180,9 @@ func NewNode[
165180
)
166181
n.lfcLog = logger.Named(lggr, "Lifecycle")
167182
n.rpc = rpc
183+
if fs, ok := any(rpc).(FinalizedStateChecker); ok {
184+
n.finalizedStateChecker = fs
185+
}
168186
n.isLoadBalancedRPC = isLoadBalancedRPC
169187
n.chainFamily = chainFamily
170188
return n
@@ -274,7 +292,7 @@ func (n *node[CHAIN_ID, HEAD, RPC]) verifyChainID(callerCtx context.Context, lgg
274292
// The node is already closed, and any subsequent transition is invalid.
275293
// To make spotting such transitions a bit easier, return the invalid node state.
276294
return nodeStateLen
277-
case nodeStateDialed, nodeStateOutOfSync, nodeStateInvalidChainID, nodeStateSyncing:
295+
case nodeStateDialed, nodeStateOutOfSync, nodeStateInvalidChainID, nodeStateSyncing, nodeStateFinalizedStateNotAvailable:
278296
default:
279297
panic(fmt.Sprintf("cannot verify node in state %v", st))
280298
}

multinode/node_fsm.go

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ func (n nodeState) String() string {
3535
return "Syncing"
3636
case nodeStateFinalizedBlockOutOfSync:
3737
return "FinalizedBlockOutOfSync"
38+
case nodeStateFinalizedStateNotAvailable:
39+
return "FinalizedStateNotAvailable"
3840
default:
3941
return fmt.Sprintf("nodeState(%d)", n)
4042
}
@@ -72,6 +74,8 @@ const (
7274
nodeStateSyncing
7375
// nodeStateFinalizedBlockOutOfSync - node is lagging behind on latest finalized block
7476
nodeStateFinalizedBlockOutOfSync
77+
// nodeStateFinalizedStateNotAvailable - node cannot serve historical state at finalized block
78+
nodeStateFinalizedStateNotAvailable
7579
// nodeStateLen tracks the number of states
7680
nodeStateLen
7781
)
@@ -182,7 +186,7 @@ func (n *node[CHAIN_ID, HEAD, RPC]) transitionToAlive(fn func()) {
182186
return
183187
}
184188
switch n.state {
185-
case nodeStateDialed, nodeStateInvalidChainID, nodeStateSyncing:
189+
case nodeStateDialed, nodeStateInvalidChainID, nodeStateSyncing, nodeStateFinalizedStateNotAvailable:
186190
n.state = nodeStateAlive
187191
default:
188192
panic(transitionFail(n.state, nodeStateAlive))
@@ -266,7 +270,7 @@ func (n *node[CHAIN_ID, HEAD, RPC]) transitionToUnreachable(fn func()) {
266270
return
267271
}
268272
switch n.state {
269-
case nodeStateUndialed, nodeStateDialed, nodeStateAlive, nodeStateOutOfSync, nodeStateInvalidChainID, nodeStateSyncing:
273+
case nodeStateUndialed, nodeStateDialed, nodeStateAlive, nodeStateOutOfSync, nodeStateInvalidChainID, nodeStateSyncing, nodeStateFinalizedStateNotAvailable:
270274
n.rpc.Close()
271275
n.state = nodeStateUnreachable
272276
default:
@@ -288,6 +292,8 @@ func (n *node[CHAIN_ID, HEAD, RPC]) declareState(state nodeState) {
288292
n.declareSyncing()
289293
case nodeStateAlive:
290294
n.declareAlive()
295+
case nodeStateFinalizedStateNotAvailable:
296+
n.declareFinalizedStateNotAvailable()
291297
default:
292298
panic(fmt.Sprintf("%#v state declaration is not implemented", state))
293299
}
@@ -311,7 +317,7 @@ func (n *node[CHAIN_ID, HEAD, RPC]) transitionToInvalidChainID(fn func()) {
311317
return
312318
}
313319
switch n.state {
314-
case nodeStateDialed, nodeStateOutOfSync, nodeStateSyncing:
320+
case nodeStateDialed, nodeStateOutOfSync, nodeStateSyncing, nodeStateFinalizedStateNotAvailable:
315321
n.rpc.Close()
316322
n.state = nodeStateInvalidChainID
317323
default:
@@ -338,7 +344,7 @@ func (n *node[CHAIN_ID, HEAD, RPC]) transitionToSyncing(fn func()) {
338344
return
339345
}
340346
switch n.state {
341-
case nodeStateDialed, nodeStateOutOfSync, nodeStateInvalidChainID:
347+
case nodeStateDialed, nodeStateOutOfSync, nodeStateInvalidChainID, nodeStateFinalizedStateNotAvailable:
342348
n.rpc.Close()
343349
n.state = nodeStateSyncing
344350
default:
@@ -351,6 +357,33 @@ func (n *node[CHAIN_ID, HEAD, RPC]) transitionToSyncing(fn func()) {
351357
fn()
352358
}
353359

360+
func (n *node[CHAIN_ID, HEAD, RPC]) declareFinalizedStateNotAvailable() {
361+
n.transitionToFinalizedStateNotAvailable(func() {
362+
n.lfcLog.Errorw("RPC Node cannot serve finalized state", "nodeState", n.state)
363+
n.wg.Add(1)
364+
go n.finalizedStateNotAvailableLoop()
365+
})
366+
}
367+
368+
func (n *node[CHAIN_ID, HEAD, RPC]) transitionToFinalizedStateNotAvailable(fn func()) {
369+
ctx, cancel := n.stopCh.NewCtx()
370+
defer cancel()
371+
n.metrics.IncrementNodeTransitionsToFinalizedStateNotAvailable(ctx, n.name)
372+
n.stateMu.Lock()
373+
defer n.stateMu.Unlock()
374+
if n.state == nodeStateClosed {
375+
return
376+
}
377+
switch n.state {
378+
case nodeStateAlive:
379+
n.rpc.Close()
380+
n.state = nodeStateFinalizedStateNotAvailable
381+
default:
382+
panic(transitionFail(n.state, nodeStateFinalizedStateNotAvailable))
383+
}
384+
fn()
385+
}
386+
354387
func transitionFail(from nodeState, to nodeState) string {
355388
return fmt.Sprintf("cannot transition from %#v to %#v", from, to)
356389
}

0 commit comments

Comments
 (0)