go-ethereum/internal/ethapi/syncing_test.go
ozpool 238b160a67 internal/ethapi, eth, eth/catalyst: gate eth_syncing on CL handshake
eth_syncing currently returns false as soon as the local downloader
believes the chain to be done. On a freshly started node this happens
before the consensus client has talked to it: the persisted head loads
into memory, no CL handshake has occurred, the downloader sees nothing
to do, Progress.Done() is true, eth_syncing reports synced.

That is wrong from an operator perspective. Load balancers (HAProxy,
NGINX), L2 supervisors and multi-node setups commonly gate routing on
eth_syncing. They start sending live traffic to a node that has not
actually learned about any new head yet, which surfaces as missing
state, stale reads, and unhealthy upstreams.

Maintainer-endorsed direction in the issue thread: "default geth to
'syncing' on startup and only switch to 'synced' once we learn about
a new block".

Implement that with a sticky atomic.Bool on *Ethereum, set the first
time the consensus layer drives the node via the Engine API
(ForkchoiceUpdated or NewPayload), and consulted from eth_syncing.

  - eth/backend.go: add Ethereum.clContacted with
    MarkConsensusContacted/ConsensusContacted helpers
  - eth/catalyst/api.go: call MarkConsensusContacted at the same point
    where lastForkchoiceUpdate / lastNewPayloadUpdate are stamped, so
    the gate flips on every CL message regardless of the response
    status (handshake recorded even when we reply STATUS_SYNCING)
  - internal/ethapi/backend.go: add ConsensusContacted() to the Backend
    interface and to the two test mocks (api_test.go testBackend,
    transaction_args_test.go backendMock; both default to true so
    existing tests keep their original semantics)
  - eth/api_backend.go: implement ConsensusContacted on EthAPIBackend
  - internal/ethapi/api.go: in EthereumAPI.Syncing, only short-circuit
    to "false" when both progress.Done() AND ConsensusContacted() are
    true; otherwise return the progress map as during an active sync

Adds dedicated tests in internal/ethapi/syncing_test.go covering:
  - the new gate (Done but no CL contact -> truthy progress)
  - normal post-handshake behavior (Done + CL contact -> false)
  - active-sync behavior is unchanged regardless of the gate

Refs #33687.
2026-05-13 12:03:50 +05:30

96 lines
3.5 KiB
Go

// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package ethapi
import (
"context"
"testing"
"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 {
Backend
progress ethereum.SyncProgress
contacted bool
}
func (b *syncingBackend) SyncProgress(_ context.Context) ethereum.SyncProgress { return b.progress }
func (b *syncingBackend) ConsensusContacted() bool { return b.contacted }
// TestSyncingReportsBeforeConsensusContact verifies that eth_syncing returns a
// truthy progress object until the consensus layer has driven the node via the
// Engine API at least once, even when the local downloader believes itself to
// be done.
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{},
contacted: false,
})
res, err := api.Syncing(context.Background())
if err != nil {
t.Fatalf("Syncing returned error: %v", err)
}
if v, ok := res.(bool); ok && !v {
t.Fatal("expected truthy syncing payload before CL handshake, got false")
}
}
// TestSyncingReportsFalseAfterConsensusContact verifies that once the
// consensus layer has handshaken at least once and progress.Done() is true,
// eth_syncing reports false.
func TestSyncingReportsFalseAfterConsensusContact(t *testing.T) {
api := NewEthereumAPI(&syncingBackend{
progress: ethereum.SyncProgress{},
contacted: true,
})
res, err := api.Syncing(context.Background())
if err != nil {
t.Fatalf("Syncing returned error: %v", err)
}
v, ok := res.(bool)
if !ok || v {
t.Fatalf("expected false after CL handshake when sync is done, got %v", res)
}
}
// TestSyncingReportsActiveSyncEvenWithoutConsensusContact verifies that when
// the downloader is actively syncing, eth_syncing returns the progress map
// 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{
progress: ethereum.SyncProgress{
StartingBlock: 100,
CurrentBlock: 150,
HighestBlock: 200, // CurrentBlock < HighestBlock => Done()=false
},
contacted: false,
})
res, err := api.Syncing(context.Background())
if err != nil {
t.Fatalf("Syncing returned error: %v", err)
}
if _, ok := res.(map[string]interface{}); !ok {
t.Fatalf("expected progress map during active sync, got %T", res)
}
}