internal/ethapi, eth, eth/catalyst: address review feedback on eth_syncing CL gate

Per @MariusVanDerWijden's review feedback, tighten the change to match
geth's existing style:

- Drop the MarkConsensusExpected/MarkConsensusContacted/ConsensusReady
  doc paragraphs on Ethereum; collapse the field comments to single
  trailing lines matching eth/handler.go's atomic.Bool style.
- Rename the unexported accessors to MarkCLExpected/MarkCLContacted
  (catalyst can't reach the fields directly).
- Drop the multi-line comments at the catalyst call sites — the method
  names are self-describing.
- Trim the Backend.ConsensusReady() interface comment and EthAPIBackend
  wrapper comment.
- Replace the verbose docstring on EthereumAPI.Syncing with a single
  reference to #33687.
- Drop the long doc comments on the syncing_test.go cases; rename test
  functions to short forms (TestSyncingBeforeCLContact, etc.).

No behavioural change. Run: `go test ./internal/ethapi/ -count=1`.
This commit is contained in:
ozpool 2026-05-19 13:53:34 +05:30
parent 20d6757391
commit 21b153df43
6 changed files with 25 additions and 91 deletions

View file

@ -422,8 +422,6 @@ func (b *EthAPIBackend) SyncProgress(ctx context.Context) ethereum.SyncProgress
return prog return prog
} }
// ConsensusReady reports whether the node's "synced" claim is currently
// meaningful. See (*Ethereum).ConsensusReady for the gating rules.
func (b *EthAPIBackend) ConsensusReady() bool { func (b *EthAPIBackend) ConsensusReady() bool {
return b.eth.ConsensusReady() return b.eth.ConsensusReady()
} }

View file

@ -125,41 +125,20 @@ type Ethereum struct {
shutdownTracker *shutdowncheck.ShutdownTracker // Tracks if and when the node has shutdown ungracefully shutdownTracker *shutdowncheck.ShutdownTracker // Tracks if and when the node has shutdown ungracefully
// clExpected is set when the Engine API is registered on this node clExpected atomic.Bool // Set when catalyst.Register attaches the Engine API
// (catalyst.Register). When unset (no Engine API attached, e.g. in clContacted atomic.Bool // Set on first Engine API call (newPayload / FCU)
// tests, in --dev mode without catalyst, in light/legacy backends),
// the node cannot be paired with a consensus client and should not be
// gated on a CL handshake. clContacted records whether the consensus
// client has spoken to us via the Engine API at least once.
clExpected atomic.Bool
clContacted atomic.Bool
} }
// MarkConsensusExpected records that this node expects to be driven by a // MarkCLExpected and MarkCLContacted are setters for the two clXxx flags;
// consensus layer via the Engine API. It should be invoked once during node // catalyst calls them from its package and so cannot reach the fields directly.
// setup, when the Engine API is registered. func (s *Ethereum) MarkCLExpected() { s.clExpected.Store(true) }
func (s *Ethereum) MarkConsensusExpected() { func (s *Ethereum) MarkCLContacted() { s.clContacted.Store(true) }
s.clExpected.Store(true)
}
// MarkConsensusContacted records that the consensus layer has driven this node // ConsensusReady reports whether eth_syncing should be allowed to return false.
// at least once via the Engine API. The flag is sticky: once set, it stays set // On nodes without an Engine API, always true. On nodes that expect a CL, true
// for the lifetime of the process. // only after the CL has driven the node at least once.
func (s *Ethereum) MarkConsensusContacted() {
s.clContacted.Store(true)
}
// ConsensusReady reports whether the node's "synced" claim is meaningful right
// now. If the node does not expect a consensus client (no Engine API), the
// answer is always yes. If it does, the answer is yes only after the consensus
// client has driven the node at least once. eth_syncing uses this to avoid
// claiming "synced" before any CL handshake has occurred on freshly started
// post-merge nodes.
func (s *Ethereum) ConsensusReady() bool { func (s *Ethereum) ConsensusReady() bool {
if !s.clExpected.Load() { return !s.clExpected.Load() || s.clContacted.Load()
return true
}
return s.clContacted.Load()
} }
// New creates a new Ethereum object (including the initialisation of the common Ethereum object), // New creates a new Ethereum object (including the initialisation of the common Ethereum object),

View file

