// Copyright 2024 XDC Network
// XDC pre-merge sync implementation for go-ethereum compatibility

package downloader

import (
	"errors"
	"fmt"
	"math/big"
	"sync"
	"sync/atomic"
	"time"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core"
	"github.com/ethereum/go-ethereum/core/rawdb"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/eth/ethconfig"
	"github.com/ethereum/go-ethereum/log"
	"github.com/ethereum/go-ethereum/trie"
)

var (
	errUnknownPeer     = errors.New("peer is unknown or unhealthy")
	errEmptyHeaderSet  = errors.New("empty header set")
	errInvalidAncestor = errors.New("invalid ancestor")
	errPeersUnavailable = errors.New("no peers available for sync")
	errStallingPeer    = errors.New("peer is stalling")
	errTooOld          = errors.New("peer is too old")
	MaxForkAncestry    = uint64(3600 * 24 * 7 / 2) // ~1 week at 2s blocks
)

// StaticPeersXDC — list of persistent peers for XDC Apothem sync
// v99: Hardcoded trusted peers that maintain stable connections
var StaticPeersXDC = []string{
	"enode://7524db6718828c2c7663e6585a5b1e066457b8b0235034b69358b36e584fea776666d36ed4fc43d0f8bf2a5c3b2a960b5600689b6c8f0c207e5a76f8b0ca432d@157.173.120.219:30304",
	"enode://729d763db071595bacbbf33037a8e7639d8e9a97bfcfcda3afe963435d919cb95634f27375f0aadf6494dad47e506c888bf15cb5633d5f81dbb793b05b27e676@207.90.192.100:30304",
	"enode://1e205ffbd5e8f68df1b7d4f704c96b5e64118f8bb7b07a1207ee78b3b38ec25db0a0b1de940f81d138a3b51a880dadfab86eb4aed9b85c12230677428f2d8f32@80.243.180.123:30304",
	"enode://49c7586c221250cac7070df41c1b6c77180c5d9051e20d1d2b77dfa0dc80b8dc48a8e3c7ca068ac757429223530d6445a06a32ab4af20819cfaa1d47282a0401@80.243.180.121:30304",
	"enode://729d763db071595bacbbf33037a8e7639d8e9a97bfcfcda3afe963435d919cb95634f27375f0aadf6494dad47e506c888bf15cb5633d5f81dbb793b05b27e676@209.209.10.19:30304",
}

// XDCSyncEnabled indicates this build supports XDC sync
var XDCSyncEnabled atomic.Bool

func init() {
	XDCSyncEnabled.Store(true)
}

// Separate channels for ancestor (single header) vs batch header responses
// This fixes the race condition where batch responses were received by ancestor lookups
var xdcAncestorCh = make(chan xdcHeaderResponse, 128)  // v96: 64→128 — deeper pipeline for 2000 bl/s
var xdcBatchCh = make(chan xdcHeaderResponse, 256)     // v96: 128→256 — deeper multi-peer batch pipeline

type xdcHeaderResponse struct {
	peerId    string
	requestID uint64
	headers   []*types.Header
}

// Threshold to distinguish single vs batch responses
const xdcBatchThreshold = 2

// DeliverHeadersXDC delivers headers received from XDC peers (legacy format)
// Routes to appropriate channel based on response size to prevent race conditions
func (d *Downloader) DeliverHeadersXDC(peerId string, headers []*types.Header, reqId uint64) {
	resp := xdcHeaderResponse{peerId: peerId, requestID: reqId, headers: headers}
	
	// Route based on response size:
	// - Single headers (1) go to ancestor channel (for head fetch, binary search)
	// - Batch headers (>=2) go to batch channel (for bulk sync)
	// - Empty headers (0) go to BOTH channels — could be response to either a
	//   batch request or single request, and signals "no more headers" to whoever
	//   is waiting. This is critical: an empty response to a batch request was
	//   previously routed to xdcAncestorCh (since 0 < threshold), causing the
	//   10s timeout to fire instead of an immediate done-signal.
	if len(headers) == 0 {
		// Broadcast empty response to both channels so whoever is waiting gets it
		log.Debug("XDC header: empty response, broadcasting to both channels", "peer", peerId, "reqId", reqId)
		select {
		case xdcBatchCh <- resp:
		default:
		}
		select {
		case xdcAncestorCh <- resp:
		default:
		}
		return
	}
	if len(headers) < xdcBatchThreshold {
		select {
		case xdcAncestorCh <- resp:
			log.Trace("XDC header delivered to ancestor channel", "peer", peerId, "count", len(headers), "reqId", reqId)
		default:
			log.Warn("XDC ancestor header channel full", "peer", peerId, "count", len(headers), "reqId", reqId)
		}
	} else {
		select {
		case xdcBatchCh <- resp:
			log.Trace("XDC header delivered to batch channel", "peer", peerId, "count", len(headers), "reqId", reqId)
		default:
			log.Warn("XDC batch header channel full", "peer", peerId, "count", len(headers), "reqId", reqId)
		}
	}
}

// xdcBodyCh is used to receive bodies from the legacy (non-RequestId) handler
var xdcBodyCh = make(chan xdcBodyResponse, 512) // v96: 256→512 — deeper body pipeline for 2000 bl/s

type xdcBodyResponse struct {
	peerId    string
	requestID uint64
	txs       [][]*types.Transaction
	uncles    [][]*types.Header
}

// DeliverBodiesXDC delivers bodies received from XDC peers (legacy format)
func (d *Downloader) DeliverBodiesXDC(peerId string, txs [][]*types.Transaction, uncles [][]*types.Header, reqId uint64) {
	select {
	case xdcBodyCh <- xdcBodyResponse{peerId: peerId, requestID: reqId, txs: txs, uncles: uncles}:
	default:
		log.Warn("XDC body delivery channel full", "peer", peerId, "reqId", reqId)
	}
}

