Skip to content

Commit 62ad130

Browse files
committed
test: add fast sync pivot gap tests
1 parent 23be98b commit 62ad130

2 files changed

Lines changed: 246 additions & 1 deletion

File tree

eth/downloader/downloader.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,12 +266,15 @@ func New(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, lightchai
266266
// and will verify the pivot block's hash after state sync completes.
267267
// It also calculates gap pivots at some intervals that need state sync.
268268
func (d *Downloader) SetPivotBlock(number uint64, hash common.Hash, root common.Hash) {
269+
// Gap pivots are an XDPoS concept; skip the calculation when XDPoS is not configured.
270+
if d.blockchain.Config().XDPoS == nil {
271+
return
272+
}
269273
d.pivotNumber = number
270274
d.pivotHash = hash
271275
d.pivotRoot = root
272276

273277
// Calculate all gap pivot numbers: N - N%Epoch - Gap where x < N
274-
275278
epoch := d.blockchain.Config().XDPoS.Epoch
276279
gap := d.blockchain.Config().XDPoS.Gap
277280
epochBase := number - number%epoch

eth/downloader/downloader_test.go

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package downloader
1818

1919
import (
20+
"encoding/json"
2021
"errors"
2122
"fmt"
2223
"math/big"
@@ -28,6 +29,7 @@ import (
2829

2930
ethereum "github.com/XinFinOrg/XDPoSChain"
3031
"github.com/XinFinOrg/XDPoSChain/common"
32+
engine_v2 "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/engines/engine_v2"
3133
"github.com/XinFinOrg/XDPoSChain/core/rawdb"
3234
"github.com/XinFinOrg/XDPoSChain/core/types"
3335
"github.com/XinFinOrg/XDPoSChain/ethdb"
@@ -62,6 +64,10 @@ type downloadTester struct {
6264

6365
insertHeaderChainHook func([]*types.Header) error
6466

67+
// configOverride, when non-nil, is returned by Config() instead of the
68+
// default TestChainConfig. Used by tests that require XDPoS to be active.
69+
configOverride *params.ChainConfig
70+
6571
lock sync.RWMutex
6672
}
6773

@@ -344,6 +350,9 @@ func (dl *downloadTester) handleProposedBlock(header *types.Header) error {
344350

345351
// Config retrieves the blockchain's chain configuration.
346352
func (dl *downloadTester) Config() *params.ChainConfig {
353+
if dl.configOverride != nil {
354+
return dl.configOverride
355+
}
347356
config := *params.TestChainConfig
348357
config.Eip1559Block = big.NewInt(0)
349358
return &config
@@ -1803,3 +1812,236 @@ func testReorgProtectionDoesNotStallSync(t *testing.T, protocol int, mode SyncMo
18031812
})
18041813
}
18051814
}
1815+
1816+
// TestSetPivotBlockStoresFields verifies that SetPivotBlock persists the pivot
1817+
// number, hash, and state root onto the downloader for later use during sync.
1818+
func TestSetPivotBlockStoresFields(t *testing.T) {
1819+
t.Parallel()
1820+
1821+
tester := newTester()
1822+
// Provide an XDPoS config so SetPivotBlock does not short-circuit.
1823+
tester.configOverride = params.TestXDPoSMockChainConfig
1824+
defer tester.terminate()
1825+
d := tester.downloader
1826+
1827+
wantNumber := uint64(1000)
1828+
wantHash := common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
1829+
wantRoot := common.HexToHash("0xcafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe")
1830+
1831+
d.SetPivotBlock(wantNumber, wantHash, wantRoot)
1832+
1833+
if d.pivotNumber != wantNumber {
1834+
t.Errorf("pivotNumber mismatch: have %v, want %v", d.pivotNumber, wantNumber)
1835+
}
1836+
if d.pivotHash != wantHash {
1837+
t.Errorf("pivotHash mismatch: have %v, want %v", d.pivotHash, wantHash)
1838+
}
1839+
if d.pivotRoot != wantRoot {
1840+
t.Errorf("pivotRoot mismatch: have %v, want %v", d.pivotRoot, wantRoot)
1841+
}
1842+
}
1843+
1844+
// TestSetPivotBlockGapCalculation verifies the gap pivot number derivation
1845+
// produced by SetPivotBlock for a range of primary pivot numbers.
1846+
//
1847+
// With TestXDPoSMockChainConfig (Epoch=900, Gap=450):
1848+
//
1849+
// epochBase = pivot - pivot%900
1850+
// baseGap = epochBase-450 (or 900-450=450 when epochBase < 450)
1851+
// gaps = { baseGap + 900*i | i=0,1,… while value < pivot }
1852+
func TestSetPivotBlockGapCalculation(t *testing.T) {
1853+
t.Parallel()
1854+
1855+
tester := newTester()
1856+
// TestXDPoSMockChainConfig has Epoch=900, Gap=450.
1857+
tester.configOverride = params.TestXDPoSMockChainConfig
1858+
defer tester.terminate()
1859+
d := tester.downloader
1860+
1861+
tests := []struct {
1862+
pivot uint64
1863+
wantGaps []uint64
1864+
}{
1865+
// pivot ≤ baseGap(450): no gap numbers are strictly less than pivot
1866+
{pivot: 200, wantGaps: nil},
1867+
{pivot: 450, wantGaps: nil},
1868+
// first gap (450) is below pivot for the first time
1869+
{pivot: 451, wantGaps: []uint64{450}},
1870+
// pivot at exact epoch boundary (900): only gap at 450
1871+
// epochBase=900, baseGap=900-450=450; 450<900→add, 1350≥900→stop
1872+
{pivot: 900, wantGaps: []uint64{450}},
1873+
// pivot at baseGap+epoch (1350): 450<1350→add, 1350≥1350→stop
1874+
{pivot: 1350, wantGaps: []uint64{450}},
1875+
// pivot just above 1350: both 450 and 1350 qualify
1876+
// epochBase=900, baseGap=450; 450<1351→add, 1350<1351→add, 2250≥1351→stop
1877+
{pivot: 1351, wantGaps: []uint64{450, 1350}},
1878+
// pivot at 2*epoch (1800): epochBase=1800, baseGap=1800-450=1350;
1879+
// 1350<1800→add, 2250≥1800→stop → only [1350]
1880+
{pivot: 1800, wantGaps: []uint64{1350}},
1881+
// pivot just above 2250 to get two gaps in a different epoch window:
1882+
// epochBase=1800, baseGap=1350; 1350<2251→add, 2250<2251→add, 3150≥2251→stop
1883+
{pivot: 2251, wantGaps: []uint64{1350, 2250}},
1884+
}
1885+
1886+
for _, tc := range tests {
1887+
d.SetPivotBlock(tc.pivot, common.Hash{}, common.Hash{})
1888+
1889+
d.pivotGapLock.RLock()
1890+
got := make([]uint64, len(d.pivotGapNumbers))
1891+
copy(got, d.pivotGapNumbers)
1892+
d.pivotGapLock.RUnlock()
1893+
1894+
if len(got) != len(tc.wantGaps) {
1895+
t.Errorf("pivot %d: gap count mismatch: have %v, want %v", tc.pivot, got, tc.wantGaps)
1896+
continue
1897+
}
1898+
for i, g := range got {
1899+
if g != tc.wantGaps[i] {
1900+
t.Errorf("pivot %d: gap[%d] = %v, want %v", tc.pivot, i, g, tc.wantGaps[i])
1901+
}
1902+
}
1903+
}
1904+
}
1905+
1906+
// TestFastSyncPivotHashMismatch checks that processFastSyncContent returns a
1907+
// descriptive "pivot block hash mismatch" error when the configured pivot hash
1908+
// does not match the actual downloaded pivot block.
1909+
func TestFastSyncPivotHashMismatch(t *testing.T) {
1910+
t.Parallel()
1911+
1912+
tester := newTester()
1913+
// XDPoS config is required so SetPivotBlock can compute gap numbers.
1914+
// TestXDPoSMockChainConfig has Epoch=900, Gap=450.
1915+
tester.configOverride = params.TestXDPoSMockChainConfig
1916+
defer tester.terminate()
1917+
1918+
// Use a chain short enough to be fast but long enough to trigger state sync.
1919+
chainLen := 300
1920+
chain := testChainBase.shorten(chainLen)
1921+
tester.newPeer("peer", 63, chain)
1922+
1923+
// Identify the natural pivot block so we can supply the correct state root
1924+
// (so state sync succeeds) while feeding a wrong hash (so the check fires).
1925+
// Natural pivot = headBlock().Number - fsMinFullBlocks = (chainLen-1) - fsMinFullBlocks.
1926+
pivotNum := uint64(chainLen - 1 - fsMinFullBlocks) // = 235
1927+
pivotRoot := chain.headerm[chain.chain[pivotNum]].Root
1928+
1929+
wrongHash := common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
1930+
tester.downloader.SetPivotBlock(pivotNum, wrongHash, pivotRoot)
1931+
1932+
err := tester.sync("peer", nil, FastSync)
1933+
if err == nil {
1934+
t.Fatal("expected pivot hash mismatch error, got nil")
1935+
}
1936+
if !strings.Contains(err.Error(), "pivot block hash mismatch") {
1937+
t.Fatalf("unexpected error: %q (want substring %q)", err.Error(), "pivot block hash mismatch")
1938+
}
1939+
}
1940+
1941+
// TestFastSyncConfiguredPivotHashMatch verifies that setting the correct pivot
1942+
// hash and state root allows fast sync to complete successfully.
1943+
func TestFastSyncConfiguredPivotHashMatch(t *testing.T) {
1944+
t.Parallel()
1945+
1946+
tester := newTester()
1947+
// XDPoS config is required so SetPivotBlock can compute gap numbers.
1948+
// TestXDPoSMockChainConfig has Epoch=900, Gap=450.
1949+
tester.configOverride = params.TestXDPoSMockChainConfig
1950+
defer tester.terminate()
1951+
1952+
chainLen := 300
1953+
chain := testChainBase.shorten(chainLen)
1954+
tester.newPeer("peer", 63, chain)
1955+
1956+
// Natural pivot = headBlock().Number - fsMinFullBlocks = (chainLen-1) - fsMinFullBlocks.
1957+
pivotNum := uint64(chainLen - 1 - fsMinFullBlocks) // = 235
1958+
pivotHash := chain.headerm[chain.chain[pivotNum]].Hash()
1959+
pivotRoot := chain.headerm[chain.chain[pivotNum]].Root
1960+
1961+
// With Epoch=900 and pivot=235 the calculated baseGap=450 > pivot, so
1962+
// pivotGapNumbers will be empty – this test focuses purely on hash verification.
1963+
tester.downloader.SetPivotBlock(pivotNum, pivotHash, pivotRoot)
1964+
1965+
if err := tester.sync("peer", nil, FastSync); err != nil {
1966+
t.Fatalf("fast sync with correct pivot hash failed: %v", err)
1967+
}
1968+
assertOwnChain(t, tester, chainLen)
1969+
}
1970+
1971+
// TestFastSyncGapPivotSync exercises the gap-pivot state-sync path: when the
1972+
// configured pivot is high enough that SetPivotBlock calculates one or more gap
1973+
// pivot numbers, processFastSyncContent must state-sync each gap block and
1974+
// generate a snapshot for it before committing the primary pivot.
1975+
//
1976+
// Chain layout (Epoch=900, Gap=450, pivot=536, gap pivot=[450]):
1977+
//
1978+
// blocks 1-535 → fast-sync (receipts)
1979+
// block 450 → gap pivot: state synced + snapshot generated
1980+
// block 536 → primary pivot: state synced + committed
1981+
// blocks 537-600 → full-sync
1982+
func TestFastSyncGapPivotSync(t *testing.T) {
1983+
t.Parallel()
1984+
1985+
tester := newTester()
1986+
// XDPoS config is required so SetPivotBlock can compute gap numbers.
1987+
// TestXDPoSMockChainConfig has Epoch=900, Gap=450.
1988+
tester.configOverride = params.TestXDPoSMockChainConfig
1989+
defer tester.terminate()
1990+
1991+
// 600 blocks: natural pivot = 600-64 = 536, gap pivot = 450.
1992+
chainLen := 600
1993+
chain := testChainBase.shorten(chainLen)
1994+
tester.newPeer("peer", 63, chain)
1995+
1996+
// Natural pivot = headBlock().Number - fsMinFullBlocks = (chainLen-1) - fsMinFullBlocks.
1997+
pivotNum := uint64(chainLen - 1 - fsMinFullBlocks) // = 535
1998+
pivotHash := chain.headerm[chain.chain[pivotNum]].Hash()
1999+
pivotRoot := chain.headerm[chain.chain[pivotNum]].Root
2000+
2001+
tester.downloader.SetPivotBlock(pivotNum, pivotHash, pivotRoot)
2002+
2003+
// After SetPivotBlock the gap list should contain exactly block 450:
2004+
// epochBase=0 (535<900), baseGap=450, first gap=450 < 535.
2005+
tester.downloader.pivotGapLock.RLock()
2006+
gaps := make([]uint64, len(tester.downloader.pivotGapNumbers))
2007+
copy(gaps, tester.downloader.pivotGapNumbers)
2008+
tester.downloader.pivotGapLock.RUnlock()
2009+
2010+
if len(gaps) != 1 || gaps[0] != 450 {
2011+
t.Fatalf("expected gap pivots [450], got %v", gaps)
2012+
}
2013+
2014+
if err := tester.sync("peer", nil, FastSync); err != nil {
2015+
t.Fatalf("fast sync with gap pivot failed: %v", err)
2016+
}
2017+
assertOwnChain(t, tester, chainLen)
2018+
2019+
// After a successful sync the gap list should have been cleared.
2020+
tester.downloader.pivotGapLock.RLock()
2021+
remaining := len(tester.downloader.pivotGapNumbers)
2022+
tester.downloader.pivotGapLock.RUnlock()
2023+
if remaining != 0 {
2024+
t.Errorf("pivotGapNumbers not cleared after sync: %d entries remain", remaining)
2025+
}
2026+
2027+
// Verify that the snapshot for the gap pivot block (450) was stored and can
2028+
// be loaded back from the downloader's state database.
2029+
gapBlockHash := chain.headerm[chain.chain[450]].Hash()
2030+
blob, err := rawdb.ReadXdposV2Snapshot(tester.downloader.stateDB, gapBlockHash)
2031+
if err != nil {
2032+
t.Fatalf("snapshot for gap pivot block 450 not found in stateDB: %v", err)
2033+
}
2034+
if len(blob) == 0 {
2035+
t.Fatal("snapshot blob for gap pivot block 450 is empty")
2036+
}
2037+
var snap engine_v2.SnapshotV2
2038+
if err := json.Unmarshal(blob, &snap); err != nil {
2039+
t.Fatalf("failed to unmarshal gap pivot snapshot: %v", err)
2040+
}
2041+
if snap.Number != 450 {
2042+
t.Errorf("snapshot number mismatch: have %d, want 450", snap.Number)
2043+
}
2044+
if snap.Hash != gapBlockHash {
2045+
t.Errorf("snapshot hash mismatch: have %v, want %v", snap.Hash, gapBlockHash)
2046+
}
2047+
}

0 commit comments

Comments
 (0)