@ -49,11 +49,7 @@ import (
// Register adds the engine API and related APIs to the full node. // Register adds the engine API and related APIs to the full node.
func Register(stack *node.Node, backend *eth.Ethereum) error { func Register(stack *node.Node, backend *eth.Ethereum) error {
// Mark the backend as expecting a consensus client. eth_syncing uses backend.MarkCLExpected()
// this to gate "synced" responses on a CL handshake: a node configured
// to take direction from a CL should not advertise itself as synced
// until that CL has actually driven it at least once.
backend.MarkConsensusExpected()
stack.RegisterAPIs([]rpc.API{ stack.RegisterAPIs([]rpc.API{
newTestingAPI(backend), newTestingAPI(backend),
{ {
@ -256,9 +252,7 @@ func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.Fo
} }
// Stash away the last update to warn the user if the beacon client goes offline // Stash away the last update to warn the user if the beacon client goes offline
api.lastForkchoiceUpdate.Store(time.Now().Unix()) api.lastForkchoiceUpdate.Store(time.Now().Unix())
// Record that the consensus layer has driven us at least once. eth_syncing api.eth.MarkCLContacted()
// uses this to avoid claiming "synced" before any CL handshake has occurred.
api.eth.MarkConsensusContacted()
// Check whether we have the block yet in our database or not. If not, we'll // Check whether we have the block yet in our database or not. If not, we'll
// need to either trigger a sync, or to reject this forkchoice update for a // need to either trigger a sync, or to reject this forkchoice update for a
@ -854,9 +848,7 @@ func (api *ConsensusAPI) newPayload(ctx context.Context, params engine.Executabl
} }
// Stash away the last update to warn the user if the beacon client goes offline // Stash away the last update to warn the user if the beacon client goes offline
api.lastNewPayloadUpdate.Store(time.Now().Unix()) api.lastNewPayloadUpdate.Store(time.Now().Unix())
// Record that the consensus layer has driven us at least once. eth_syncing api.eth.MarkCLContacted()
// uses this to avoid claiming "synced" before any CL handshake has occurred.
api.eth.MarkConsensusContacted()
// If we already have the block locally, ignore the entire execution and just // If we already have the block locally, ignore the entire execution and just
// return a fake success. // return a fake success.

View file

@ -153,19 +153,12 @@ func (api *EthereumAPI) BlobBaseFee(ctx context.Context) *hexutil.Big {
// - highestBlock: block number of the highest block header this node has received from peers // - highestBlock: block number of the highest block header this node has received from peers
// - pulledStates: number of state entries processed until now // - pulledStates: number of state entries processed until now
// - knownStates: number of known state entries that still need to be pulled // - knownStates: number of known state entries that still need to be pulled
//
// On post-merge nodes paired with a consensus client, Syncing also waits for
// the consensus client to drive the node at least once before reporting
// "false". This avoids advertising a freshly started node as synced before any
// CL handshake has happened — see (*Ethereum).ConsensusReady. Backends without
// an Engine API (tests, --dev mode without catalyst, light clients) report
// ready immediately, preserving legacy behavior.
func (api *EthereumAPI) Syncing(ctx context.Context) (interface{}, error) { func (api *EthereumAPI) Syncing(ctx context.Context) (interface{}, error) {
progress := api.b.SyncProgress(ctx) progress := api.b.SyncProgress(ctx)
// Return not syncing if the synchronisation already completed AND, for // Don't claim "synced" until the CL has driven us at least once (post-merge
// backends that expect one, the consensus client has driven the node at // nodes with Engine API attached). Backends without a CL report ready
// least once. // immediately via ConsensusReady. Refs #33687.
if progress.Done() && api.b.ConsensusReady() { if progress.Done() && api.b.ConsensusReady() {
return false, nil return false, nil
} }

View file

@ -42,12 +42,6 @@ import (
type Backend interface { type Backend interface {
// General Ethereum API // General Ethereum API
SyncProgress(ctx context.Context) ethereum.SyncProgress SyncProgress(ctx context.Context) ethereum.SyncProgress
// ConsensusReady reports whether the node's "synced" claim is currently
// meaningful. For backends that do not expect a consensus client (no
// Engine API attached) the answer is always true. For backends that do,
// the answer flips to true only after the consensus client has driven
// the node at least once. eth_syncing uses this to avoid reporting
// "synced" before any CL handshake has happened.
ConsensusReady() bool ConsensusReady() bool
SuggestGasTipCap(ctx context.Context) (*big.Int, error) SuggestGasTipCap(ctx context.Context) (*big.Int, error)

View file

@ -23,9 +23,6 @@ import (
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
) )
// syncingBackend is a minimal Backend embedding that only implements the two
// methods Syncing calls. Embedding the interface avoids pulling in the full
// testBackend setup just to flip a single bool.
type syncingBackend struct { type syncingBackend struct {
Backend Backend
progress ethereum.SyncProgress progress ethereum.SyncProgress
@ -35,18 +32,9 @@ type syncingBackend struct {
func (b *syncingBackend) SyncProgress(_ context.Context) ethereum.SyncProgress { return b.progress } func (b *syncingBackend) SyncProgress(_ context.Context) ethereum.SyncProgress { return b.progress }
func (b *syncingBackend) ConsensusReady() bool { return b.ready } func (b *syncingBackend) ConsensusReady() bool { return b.ready }
// TestSyncingReportsBeforeConsensusContact verifies that on a CL-paired node // Issue #33687: a Done downloader but no CL handshake yet must report syncing.
// (ConsensusReady false), eth_syncing returns a truthy progress object even func TestSyncingBeforeCLContact(t *testing.T) {
// when the local downloader believes itself to be done. This is the bug fix api := NewEthereumAPI(&syncingBackend{progress: ethereum.SyncProgress{}, ready: false})
// for issue #33687: a freshly started node must not advertise itself as
// "synced" before the consensus client has actually driven it.
func TestSyncingReportsBeforeConsensusContact(t *testing.T) {
api := NewEthereumAPI(&syncingBackend{
// progress.Done() returns true on a zero-valued struct because all
// remaining counters are zero and CurrentBlock >= HighestBlock.
progress: ethereum.SyncProgress{},
ready: false,
})
res, err := api.Syncing(context.Background()) res, err := api.Syncing(context.Background())
if err != nil { if err != nil {
t.Fatalf("Syncing returned error: %v", err) t.Fatalf("Syncing returned error: %v", err)
@ -56,34 +44,24 @@ func TestSyncingReportsBeforeConsensusContact(t *testing.T) {
} }
} }
// TestSyncingReportsFalseAfterConsensusContact verifies that once the func TestSyncingAfterCLContact(t *testing.T) {
// consensus layer has handshaken at least once (or the backend does not api := NewEthereumAPI(&syncingBackend{progress: ethereum.SyncProgress{}, ready: true})
// expect one) and progress.Done() is true, eth_syncing reports false.
func TestSyncingReportsFalseAfterConsensusContact(t *testing.T) {
api := NewEthereumAPI(&syncingBackend{
progress: ethereum.SyncProgress{},
ready: true,
})
res, err := api.Syncing(context.Background()) res, err := api.Syncing(context.Background())
if err != nil { if err != nil {
t.Fatalf("Syncing returned error: %v", err) t.Fatalf("Syncing returned error: %v", err)
} }
v, ok := res.(bool) if v, ok := res.(bool); !ok || v {
if !ok || v {
t.Fatalf("expected false after CL handshake when sync is done, got %v", res) t.Fatalf("expected false after CL handshake when sync is done, got %v", res)
} }
} }
// TestSyncingReportsActiveSyncEvenWithoutConsensusContact verifies that when // Active sync stays truthy regardless of the CL gate.
// the downloader is actively syncing, eth_syncing returns the progress map func TestSyncingActiveSyncIgnoresCLGate(t *testing.T) {
// regardless of the CL gate. This preserves the legacy semantics for the case
// the issue thread did not affect.
func TestSyncingReportsActiveSyncEvenWithoutConsensusContact(t *testing.T) {
api := NewEthereumAPI(&syncingBackend{ api := NewEthereumAPI(&syncingBackend{
progress: ethereum.SyncProgress{ progress: ethereum.SyncProgress{
StartingBlock: 100, StartingBlock: 100,
CurrentBlock: 150, CurrentBlock: 150,
HighestBlock: 200, // CurrentBlock < HighestBlock => Done()=false HighestBlock: 200,
}, },
ready: false, ready: false,
}) })