// SynchroniseXDC starts a sync with the given peer (XDC pre-merge style)
func (d *Downloader) SynchroniseXDC(id string, head common.Hash, td *big.Int, mode SyncMode) error {
	err := d.synchroniseXDC(id, head, td, mode)

	switch err {
	case nil, errBusy, errCanceled:
		return err
	}

	if errors.Is(err, errInvalidChain) || errors.Is(err, errBadPeer) || errors.Is(err, errInvalidAncestor) {
		// v7: Don't drop peers for chain validation errors.
		// "unknown ancestor" is commonly caused by stale responses on our
		// global header channel, not by the peer sending bad data.
		// Dropping peers here causes peer count to collapse, killing sync speed.
		log.Warn("XDC sync: chain validation error (NOT dropping peer)", "peer", id, "err", err)
		return err
	}
	if errors.Is(err, errTimeout) || errors.Is(err, errStallingPeer) || errors.Is(err, errEmptyHeaderSet) ||
		errors.Is(err, errPeersUnavailable) || errors.Is(err, errTooOld) {
		// Timeouts and stalls — don't drop peer, just retry later
		log.Warn("XDC sync failed (will retry)", "peer", id, "err", err)
		return err
	}
	log.Warn("XDC sync failed, retrying", "err", err)
	return err
}

// synchroniseXDC performs the actual sync with the given peer
func (d *Downloader) synchroniseXDC(id string, hash common.Hash, td *big.Int, mode SyncMode) error {
	// Make sure only one goroutine is ever allowed past this point at once
	if !d.synchronising.CompareAndSwap(false, true) {
		return errBusy
	}
	defer d.synchronising.Store(false)

	// Post a user notification of the sync (only once per session)
	if d.notified.CompareAndSwap(false, true) {
		log.Info("XDC block synchronisation started")
	}

	// XDC P0 optimization: Enable bulk sync mode for faster processing
	// This skips expensive signing tx ECDSA recovery and reduces state overhead
	core.XdcBulkSyncMode.Store(true)
	defer core.XdcBulkSyncMode.Store(false)

	// Get the peer BEFORE resetting (Reset clears the peer set)
	peer := d.peers.Peer(id)
	if peer == nil {
		return errUnknownPeer
	}

	// Reset the queue (but NOT peer state — we already have the peer reference)
	d.queue.Reset(blockCacheMaxItems, blockCacheInitialItems)

	// Drain channels
	for _, ch := range []chan bool{d.queue.blockWakeCh, d.queue.receiptWakeCh} {
		select {
		case <-ch:
		default:
		}
	}
	for empty := false; !empty; {
		select {
		case <-d.headerProcCh:
		default:
			empty = true
		}
	}
	// Drain XDC header channels (both ancestor and batch)
	for {
		select {
		case <-xdcAncestorCh:
		default:
			goto drainBatch
		}
	}
drainBatch:
	for {
		select {
		case <-xdcBatchCh:
		default:
			goto done
		}
	}
done:

	// Create cancel channel for aborting mid-flight
	d.cancelLock.Lock()
	d.cancelCh = make(chan struct{})
	d.cancelLock.Unlock()

	defer d.Cancel()

	// Set the sync mode
	d.mode.Store(uint32(mode))
	defer d.mode.Store(0)

	return d.syncWithPeerXDC(peer, hash, td)
}

