diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 5e90164aaa..6c0fa2315c 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -76,6 +76,7 @@ var ( utils.BlobPoolDataCapFlag, utils.BlobPoolPriceBumpFlag, utils.SyncModeFlag, + utils.SnapExactPivotFlag, utils.SyncTargetFlag, utils.ExitWhenSyncedFlag, utils.GCModeFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index a248d4fa8a..8c5464acea 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -280,6 +280,15 @@ var ( Value: ethconfig.Defaults.SyncMode.String(), Category: flags.StateCategory, } + SnapExactPivotFlag = &cli.BoolFlag{ + Name: "snap.exactpivot", + Usage: "Pin the snap sync pivot to the exact announced head instead of head-64, so the downloaded " + + "state root equals stateRoot(head). Only safe against a trusted peer that serves a fixed, " + + "non-reorging head (e.g. a frozen / snapshot-restored source); on a live network the head can reorg " + + "beneath the pivot and corrupt the synced state. Intended for state-growth benchmarking and " + + "trusted-peer setups, not general mainnet operation.", + Category: flags.StateCategory, + } GCModeFlag = &cli.StringFlag{ Name: "gcmode", Usage: `Blockchain garbage collection mode ("full", "archive")`, @@ -1757,6 +1766,15 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { } } + if ctx.IsSet(SnapExactPivotFlag.Name) { + cfg.SnapExactPivot = ctx.Bool(SnapExactPivotFlag.Name) + if cfg.SnapExactPivot { + log.Warn("Snap sync pivot pinned to the exact head via --snap.exactpivot: only safe against a " + + "trusted peer serving a fixed, non-reorging head. On a live network a reorg beneath the pivot " + + "can corrupt the synced state. Use only for benchmarking / trusted-peer setups.") + } + } + if ctx.IsSet(ChainHistoryFlag.Name) { value := ctx.String(ChainHistoryFlag.Name) if err := cfg.HistoryMode.UnmarshalText([]byte(value)); err != nil { diff --git a/eth/backend.go b/eth/backend.go index 7438486696..0e37c71430 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -352,6 +352,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { TxPool: eth.txPool, Network: networkID, Sync: config.SyncMode, + SnapExactPivot: config.SnapExactPivot, BloomCache: uint64(cacheLimit), RequiredBlocks: config.RequiredBlocks, SnapV2: config.SnapV2, diff --git a/eth/downloader/beaconsync.go b/eth/downloader/beaconsync.go index 23daafa8f6..1ef5d172c8 100644 --- a/eth/downloader/beaconsync.go +++ b/eth/downloader/beaconsync.go @@ -306,6 +306,9 @@ func (d *Downloader) fetchHeaders(from uint64) error { // Retrieve the next pivot header, either from skeleton chain // or the filled chain number := head.Number.Uint64() - uint64(fsMinFullBlocks) + if d.exactPivot { + number = head.Number.Uint64() + } log.Warn("Pivot seemingly stale, moving", "old", d.pivotHeader.Number, "new", number) if d.pivotHeader = d.skeleton.Header(number); d.pivotHeader == nil { diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index f23d9396ee..9e65b43252 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -141,6 +141,7 @@ type Downloader struct { // State sync pivotHeader *types.Header // Pivot block header to dynamically push the syncing state root pivotLock sync.RWMutex // Lock protecting pivot header reads from updates + exactPivot bool // Pin the snap pivot to the exact head instead of head-fsMinFullBlocks snapSyncer snap.Syncer // snap/1 or snap/2 state syncer, selected at construction stateSyncStart chan *stateSync @@ -261,6 +262,13 @@ func New(stateDb ethdb.Database, mode ethconfig.SyncMode, chain BlockChain, drop return dl } +// SetExactPivot pins the snap sync pivot to the exact announced head instead of +// head-fsMinFullBlocks, so the downloaded state root equals stateRoot(head). +// Must be set before synchronisation starts. +func (d *Downloader) SetExactPivot(v bool) { + d.exactPivot = v +} + // Progress retrieves the synchronisation boundaries, specifically the origin // block where synchronisation started at (may have failed/suspended); the block // or header sync is currently at; and the latest known block which the sync targets. @@ -472,7 +480,12 @@ func (d *Downloader) syncToHead() (err error) { if err != nil { return err } - if latest.Number.Uint64() > uint64(fsMinFullBlocks) { + if d.exactPivot { + // With --snap.exactpivot, pin the pivot to the exact announced head + // instead of head-fsMinFullBlocks, so the synced state root equals + // stateRoot(head). Safe only against a fixed, non-reorging head. + pivot = latest + } else if latest.Number.Uint64() > uint64(fsMinFullBlocks) { number := latest.Number.Uint64() - uint64(fsMinFullBlocks) // Retrieve the pivot header from the skeleton chain segment but diff --git a/eth/downloader/downloader_test.go b/eth/downloader/downloader_test.go index de3a3f1991..8e86d205ee 100644 --- a/eth/downloader/downloader_test.go +++ b/eth/downloader/downloader_test.go @@ -683,6 +683,43 @@ func testBeaconSync(t *testing.T, protocol uint, mode SyncMode) { } } +// TestBeaconSyncSnapExactPivot verifies that SetExactPivot pins the snap sync +// pivot to the exact announced head instead of head-fsMinFullBlocks, so the +// synced state root equals stateRoot(head). +func TestBeaconSyncSnapExactPivot(t *testing.T) { + success := make(chan struct{}) + tester := newTesterWithNotification(t, SnapSync, func() { + close(success) + }) + defer tester.terminate() + tester.downloader.SetExactPivot(true) + + chain := testChainBase.shorten(blockCacheMaxItems - 15) + tester.newPeer("peer", eth.ETH69, chain.blocks[1:]) + + head := chain.blocks[len(chain.blocks)-1].Header() + if err := tester.downloader.BeaconSync(head, nil); err != nil { + t.Fatalf("failed to beacon sync chain: %v", err) + } + select { + case <-success: + if bs := int(tester.chain.CurrentBlock().Number.Uint64()) + 1; bs != len(chain.blocks) { + t.Fatalf("synchronised blocks mismatch: have %v, want %v", bs, len(chain.blocks)) + } + // With the exact-pivot flag the snap pivot must equal the head, not + // head-fsMinFullBlocks as in the default beacon snap sync. + pivot := rawdb.ReadLastPivotNumber(tester.downloader.stateDB) + if pivot == nil { + t.Fatalf("no snap pivot recorded") + } + if *pivot != head.Number.Uint64() { + t.Fatalf("pivot mismatch: have %d, want %d (exact head)", *pivot, head.Number.Uint64()) + } + case <-time.NewTimer(time.Second * 3).C: + t.Fatalf("failed to sync chain in three seconds") + } +} + // Tests that synchronisation progress (origin block number, current block number // and highest block number) is tracked and updated correctly. func TestSyncProgressFull(t *testing.T) { testSyncProgress(t, eth.ETH69, FullSync) } diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 99a0fa89f7..6d22c48100 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -94,6 +94,11 @@ type Config struct { NetworkId uint64 SyncMode SyncMode + // SnapExactPivot pins the snap sync pivot to the exact announced head + // instead of head-fsMinFullBlocks, so the downloaded state root equals + // stateRoot(head). Intended for syncing a fixed state from a trusted peer. + SnapExactPivot bool + // HistoryMode configures chain history retention. HistoryMode history.HistoryMode diff --git a/eth/handler.go b/eth/handler.go index 2028517cec..a05aca9fad 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -106,6 +106,7 @@ type handlerConfig struct { TxPool txPool // Transaction pool to propagate from Network uint64 // Network identifier to advertise Sync ethconfig.SyncMode // Whether to snap or full sync + SnapExactPivot bool // Pin the snap pivot to the exact head instead of head-fsMinFullBlocks BloomCache uint64 // Megabytes to alloc for snap sync bloom RequiredBlocks map[uint64]common.Hash // Hard coded map of required block hashes for sync challenges SnapV2 bool // Whether to advertise and sync via the snap/2 protocol @@ -158,6 +159,7 @@ func newHandler(config *handlerConfig) (*handler, error) { } // Construct the downloader (long sync) h.downloader = downloader.New(config.Database, config.Sync, h.chain, h.removePeer, h.enableSyncedFeatures, config.SnapV2) + h.downloader.SetExactPivot(config.SnapExactPivot) // If snap sync is requested but snapshots are disabled, fail loudly if h.downloader.ConfigSyncMode() == ethconfig.SnapSync && (config.Chain.Snapshots() == nil && config.Chain.TrieDB().Scheme() == rawdb.HashScheme) {