Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions eth/fetcher/block_fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ const (
blockLimit = 64 // Maximum number of unique blocks a peer may have delivered
)

// IsPlausibleAnnouncement reports whether a block announcement at the given
// number is within the fetcher's plausibility window of the current chain
// height. Untrusted announced numbers must be gated on this check before being
// recorded (e.g. as a peer's live tip), so a peer cannot inflate the recorded
// number beyond the window.
func IsPlausibleAnnouncement(number, height uint64) bool {
if number == 0 {
return true
}
if number <= height {
return height <= number+maxUncleDist
}
return number <= height+maxQueueDist
Comment thread
gzliudan marked this conversation as resolved.
}

var (
blockAnnounceInMeter = metrics.NewRegisteredMeter("eth/fetcher/block/announces/in", nil)
blockAnnounceOutTimer = metrics.NewRegisteredTimer("eth/fetcher/block/announces/out", nil)
Expand Down Expand Up @@ -365,12 +380,11 @@ func (f *BlockFetcher) loop() {
break
}
// If we have a valid block number, check that it's potentially useful
if notification.number > 0 {
if dist := int64(notification.number) - int64(f.chainHeight()); dist < -maxUncleDist || dist > maxQueueDist {
log.Debug("Peer discarded announcement", "peer", notification.origin, "number", notification.number, "hash", notification.hash, "distance", dist)
blockAnnounceDropMeter.Mark(1)
break
}
height := f.chainHeight()
if !IsPlausibleAnnouncement(notification.number, height) {
log.Debug("Peer discarded announcement", "peer", notification.origin, "number", notification.number, "hash", notification.hash, "height", height)
blockAnnounceDropMeter.Mark(1)
break
}
// All is well, schedule the announce if block's not yet downloading
if _, ok := f.fetching[notification.hash]; ok {
Expand Down Expand Up @@ -658,9 +672,11 @@ func (f *BlockFetcher) enqueue(peer string, block *types.Block) {
f.forgetHash(hash)
return
}
// Discard any past or too distant blocks
if dist := int64(block.NumberU64()) - int64(f.chainHeight()); dist < -maxUncleDist || dist > maxQueueDist {
log.Debug("Discarded propagated block, too far away", "peer", peer, "number", block.Number(), "hash", hash, "distance", dist)
// Discard any past or too distant blocks, using the same overflow-safe
// plausibility window as block announcements so a far-future number (e.g.
// MaxUint64) cannot wrap to a small distance and slip into the queue.
if !IsPlausibleAnnouncement(block.NumberU64(), f.chainHeight()) {
log.Debug("Discarded propagated block, too far away", "peer", peer, "number", block.Number(), "hash", hash)
blockBroadcastDropMeter.Mark(1)
f.forgetHash(hash)
return
Expand Down
50 changes: 50 additions & 0 deletions eth/fetcher/block_fetcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,56 @@ func TestDistantPropagationDiscarding(t *testing.T) {
}
}

// Tests that a far-future block whose number overflows int64 (e.g. MaxUint64)
// is discarded by enqueue, matching IsPlausibleAnnouncement, instead of being
// wrapped to a negative distance and accepted into the import queue.
func TestMaxUint64PropagationDiscarding(t *testing.T) {
// Build a low chain so the wrapped distance of a MaxUint64 block (-1) minus
// the head would fall inside the acceptance window under the old signed
// subtraction (height <= maxUncleDist).
hashes, blocks := makeChain(6, 0, genesis)
head := hashes[2] // block #4

tester := newTester()
tester.lock.Lock()
tester.hashes = []common.Hash{head}
tester.blocks = map[common.Hash]*types.Block{head: blocks[head]}
tester.lock.Unlock()

// A far-future block must be discarded rather than queued.
maxFuture := types.NewBlockWithHeader(&types.Header{
Number: new(big.Int).SetUint64(^uint64(0)),
Difficulty: big.NewInt(1),
})
tester.fetcher.Enqueue("maxfuture", maxFuture)
time.Sleep(10 * time.Millisecond)
if !tester.fetcher.queue.Empty() {
t.Fatalf("fetcher queued far-future block")
}
}

// Tests that IsPlausibleAnnouncement accepts announcements within the fetcher's
// plausibility window and rejects those too far from the local chain head.
func TestIsPlausibleAnnouncement(t *testing.T) {
height := uint64(1000)
tests := []struct {
number uint64
want bool
}{
{number: 0, want: true}, // Unknown height, always plausible
{number: height - maxUncleDist, want: true}, // Backward window edge
{number: height - maxUncleDist - 1, want: false}, // Too far behind
{number: height + maxQueueDist, want: true}, // Forward window edge
{number: height + maxQueueDist + 1, want: false}, // Too far ahead
{number: ^uint64(0), want: false}, // Far future
}
for i, tt := range tests {
if got := IsPlausibleAnnouncement(tt.number, height); got != tt.want {
t.Fatalf("case %d: number=%d got %v, want %v", i, tt.number, got, tt.want)
}
}
}

// Tests that announcements with numbers much lower or higher than out current
// head get discarded to prevent wasting resources on useless blocks from faulty
// peers.
Expand Down
18 changes: 16 additions & 2 deletions eth/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,9 +756,18 @@ func (pm *ProtocolManager) handleMsg(p *peer) error {
if err := msg.Decode(&announces); err != nil {
return errResp(ErrDecode, "%v: %v", msg, err)
}
// Mark the hashes as present at the remote node
// Mark the hashes as present at the remote node and track the highest
// plausible announced block as the peer's live tip.
height := pm.blockchain.CurrentBlock().Number.Uint64()
var tip uint64
for _, block := range announces {
p.MarkBlock(block.Hash)
if fetcher.IsPlausibleAnnouncement(block.Number, height) {
tip = max(tip, block.Number)
}
}
if tip > 0 {
p.SetTipNumber(tip)
}
// Schedule all the unknown hashes for retrieval
unknown := make(newBlockHashesData, 0, len(announces))
Expand Down Expand Up @@ -791,8 +800,13 @@ func (pm *ProtocolManager) handleMsg(p *peer) error {
request.Block.ReceivedAt = msg.ReceivedAt
request.Block.ReceivedFrom = p

// Mark the peer as owning the block and schedule it for import
// Mark the peer as owning the block, track its tip (only when the block
// number is within the fetcher's plausibility window), and schedule it
// for import
p.MarkBlock(request.Block.Hash())
if fetcher.IsPlausibleAnnouncement(request.Block.NumberU64(), pm.blockchain.CurrentBlock().Number.Uint64()) {
p.SetTipNumber(request.Block.NumberU64())
Comment thread
gzliudan marked this conversation as resolved.
}
pm.blockFetcher.Enqueue(p.id, request.Block)

// Assuming the block is importable by the peer, but possibly not yet done so,
Expand Down
37 changes: 37 additions & 0 deletions eth/peer.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ type peer struct {
td *big.Int
lock sync.RWMutex

tipNumber uint64 // Highest block number announced by the peer (live network high-water mark)

knownBlocks mapset.Set[common.Hash] // Set of block hashes known to be known by this peer
queuedBlocks chan *propEvent // Queue of blocks to broadcast to the peer
queuedBlockAnns chan *types.Block // Queue of blocks to announce to the peer
Expand Down Expand Up @@ -353,6 +355,25 @@ func (p *peer) SetHead(hash common.Hash, td *big.Int) {
p.td.Set(td)
}

// TipNumber retrieves the highest block number announced by the peer.
func (p *peer) TipNumber() uint64 {
p.lock.RLock()
defer p.lock.RUnlock()
return p.tipNumber
}

// SetTipNumber records the highest block number announced by the peer. The tip
// only ever moves forward; unlike SetHead (which conservatively tracks the
// parent of an announced block), this reflects the actual tip the peer has
// advertised, so it can serve as a live network high-water mark.
func (p *peer) SetTipNumber(number uint64) {
p.lock.Lock()
defer p.lock.Unlock()
if number > p.tipNumber {
p.tipNumber = number
}
}

// MarkBlock marks a block as known for the peer, ensuring that the block will
// never be propagated to this particular peer.
func (p *peer) MarkBlock(hash common.Hash) {
Expand Down Expand Up @@ -1051,6 +1072,22 @@ func (ps *peerSet) BestPeer() *peer {
return bestPeer
}

// HighestTipNumber returns the highest block number announced by any known
// peer, providing a live network high-water mark that stays current even when
// the downloader is idle.
func (ps *peerSet) HighestTipNumber() uint64 {
ps.lock.RLock()
defer ps.lock.RUnlock()

var highest uint64
for _, p := range ps.peers {
if tip := p.TipNumber(); tip > highest {
highest = tip
}
}
return highest
}

// Close disconnects all peers.
// No new peers can be registered after Close has returned.
func (ps *peerSet) Close() {
Expand Down
63 changes: 52 additions & 11 deletions eth/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,24 +205,65 @@ func (pm *ProtocolManager) syncStatusLogger() {
for {
select {
case <-ticker.C:
if pm.downloader.Synchronising() {
progress := pm.downloader.Progress()
log.Warn("Block synchronisation in progress",
"starting", progress.StartingBlock,
"current", progress.CurrentBlock,
"highest", progress.HighestBlock,
"pulledStates", progress.PulledStates,
"knownStates", progress.KnownStates,
"peers", pm.peers.Len(),
)
}
pm.reportSyncStatus()

case <-pm.quitSync:
return
}
}
}

// reportSyncStatus emits a warn-level periodic sync status line, so that the
// current sync state is always visible in the logs every cycle regardless of
// whether the node is catching up or already in sync.
func (pm *ProtocolManager) reportSyncStatus() {
var (
current uint64
highest uint64
)
// Seed current/highest from the downloader while it is actively
// synchronising (it knows the discovered sync target and, in fast sync,
// reports the snap block as the current height). Otherwise seed both from
// the local chain head. computeSyncStatus then folds the live per-peer
// announced-tip high-water mark into highest in both states, so the
// reported highest always reflects the freshest known chain tip.
if pm.downloader.Synchronising() {
progress := pm.downloader.Progress()
current, highest = progress.CurrentBlock, progress.HighestBlock
} else {
current = pm.blockchain.CurrentBlock().Number.Uint64()
}
status := computeSyncStatus(current, highest, pm.peers.HighestTipNumber())
log.Warn("Block synchronisation status",
"current", status.current,
"highest", status.highest,
"behind", status.behind,
"peers", pm.peers.Len(),
)
}

// syncStatus holds the values reported by the periodic sync status heartbeat.
type syncStatus struct {
current uint64 // Local head, or the fast-sync snap block while bulk syncing
highest uint64 // Highest known network block (downloader target + announced tips)
behind uint64 // Number of blocks behind the highest known network block
}

// computeSyncStatus derives the heartbeat values. It always folds the live
// network high-water mark (the highest block announced by any peer, bounded by
// IsPlausibleAnnouncement) into the reported highest, regardless of whether the
// downloader is bulk-syncing. The reported highest is therefore the maximum of
// the downloader target, the local head and the live announced tip in every
// state, so it reflects the freshest known chain tip.
func computeSyncStatus(current, highest, announcedTip uint64) syncStatus {
highest = max(current, max(highest, announcedTip))
behind := uint64(0)
if highest > current {
behind = highest - current
}
return syncStatus{current: current, highest: highest, behind: behind}
}

// synchronise tries to sync up our local block chain with a remote peer.
func (pm *ProtocolManager) synchronise(peer *peer) {
// Short circuit if no peers are available
Expand Down
Loading