// insertCheckpointAnchor fetches the configured cutoff block from the peer and
// installs it as a trusted anchor in the local chain. This lets sync skip the
// history before the checkpoint while still giving the first downloaded block
// (cutoff+1) a valid parent for ancestor checks. The fetched header is matched
// against the trusted checkpoint hash when one is configured.
func (d *Downloader) syncWithPeerXDC(p *peerConnection, hash common.Hash, td *big.Int) (err error) {
	d.mux.Post(StartEvent{})
	defer func() {
		if err != nil {
			d.mux.Post(FailedEvent{err})
		} else {
			latest := d.blockchain.CurrentHeader()
			d.mux.Post(DoneEvent{latest})
			// Refresh ancestor cache on successful sync so next peer skips binary search
			lastSyncAncestorMu.Lock()
			lastSyncAncestor = latest.Number.Uint64()
			lastSyncAncestorTime = time.Now()
			lastSyncAncestorMu.Unlock()

			// CRITICAL FIX: Clear checkpoint metadata on successful sync completion.
			// Without this, the node forces resume from checkpoint on every restart
			// even after full sync is complete, causing unnecessary re-download.
			if d.chainCutoffNumber != 0 && latest.Number.Uint64() > d.chainCutoffNumber+100000 {
				log.Info("XDC sync: clearing checkpoint metadata after full sync (100k+ past checkpoint)",
					"cutoff", d.chainCutoffNumber,
					"current", latest.Number.Uint64())
				rawdb.DeleteCheckpointSyncMetadata(d.stateDB)
				rawdb.DeleteLowestInsertedHeader(d.stateDB)
				// Also clear the in-memory cutoff so next sync starts fresh
				d.chainCutoffNumber = 0
				// Clear the checkpoint prune point so full history is available again
				d.blockchain.SetCheckpointPrunePoint(0, common.Hash{})
			}
		}
	}()
	mode := d.getMode()
	log.Info("XDC sync: synchronising with peer", "peer", p.id, "head", hash.Hex()[:16], "td", td, "mode", mode)

	defer func(start time.Time) {
		log.Debug("XDC sync: terminated", "elapsed", time.Since(start))
	}(time.Now())

	// Fetch the peer's head header using legacy format
	latest, err := d.fetchHeightXDC(p, hash)
	if err != nil {
		return err
	}
	height := latest.Number.Uint64()
	log.Info("XDC sync: remote head identified", "number", height, "hash", latest.Hash().Hex()[:16])

	// Find common ancestor — use cache to skip expensive binary search
	var origin uint64
	localHead := d.blockchain.CurrentBlock().Number.Uint64()

	// v8: ALWAYS use localHead as origin. Previous versions used findAncestorXDC()
	// which calls HasBlock() — but HasBlock() returns true for blocks with
	// headers+bodies stored (from previous incomplete sync) even when their
	// STATE is missing. This caused origin to be set far ahead of the actual
	// executable chain head, making InsertChain fail with "unknown ancestor"
	// on every batch.
	//
	// localHead = CurrentBlock().Number = last block with full state.
	// This is always the correct origin for full sync.
	// For snap sync, use CurrentSnapBlock so we resume from the existing snap state.
	if mode == ethconfig.SnapSync {
		snapHead := d.blockchain.CurrentSnapBlock()
		if snapHead != nil {
			origin = snapHead.Number.Uint64()
			log.Info("v8: using snap block as origin for snap sync", "origin", origin, "fullHead", localHead)
		} else {
			origin = localHead
			log.Info("v8: snap head missing, falling back to full block origin", "origin", origin)
		}
	} else {
		origin = localHead
		log.Info("v8: using chain head as origin (always correct for full sync)", "origin", origin)
	}

	// v114: If syncFromBlock is set and we're on a fresh chain (origin == 0),
	// pre-insert the checkpoint block as a trusted anchor, then ADVANCE origin
	// to the checkpoint. This lets us skip downloading all 56M headers from
	// genesis — we only need headers from the checkpoint onwards. The checkpoint
	// anchor provides the trusted parent chain for the first post-checkpoint block.
	// Bodies/receipts are also skipped before the cutoff via chainOffset.
	if d.chainCutoffNumber > 0 && origin == 0 {
		// v335 FIX: Try to insert checkpoint anchor, but if it fails (e.g. peer
		// can't serve old block), still advance origin to checkpoint-1. The
		// InsertHeadersBeforeCutoff during normal sync will validate checkpoint
		// headers against trusted checkpoints. We don't need the anchor in DB
		// to skip genesis headers — we just need to start fetching from checkpoint.
		if err := d.insertCheckpointAnchor(p); err != nil {
			log.Warn("XDC sync: checkpoint anchor insert failed, advancing origin anyway", "err", err, "checkpoint", d.chainCutoffNumber)
		} else {
			log.Info("XDC sync: checkpoint anchor installed",
				"checkpoint", d.chainCutoffNumber, "mode", mode)
			// P0 FIX: Checkpoint anchor was inserted without state. Enable
			// checkpointSyncNoState so that block import skips state validation
			// until state is built incrementally.
			if d.blockchain.SetCheckpointSyncNoState != nil {
				d.blockchain.SetCheckpointSyncNoState(true)
				log.Info("XDC sync: enabled checkpoint sync without state mode")
			}
		}
		// v335 FIX: Always advance origin to checkpoint-1 when on fresh chain.
		// The checkpoint header will be downloaded as part of normal sync and
		// validated against TrustedSyncCheckpoints in InsertHeadersBeforeCutoff.
		if d.chainCutoffNumber > 1 {
			origin = d.chainCutoffNumber - 1
			log.Info("XDC sync: advanced origin to checkpoint-1, skipping pre-checkpoint header download",
				"origin", origin, "checkpoint", d.chainCutoffNumber)
		}
	}

	// For XDPoS: on FIRST sync only, adjust origin to include the most recent checkpoint.
	// This ensures we have masternode lists for validating subsequent blocks.
	// Skip adjustment on resume (when origin == local head) to avoid re-downloading.
	// v7.2: Checkpoint adjustment DISABLED — it was causing "unknown ancestor" errors
	// because the checkpoint block often doesn't exist in our DB during fresh sync.
	// The adjustment pulled origin below the verified ancestor, then fetchHeaders
	// started from a block whose parent we don't have → ErrUnknownAncestor.
	// XDPoS validation is bypassed during sync anyway (state root bypass).
	log.Debug("XDC sync: using verified ancestor as origin (no checkpoint adjustment)", "origin", origin)

	// Update sync stats
	d.syncStatsLock.Lock()
	if d.syncStatsChainHeight <= origin || d.syncStatsChainOrigin > origin {
		d.syncStatsChainOrigin = origin
	}
	d.syncStatsChainHeight = height
	d.syncStatsLock.Unlock()

	// Calculate pivot for snap sync
	pivot := uint64(0)
	if mode == ethconfig.SnapSync {
		if height <= uint64(fsMinFullBlocks) {
			origin = 0
		} else {
			pivot = height - uint64(fsMinFullBlocks)
			if pivot <= origin {
				origin = pivot - 1
			}
		}
	}
	d.committed.Store(true)
	if mode == ethconfig.SnapSync && pivot != 0 {
		d.committed.Store(false)
	}

	// v8.1: Fetch pivot header for snap sync so processSnapSyncContent has it.
	// The standard downloader sets this in spawnSync after findAncestor, but
	// XDC's syncWithPeerXDC skips that path and goes straight to fetchers.
	if mode == ethconfig.SnapSync && pivot != 0 {
		pivotHeaders, err := d.requestHeadersByNumberXDC(p, pivot, 1, 0, false)
		if err != nil || len(pivotHeaders) == 0 {
			log.Warn("XDC sync: failed to fetch pivot header, falling back to full sync", "pivot", pivot, "err", err)
			mode = ethconfig.FullSync
		} else {
			d.pivotLock.Lock()
			d.pivotHeader = pivotHeaders[0]
			d.pivotLock.Unlock()
			log.Info("XDC sync: pivot header fetched", "pivot", pivot, "hash", pivotHeaders[0].Hash().Hex()[:16])
		}
	}

	// XDC: Apply chainCutoffNumber skip for both FullSync and SnapSync.
	// chainOffset is used for body/receipt download — only download content
	// from the cutoff onwards. Headers are ALWAYS downloaded from genesis.
	chainOffset := origin + 1
	if d.chainCutoffNumber != 0 && latest.Number.Uint64() > d.chainCutoffNumber+100000 {
		if chainOffset < d.chainCutoffNumber {
			chainOffset = d.chainCutoffNumber
			log.Info("Skip chain segment before cutoff", "origin", origin, "cutoff", d.chainCutoffNumber)
		}
	}

	// Prepare the queue. Result cache offset must equal the number of the FIRST
	// block that will be scheduled for body download — otherwise items[0] stays
	// nil forever and Results() blocks (deadlocking sync at the checkpoint).
	//
	// When --syncfromblock anchored a checkpoint and origin was advanced to
	// chainCutoffNumber-1 (see ~line 397), processHeaders will skip the checkpoint
	// block itself from body scheduling (see df21004d), so the first scheduled body
	// is checkpoint+1. Compensate the offset accordingly.
	prepareOffset := origin + 1
	if d.chainCutoffNumber != 0 && prepareOffset == d.chainCutoffNumber {
		prepareOffset = d.chainCutoffNumber + 1
	}
	d.queue.Prepare(prepareOffset, mode)
	// Headers are ALWAYS fetched from origin+1 to establish parent chain.
	// Only bodies/receipts are skipped before the cutoff.
	fetchers := []func() error{
		func() error { return d.fetchHeadersXDC(p, origin+1, pivot, height) },
		func() error { return d.fetchBodiesXDC(p, chainOffset) },
	}
	if mode == ethconfig.SnapSync {
		fetchers = append(fetchers, func() error { return d.fetchReceipts(chainOffset) })
	}
	fetchers = append(fetchers, func() error { return d.processHeaders(origin+1) })

	if mode == ethconfig.SnapSync {
		fetchers = append(fetchers, d.processSnapSyncContent)
	} else {
		fetchers = append(fetchers, d.processFullSyncContent)
	}

	return d.spawnSync(fetchers)
}

