go-ethereum/internal/ethapi/syncing_test.go
ozpool 20d6757391 internal/ethapi, eth: only gate eth_syncing when Engine API is registered
The previous version of this change unconditionally returned the progress
map until the consensus client had driven the node at least once. That
broke ethclient.TestEthClient/StatusFunctions and any other backend that
runs without a consensus client (in-process tests, --dev mode without
catalyst, light/legacy backends), where reporting "syncing" forever is
clearly wrong.

Split the gate into two flags:

  - clExpected: set in eth/catalyst.Register, the only entry point that
    attaches the Engine API to a node. If a backend never calls Register,
    it is not paired with a consensus client.
  - clContacted: set on every Engine API call (forkchoiceUpdated and
    newPayload), unchanged from before.

Replace ConsensusContacted on the Backend interface with ConsensusReady,
which folds the two flags into the question eth_syncing actually wants
answered: "is the synced claim meaningful right now?" Backends that
never expect a CL answer yes immediately, preserving legacy behavior.
Backends that do expect one answer yes only after the first FCU/NewPayload.

  - eth/backend.go: clExpected, clContacted, MarkConsensusExpected,
    MarkConsensusContacted, ConsensusReady on (*Ethereum)
  - eth/catalyst/api.go: backend.MarkConsensusExpected() in Register
  - eth/api_backend.go: ConsensusReady delegates to (*Ethereum)
  - internal/ethapi/backend.go: rename interface method to ConsensusReady
  - internal/ethapi/api.go: Syncing checks ConsensusReady
  - internal/ethapi/{api_test,transaction_args_test}.go: rename the test
    mock methods (default to true so existing tests are unaffected)
  - internal/ethapi/syncing_test.go: rename the helper field; tests now
    cover (a) CL-paired node before handshake -> truthy, (b) ready node
    -> false, (c) active sync -> progress map regardless of gate

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

97 lines
3.6 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
ready bool
}
func (b *syncingBackend) SyncProgress(_ context.Context) ethereum.SyncProgress { return b.progress }
func (b *syncingBackend) ConsensusReady() bool { return b.ready }
// TestSyncingReportsBeforeConsensusContact verifies that on a CL-paired node
// (ConsensusReady false), eth_syncing returns a truthy progress object even
// when the local downloader believes itself to be done. This is the bug fix
// 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())
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 (or the backend does not
// 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())
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
},
ready: 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)
}
}