// fetchHeightXDC gets the header for the given hash from the peer using legacy format
func (d *Downloader) fetchHeightXDC(p *peerConnection, hash common.Hash) (*types.Header, error) {
	log.Debug("XDC sync: fetching head header (legacy)", "hash", hash.Hex()[:16])

	// Use legacy request (no RequestId wrapper)
	reqId := d.nextRequestID.Add(1)
	if err := p.peer.RequestHeadersByHashLegacy(hash, 1, 0, false, reqId); err != nil {
		return nil, fmt.Errorf("failed to request header: %w", err)
	}

	// Wait for response on the XDC header channel
	// v7: Reduced to 2s — peer should respond in <100ms, 2s is generous.
	timeout := time.NewTimer(2 * time.Second)
	defer timeout.Stop()

	for {
		select {
		case resp := <-xdcAncestorCh:
			if resp.peerId != p.id {
				// Response from different peer, put it back and continue
				select {
				case xdcAncestorCh <- resp:
				default:
				}
				continue
			}
			if resp.requestID != reqId {
				log.Warn("XDC sync: discarding stale head header response (request ID mismatch)", "expected", reqId, "got", resp.requestID, "peer", resp.peerId[:8])
				continue
			}
			if len(resp.headers) != 1 {
				return nil, fmt.Errorf("expected 1 header, got %d", len(resp.headers))
			}
			return resp.headers[0], nil

		case <-timeout.C:
			return nil, errTimeout

		case <-d.cancelCh:
			return nil, errCanceled
		}
	}
}

// drainHeaderChannels removes any stale responses from both header channels
func drainHeaderChannels() {
	// Drain ancestor channel
	for {
		select {
		case <-xdcAncestorCh:
			// Discard stale response
		default:
			goto drainBatch
		}
	}
drainBatch:
	// Drain batch channel
	for {
		select {
		case <-xdcBatchCh:
			// Discard stale response
		default:
			return
		}
	}
}

// requestHeadersByNumberXDC requests headers with timeout handling using legacy format
// Uses appropriate channel based on expected response size to avoid race conditions
func (d *Downloader) requestHeadersByNumberXDC(p *peerConnection, from uint64, count, skip int, reverse bool) ([]*types.Header, error) {
	return d.requestHeadersByNumberXDCWithTimeout(p, from, count, skip, reverse, 0)
}

// requestHeadersByNumberXDCWithTimeout is like requestHeadersByNumberXDC but
// allows the caller to specify the per-request timeout. Pass 0 to use the
// default (15s for V1, 30s for V2). Used by the parallel header dispatcher
// (XDC #546) to set a short timeout (~4s) so a single slow peer doesn't gate
// the synchronous collect loop in fetchHeadersXDC.
func (d *Downloader) requestHeadersByNumberXDCWithTimeout(p *peerConnection, from uint64, count, skip int, reverse bool, customTimeout time.Duration) ([]*types.Header, error) {
	// Determine which channel to use based on request count
	isSingleRequest := count < xdcBatchThreshold
	var headerCh chan xdcHeaderResponse
	if isSingleRequest {
		headerCh = xdcAncestorCh
	} else {
		headerCh = xdcBatchCh
	}
	
	// Drain any stale responses before making new request
	drainHeaderChannels()
	
	// Issue #314: atomically increment request ID and include it in request metadata
	reqId := d.nextRequestID.Add(1)
	
	// Use legacy request (no RequestId wrapper on wire, but tracked locally)
	if err := p.peer.RequestHeadersByNumberLegacy(from, count, skip, reverse, reqId); err != nil {
		return nil, fmt.Errorf("failed to request headers: %w", err)
	}

	// v7: Reduced to 2s for faster failover to next peer.
	// v142: Use 30s timeout for V2 blocks where consensus validation is heavy
	timeoutDuration := 15 * time.Second
	chainConfig := d.blockchain.Config()
	if chainConfig != nil && chainConfig.XDPoS != nil && chainConfig.XDPoS.V2 != nil &&
		chainConfig.XDPoS.V2.SwitchBlock != nil {
		switchBlock := chainConfig.XDPoS.V2.SwitchBlock.Uint64()
		if from >= switchBlock {
			timeoutDuration = 30 * time.Second
			log.Trace("[requestHeadersByNumberXDC] V2 block detected, using extended timeout", "from", from, "timeout", timeoutDuration)
		}
	}
	// XDC #546: caller-provided timeout overrides the default. The parallel
	// header dispatcher uses ~4s so stragglers fail fast rather than gating
	// the synchronous collect loop.
	if customTimeout > 0 {
		timeoutDuration = customTimeout
	}
	timeout := time.NewTimer(timeoutDuration)
	defer timeout.Stop()
	for {
		select {
		case resp := <-headerCh:
			if resp.peerId != p.id {
				// Response from different peer, put it back
			select {
				case headerCh <- resp:
				default:
				}
				continue
			}
			// Issue #314: discard stale responses from previous sync cycles
			if resp.requestID != reqId {
				log.Warn("XDC sync: discarding stale header response (request ID mismatch)", "expected", reqId, "got", resp.requestID, "peer", resp.peerId[:8], "count", len(resp.headers))
				continue
			}
			// Empty response means peer has no headers at/after `from` — treat as end-of-chain
			// (eliminates 10s timeout when we're caught up to the peer's head)
			if len(resp.headers) == 0 {
				log.Debug("XDC sync: empty header response — will retry with smaller batch", "from", from, "requested", count)
				return nil, errEmptyHeaderSet
			}
			// v7: ALWAYS validate firstNum matches requested range.
			// Stale responses from previous sync cycles on the global channel
			// caused "unknown ancestor" errors when accepted blindly.
			firstNum := resp.headers[0].Number.Uint64()
			if firstNum != from {
				// Response doesn't match our request — stale from previous cycle
				log.Debug("XDC sync: discarding stale header response", "expected", from, "got", firstNum, "peer", resp.peerId[:8])
				continue
			}
			return resp.headers, nil

		case <-timeout.C:
			return nil, errTimeout

		case <-d.cancelCh:
			return nil, errCanceled
		}
	}
}

// findAncestorXDC finds common ancestor using span search then binary search
func (d *Downloader) findAncestorXDC(p *peerConnection, remoteHeader *types.Header) (uint64, error) {
	var (
		floor        = int64(-1)
		localHeight  = d.blockchain.CurrentBlock().Number.Uint64()
		remoteHeight = remoteHeader.Number.Uint64()
	)

	log.Debug("XDC sync: finding ancestor", "local", localHeight, "remote", remoteHeight)

	if localHeight >= MaxForkAncestry {
		floor = int64(localHeight - MaxForkAncestry)
	}

	// If we're starting from scratch, ancestor is 0
	if localHeight == 0 {
		log.Info("XDC sync: starting from genesis, ancestor is 0")
		return 0, nil
	}

	// XDC opt: fast-path — verify current chain head with remote peer.
	// If the peer has our head block, skip the expensive span+binary search.
	if localHeight > 0 {
		headBlock := d.blockchain.CurrentBlock()
		headers, err := d.requestHeadersByNumberXDC(p, localHeight, 1, 0, false)
		if err == nil && len(headers) == 1 && headers[0].Hash() == headBlock.Hash() {
			log.Info("XDC sync: fast ancestor (chain head match)", "number", localHeight)
			return localHeight, nil
		}
	}

	// Calculate span parameters
	from, count, skip, max := calculateRequestSpanXDC(remoteHeight, localHeight)
	log.Debug("XDC sync: span search", "from", from, "count", count, "skip", skip, "max", max)

	// Request headers for span search
	headers, err := d.requestHeadersByNumberXDC(p, uint64(from), count, skip, false)
	if err != nil {
		return 0, err
	}

	if len(headers) == 0 {
		log.Warn("XDC sync: empty header set in ancestor search")
		return 0, errEmptyHeaderSet
	}

	// Find the highest known block in the response
	number, hash := uint64(0), common.Hash{}
	for i := len(headers) - 1; i >= 0; i-- {
		if headers[i].Number.Int64() < from || headers[i].Number.Uint64() > max {
			continue
		}
		h := headers[i].Hash()
		n := headers[i].Number.Uint64()

		if d.blockchain.HasBlock(h, n) {
			number, hash = n, h
			break
		}
	}

	// If we found an ancestor, return it
	if hash != (common.Hash{}) {
		if int64(number) <= floor {
			log.Warn("XDC sync: ancestor below floor", "number", number, "floor", floor)
			return 0, errInvalidAncestor
		}
		log.Info("XDC sync: found ancestor in span", "number", number)
		return number, nil
	}

	// Binary search for ancestor
	log.Debug("XDC sync: binary searching for ancestor")
	start, end := uint64(0), remoteHeight
	if floor > 0 {
		start = uint64(floor)
	}

	for start+1 < end {
		check := (start + end) / 2

		headers, err := d.requestHeadersByNumberXDC(p, check, 1, 0, false)
		if err != nil {
			return 0, err
		}
		if len(headers) != 1 {
			return 0, fmt.Errorf("expected 1 header, got %d", len(headers))
		}

		h := headers[0].Hash()
		n := headers[0].Number.Uint64()

		if d.blockchain.HasBlock(h, n) {
			start = check
			hash = h
		} else {
			end = check
		}
	}

	if int64(start) <= floor {
		return 0, errInvalidAncestor
	}

	log.Info("XDC sync: found ancestor via binary search", "number", start)
	return start, nil
}

// fetchHeadersXDC downloads headers using parallel multi-peer fetching with gap filling.
// v14: Dispatches to ALL available peers in parallel with non-overlapping ranges.
// Each peer gets a chunk. Results are collected, gaps are detected and filled,
// then fed to the processor in order. This achieves multi-peer throughput while
// maintaining correctness via gap-filling retries.
func (d *Downloader) fetchHeadersXDC(p *peerConnection, from uint64, pivot uint64, targetHeight uint64) error {
	log.Info("XDC sync: downloading headers (v14 parallel with gap-fill)", "from", from, "pivot", pivot, "target", targetHeight)

	consecutiveFailures := 0
	const maxConsecutiveFailures = 10
	const maxGapFillRetries = 3

	for from <= targetHeight {
		select {
		case <-d.cancelCh:
			return errCanceled
		default:
		}

		// v143: v268 peer batch tracking — some v268 peers stall after ~2 batches
		// due to local verification deadlock. We track batches per peer and rotate
		// to give the peer time to recover, while keeping v142's parallel fetch.
		const v268MaxBatchesPerSession = 2
		v268PeerBatchCount := make(map[string]int)

		// Get all available peers
		allPeers := d.peers.AllPeers()
		if len(allPeers) == 0 {
			allPeers = []*peerConnection{p} // Fallback to primary
		}

		// Filter out v268 peers that have hit their batch limit
		filteredPeers := make([]*peerConnection, 0, len(allPeers))
		for _, peer := range allPeers {
			if peer.version >= 268 {
				if v268PeerBatchCount[peer.id] >= v268MaxBatchesPerSession {
					log.Trace("XDC sync: v268 peer at batch limit, skipping", "peer", peer.id[:8], "batches", v268PeerBatchCount[peer.id])
					continue
				}
			}
			filteredPeers = append(filteredPeers, peer)
		}
		if len(filteredPeers) == 0 {
			// All v268 peers at limit — wait 5s for recovery then retry
			log.Info("XDC sync: all v268 peers at batch limit, waiting for recovery", "from", from)
			time.Sleep(5 * time.Second)
			continue
		}
		allPeers = filteredPeers

		// Dispatch parallel requests — each peer gets a non-overlapping range
		type headerResult struct {
			from    uint64
			headers []*types.Header
			err     error
			peerID  string
		}
		resultCh := make(chan headerResult, len(allPeers))
		dispatched := 0

		for _, peer := range allPeers {
			rangeFrom := from + uint64(dispatched*MaxHeaderFetch)
			if rangeFrom > targetHeight {
				break
			}
			count := MaxHeaderFetch
			if rangeFrom+uint64(count) > targetHeight {
				count = int(targetHeight - rangeFrom + 1)
			}
			if count <= 0 {
				break
			}

			// Launch parallel request
			go func(peer *peerConnection, reqFrom uint64, reqCount int) {
				// XDC #546: short 4s timeout (~25× median 150ms RTT) so stragglers
				// fail fast and the synchronous collect loop completes in ~4s
				// instead of being gated by one slow peer's 15s default timeout.
				// Failed requests return errTimeout, which the existing failure-
				// handling treats as "no progress this iteration" → next outer
				// loop iteration retries, no gap-fill explosion.
				const parallelHeaderTimeout = 4 * time.Second
				headers, err := d.requestHeadersByNumberXDCWithTimeout(peer, reqFrom, reqCount, 0, false, parallelHeaderTimeout)
				// v150: If empty response, retry with smaller batch — v268 peers often
				// return empty for large batches but serve small batches (e.g., 6 headers)
				if err == errEmptyHeaderSet && reqCount > 6 {
					log.Debug("XDC sync: retrying with smaller batch", "peer", peer.id[:8], "from", reqFrom, "was", reqCount, "retry", 6)
					headers, err = d.requestHeadersByNumberXDCWithTimeout(peer, reqFrom, 6, 0, false, parallelHeaderTimeout)
				}
				resultCh <- headerResult{from: reqFrom, headers: headers, err: err, peerID: peer.id}
			}(peer, rangeFrom, count)
			dispatched++
		}

		if dispatched == 0 {
			time.Sleep(500 * time.Millisecond)
			continue
		}

		// Collect results
		results := make(map[uint64][]*types.Header)
		emptySetCount := 0
		successCount := 0

		for i := 0; i < dispatched; i++ {
			result := <-resultCh
			if result.err == errEmptyHeaderSet {
				emptySetCount++
				continue
			}
			if result.err != nil || len(result.headers) == 0 {
				continue
			}
			// Validate first header matches requested range
			firstNum := result.headers[0].Number.Uint64()
			if firstNum != result.from {
				log.Debug("XDC sync: parallel header mismatch", "expected", result.from, "got", firstNum, "peer", result.peerID[:8])
				continue
			}
			results[result.from] = result.headers
			successCount++
			// v143: Track successful batches for v268 peers
			if peer := d.peers.Peer(result.peerID); peer != nil && peer.version >= 268 {
				v268PeerBatchCount[result.peerID]++
				log.Trace("XDC sync: v268 peer batch count incremented", "peer", result.peerID[:8], "count", v268PeerBatchCount[result.peerID])
			}
		}

		if successCount == 0 {
			if emptySetCount == dispatched {
				consecutiveFailures++
				if consecutiveFailures >= maxConsecutiveFailures {
					// Issue #538: if every peer keeps returning empty headers
					// while we are still below targetHeight (the peer's claimed
					// head), the peers cannot serve `from` — this is the
					// checkpoint-only-peer trap (peer's chain starts past
					// `from`, no pre-checkpoint history available). Surface as
					// an error so the syncer records peer backoff / rotates
					// instead of silently exiting with zero progress.
					if from <= targetHeight {
						log.Warn("XDC sync: peer cannot serve requested header range — likely checkpoint-only peer",
							"from", from, "targetHeight", targetHeight, "peer", p.id[:8])
						return errPeersUnavailable
					}
					log.Info("XDC sync: all peers report no more headers", "from", from)
					break
				}
				time.Sleep(500 * time.Millisecond)
				continue
			}
			consecutiveFailures++
			if consecutiveFailures >= maxConsecutiveFailures {
				log.Warn("XDC sync: header download failed after retries", "from", from)
				return errTimeout
			}
			time.Sleep(500 * time.Millisecond)
			continue
		}
		consecutiveFailures = 0

		// Feed results to processor IN ORDER, with gap filling
		nextExpected := from
		gapsFilled := 0

		for {
			hdrs, ok := results[nextExpected]
			if !ok {
				// Gap detected — try to fill from any peer
				if gapsFilled >= maxGapFillRetries {
					log.Debug("XDC sync: gap fill retries exhausted", "from", nextExpected)
					break
				}
				gapPeer := d.peers.Peer(p.id)
				if gapPeer == nil && len(allPeers) > 0 {
					gapPeer = allPeers[0]
				}
				if gapPeer == nil {
					gapPeer = p
				}

				gapCount := MaxHeaderFetch
				if nextExpected+uint64(gapCount) > targetHeight {
					gapCount = int(targetHeight - nextExpected + 1)
				}
				// XDC #546: gap-fill uses the same 4s short timeout as the parallel
				// path. Previously it used the default 15s, which dominated the
				// per-iteration cost when even one parallel peer timed out (4s + 3
				// retries × 15s = up to 49s/iteration). With 4s gap-fill, the
				// worst case becomes 4s + 3 × 4s = 16s, and typical case is ~4s.
				const gapFillTimeout = 4 * time.Second
				gapHeaders, gapErr := d.requestHeadersByNumberXDCWithTimeout(gapPeer, nextExpected, gapCount, 0, false, gapFillTimeout)
				if gapErr == nil && len(gapHeaders) > 0 && gapHeaders[0].Number.Uint64() == nextExpected {
					results[nextExpected] = gapHeaders
					hdrs = gapHeaders
					ok = true
					gapsFilled++
					log.Debug("XDC sync: filled gap", "from", nextExpected, "count", len(gapHeaders), "peer", gapPeer.id[:8])
				} else {
					break // Can't fill gap, stop here
				}
			}

			hashes := make([]common.Hash, len(hdrs))
			for i, h := range hdrs {
				hashes[i] = h.Hash()
			}

			select {
			case d.headerProcCh <- &headerTask{headers: hdrs, hashes: hashes}:
			case <-d.cancelCh:
				return errCanceled
			}

			nextExpected = hdrs[len(hdrs)-1].Number.Uint64() + 1
		}

		// Advance from to the next block we need
		if nextExpected > from {
			from = nextExpected
			// XDC #546: previously a hardcoded 2s sleep was added (v144) to avoid
			// overwhelming v268 peers. Combined with the synchronous wait for ALL
			// dispatched peers above, this caused observed 16-19s gaps between
			// header batches and capped sync at ~30-40 blocks/sec. v268 peers are
			// already rate-limited via v268MaxBatchesPerSession=2, so the global
			// sleep is redundant. Reduced to 100ms to keep a small breathing room
			// for legacy peers without throttling the dispatcher.
			time.Sleep(100 * time.Millisecond)
			log.Debug("XDC sync: parallel header progress", "from", from, "peers", dispatched, "success", successCount, "gaps_filled", gapsFilled)
		} else {
			// No contiguous progress — retry from same point
			consecutiveFailures++
			time.Sleep(200 * time.Millisecond)
		}

		if from > targetHeight {
			log.Info("XDC sync: header download complete", "target", targetHeight)
			break
		}
	}

	// Signal header processor that all headers have been sent
	select {
	case d.headerProcCh <- &headerTask{}:
		log.Debug("XDC sync: sent header completion signal")
	case <-d.cancelCh:
		return errCanceled
	}

	return nil
}

// fetchBodiesXDC downloads block bodies using legacy XDC format.
// v10: Event-driven multi-peer body fetch using queue.ReserveBodies/DeliverBodies.
// Key improvements over v9:
// - Event-driven dispatch: delivery immediately triggers re-dispatch (no 100ms wait)
// - QoS-based batch sizing via peer.BodyCapacity() from msgrate tracker
// - Longer per-peer timeout (8s) to avoid wasteful re-reserves on large batches
// - Larger max batch (MaxBlockFetch=128 from queue, used directly)
// - Immediate re-dispatch channel eliminates idle gaps between fetch rounds
func (d *Downloader) fetchBodiesXDC(p *peerConnection, from uint64) error {
	log.Info("XDC sync: downloading bodies (v10 event-driven)", "from", from)

	// Per-peer in-flight tracking
	type peerRequest struct {
		headers      []*types.Header
		startTime    time.Time
		expectedFrom uint64
		reqId        uint64
	}
	inFlight := make(map[string]*peerRequest)

	// Per-peer QoS tracking
	peerTimeouts := make(map[string]int)

	headersComplete := false
	lastProgress := time.Now()

	// v10: Use a dispatch signal channel — fired on delivery, wake, or tick
	// This eliminates the 100ms idle gap between delivery and next dispatch
	dispatchCh := make(chan struct{}, 1)
	triggerDispatch := func() {
		select {
		case dispatchCh <- struct{}{}:
		default:
		}
	}

	// Background ticker for expiry checks (500ms — less aggressive than 100ms)
	ticker := time.NewTicker(500 * time.Millisecond)
	defer ticker.Stop()

	progressTicker := time.NewTicker(8 * time.Second)
	defer progressTicker.Stop()
	totalBodiesDelivered := 0

	// v11: 5s timeout — faster peer rotation; large batches with 64-block minimum
	const peerTimeout = 5 * time.Second
	const globalStallTimeout = 20 * time.Second

	// Trigger initial dispatch
	triggerDispatch()

	for {
		select {
		case <-d.cancelCh:
			return errCanceled

		case resp := <-xdcBodyCh:
			// === DELIVERY ===
			req, ok := inFlight[resp.peerId]
			if !ok {
				log.Debug("XDC sync: late body response", "peer", resp.peerId[:8], "bodies", len(resp.txs))
				continue
			}
			// Issue #314: discard stale body responses from previous sync cycles
			if resp.requestID != req.reqId {
				log.Warn("XDC sync: discarding stale body response (request ID mismatch)", "expected", req.reqId, "got", resp.requestID, "peer", resp.peerId[:8])
				continue
			}

			bodyCount := len(resp.txs)
			if bodyCount == 0 {
				log.Debug("XDC sync: empty body response", "peer", resp.peerId[:8])
				delete(inFlight, resp.peerId)
				// Only expire in queue if request still exists (avoid race with ticker)
				if d.queue.ExpireBodies(resp.peerId) == 0 {
					log.Debug("XDC sync: body request already expired by ticker", "peer", resp.peerId[:8])
				}
				// v150: Don't penalize v268 peers for empty body responses — they may
				// not have bodies for blocks they're serving headers for (e.g., light clients
				// or nodes with pruned bodies). Just retry later.
				if peer := d.peers.Peer(resp.peerId); peer != nil && peer.version >= 268 {
					log.Trace("XDC sync: v268 peer returned empty bodies, not counting as timeout", "peer", resp.peerId[:8])
				} else {
					peerTimeouts[resp.peerId]++
				}
				triggerDispatch()
				continue
			}

			if len(resp.uncles) != bodyCount {
				log.Warn("XDC sync: tx/uncle count mismatch", "peer", resp.peerId[:8],
					"txs", bodyCount, "uncles", len(resp.uncles))
				delete(inFlight, resp.peerId)
				// Only expire in queue if request still exists (avoid race with ticker)
				if d.queue.ExpireBodies(resp.peerId) == 0 {
					log.Debug("XDC sync: body request already expired by ticker", "peer", resp.peerId[:8])
				}
				peerTimeouts[resp.peerId]++
				triggerDispatch()
				continue
			}

			delete(inFlight, resp.peerId)

			// Compute hashes for delivery validation
			hasher := trie.NewStackTrie(nil)
			txHashes := make([]common.Hash, bodyCount)
			uncleHashes := make([]common.Hash, bodyCount)
			withdrawals := make([][]*types.Withdrawal, bodyCount)
			withdrawalHashes := make([]common.Hash, bodyCount)

			for i := 0; i < bodyCount; i++ {
				txHashes[i] = types.DeriveSha(types.Transactions(resp.txs[i]), hasher)
				uncleHashes[i] = types.CalcUncleHash(resp.uncles[i])
			}

			// v10: Update peer throughput tracker for QoS-based batch sizing
			if peer := d.peers.Peer(resp.peerId); peer != nil && req != nil {
				peer.UpdateBodyRate(bodyCount, time.Since(req.startTime))
			}

			accepted, err := d.queue.DeliverBodies(resp.peerId, resp.txs, txHashes,
				resp.uncles, uncleHashes, withdrawals, withdrawalHashes)
			if err != nil {
				log.Debug("XDC sync: body delivery failed", "peer", resp.peerId[:8], "err", err)
				peerTimeouts[resp.peerId]++
			} else if accepted > 0 {
				log.Debug("XDC sync: bodies delivered", "peer", resp.peerId[:8],
					"accepted", accepted, "elapsed", time.Since(req.startTime))
				lastProgress = time.Now()
				totalBodiesDelivered += accepted
				peerTimeouts[resp.peerId] = 0
			}

			// v10: IMMEDIATELY re-dispatch — this peer is now idle
			triggerDispatch()

		case <-progressTicker.C:
			log.Debug("XDC sync: body progress",
				"pending", d.queue.PendingBodies(),
				"inFlight", len(inFlight),
				"delivered", totalBodiesDelivered,
				"peers", d.peers.Len(),
				"headersComplete", headersComplete)

		case cont := <-d.queue.blockWakeCh:
			if !cont {
				headersComplete = true
				log.Info("XDC sync: header processing complete (v10)")
			}
			// New headers scheduled — dispatch work
			triggerDispatch()

		case <-ticker.C:
			// === PERIODIC: Expire timeouts + check completion ===

			// 1. Check completion
			pending := d.queue.PendingBodies()
			flying := len(inFlight)
			if headersComplete && pending == 0 && flying == 0 {
				log.Info("XDC sync: body download complete (v10)",
					"total", totalBodiesDelivered)
				return nil
			}

			// 2. Expire timed-out per-peer requests
			now := time.Now()
			expired := false
			for peerId, req := range inFlight {
				if now.Sub(req.startTime) > peerTimeout {
					log.Debug("XDC sync: peer body timeout", "peer", peerId[:8],
						"elapsed", now.Sub(req.startTime),
						"blocks", len(req.headers))
					d.queue.ExpireBodies(peerId)
					delete(inFlight, peerId)
					// v150: Don't penalize v268 peers for body timeouts — they may be
					// slow to respond due to verification overhead or pruned data.
					if peer := d.peers.Peer(peerId); peer != nil && peer.version >= 268 {
						log.Trace("XDC sync: v268 peer body timeout, not counting", "peer", peerId[:8])
					} else {
						peerTimeouts[peerId]++
					}
					expired = true

					// Don't drop peers — just skip them for a while
					if peerTimeouts[peerId] >= 10 {
						log.Debug("XDC sync: peer has many timeouts, skipping", "peer", peerId[:8], "timeouts", peerTimeouts[peerId])
						delete(peerTimeouts, peerId)
					}
				}
			}
			if expired {
				lastProgress = now
				triggerDispatch()
			}

			// 3. Global stall check
			// v136-fix: Only stall if we have NO peers AND no primary peer fallback.
			// The primary peer `p` passed to fetchBodiesXDC is always valid if
			// we got this far — use it as ultimate fallback.
			if (pending > 0 || flying > 0) && d.peers.Len() == 0 {
				// If primary peer exists, don't stall — it can still serve bodies
				if p == nil {
					if now.Sub(lastProgress) > globalStallTimeout {
						log.Warn("XDC sync: no peers for body download",
							"pending", pending, "elapsed", now.Sub(lastProgress))
						return errNoPeers
					}
				} else {
					log.Debug("XDC sync: peer set empty but primary peer available, continuing", "peer", p.id[:8])
					lastProgress = now // Reset stall timer since we have a viable peer
				}
			}

			// Also trigger periodic dispatch in case signals were missed
			triggerDispatch()

		case <-dispatchCh:
			// === DISPATCH: Send work to all idle peers ===
			if d.queue.PendingBodies() <= 0 {
				continue
			}

			allPeers := d.peers.AllPeers()
			// v135-fix: If downloader peer set is empty but we have a primary sync peer,
			// fall back to using it directly. This fixes body download stall when the
			// peer is connected at p2p layer but not registered in the downloader.
			if len(allPeers) == 0 && p != nil {
				log.Debug("XDC sync: downloader peer set empty, falling back to primary sync peer", "peer", p.id[:8], "version", p.version)
				allPeers = []*peerConnection{p}
			}
			if len(allPeers) == 0 {
				log.Debug("XDC sync: no peers available for body dispatch")
				continue
			}
			now := time.Now()
			for _, peer := range allPeers {
				if d.queue.PendingBodies() <= 0 {
					break
				}
				if _, ok := inFlight[peer.id]; ok {
					continue
				}
				if peerTimeouts[peer.id] >= 8 {
					continue
				}

				// v10: Use QoS-based capacity from msgrate tracker
				// BodyCapacity returns optimal batch size based on measured RTT
				// Falls back to MaxBlockFetch (256) for new peers (v96: increased from 128)
				batchSize := peer.BodyCapacity(time.Second)
				if batchSize < core.XdcBodyMinBatch {
					batchSize = core.XdcBodyMinBatch
				}
				// v96: Cap at MaxBlockFetch (256) for larger body batches
				if batchSize > MaxBlockFetch {
					batchSize = MaxBlockFetch
				}

				request, _, _ := d.queue.ReserveBodies(peer, batchSize)
				if request == nil {
					log.Trace("XDC sync: ReserveBodies returned nil", "peer", peer.id[:8], "batchSize", batchSize, "pending", d.queue.PendingBodies())
					continue
				}

				hashes := make([]common.Hash, len(request.Headers))
				for i, header := range request.Headers {
					hashes[i] = header.Hash()
				}

				expectedFrom := request.Headers[0].Number.Uint64()
				log.Trace("XDC sync: dispatching bodies", "peer", peer.id[:8],
					"count", len(hashes), "capacity", batchSize,
					"from", expectedFrom, "firstHash", hashes[0].Hex())

				// Issue #314: atomically increment request ID and include it in request metadata
				reqId := d.nextRequestID.Add(1)
				if err := peer.peer.RequestBodiesLegacy(hashes, reqId); err != nil {
					log.Debug("XDC sync: body request failed", "peer", peer.id[:8], "err", err)
					d.queue.ExpireBodies(peer.id)
					continue
				}

				inFlight[peer.id] = &peerRequest{
					headers:      request.Headers,
					startTime:    now,
					expectedFrom: expectedFrom,
					reqId:        reqId,
				}
			}
		}
	}
}

// calculateRequestSpanXDC calculates header request parameters
func calculateRequestSpanXDC(remoteHeight, localHeight uint64) (int64, int, int, uint64) {
	var (
		from     int
		count    int
		MaxCount = MaxHeaderFetch / 16
	)

	requestHead := int(remoteHeight) - 1
	if requestHead < 0 {
		requestHead = 0
	}

	requestBottom := int(localHeight - 1)
	if requestBottom < 0 {
		requestBottom = 0
	}

	totalSpan := requestHead - requestBottom
	span := 1 + totalSpan/MaxCount
	if span < 2 {
		span = 2
	}
	if span > 16 {
		span = 16
	}

	count = 1 + totalSpan/span
	if count > MaxCount {
		count = MaxCount
	}
	if count < 2 {
		count = 2
	}

	from = requestHead - (count-1)*span
	if from < 0 {
		from = 0
	}

	max := from + (count-1)*span
	return int64(from), count, span - 1, uint64(max)
}

// XDCHeaderDeliveryLock protects concurrent header delivery
var XDCHeaderDeliveryLock sync.Mutex

// Ancestor cache — avoids repeated binary search when sync restarts with a new peer
var (
	lastSyncAncestor     uint64
	lastSyncAncestorTime time.Time
	lastSyncAncestorMu   sync.Mutex
)
