diff --git a/cmd/geth/main.go b/cmd/geth/main.go index e6d1128ba..a94c0c17d 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -86,6 +86,7 @@ var ( utils.TxPoolGlobalQueueFlag, utils.TxPoolLifetimeFlag, utils.SyncModeFlag, + utils.SyncTargetFlag, utils.ExitWhenSyncedFlag, utils.GCModeFlag, utils.SnapshotFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index d705d7a16..974c03579 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -18,6 +18,7 @@ package utils import ( + "bytes" "context" "crypto/ecdsa" "errors" @@ -36,10 +37,12 @@ import ( "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/fdlimit" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth" @@ -68,6 +71,7 @@ import ( "github.com/ethereum/go-ethereum/p2p/nat" "github.com/ethereum/go-ethereum/p2p/netutil" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" pcsclite "github.com/gballet/go-libpcsclite" gopsutil "github.com/shirou/gopsutil/mem" @@ -664,11 +668,18 @@ var ( Category: flags.LoggingCategory, } + // MISC settings IgnoreLegacyReceiptsFlag = &cli.BoolFlag{ Name: "ignore-legacy-receipts", Usage: "Geth will start up even if there are legacy receipts in freezer", Category: flags.MiscCategory, } + SyncTargetFlag = &cli.PathFlag{ + Name: "synctarget", + Usage: `File for containing the hex-encoded block-rlp as sync target(dev feature)`, + TakesFile: true, + Category: flags.MiscCategory, + } // RPC settings IPCDisabledFlag = &cli.BoolFlag{ @@ -1874,6 +1885,25 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { cfg.EthDiscoveryURLs = SplitAndTrim(urls) } } + if ctx.IsSet(SyncTargetFlag.Name) { + path := ctx.Path(SyncTargetFlag.Name) + if path == "" { + Fatalf("Failed to resolve file path") + } + blob, err := os.ReadFile(path) + if err != nil { + Fatalf("Failed to read block file: %v", err) + } + rlpBlob, err := hexutil.Decode(string(bytes.TrimRight(blob, "\r\n"))) + if err != nil { + Fatalf("Failed to decode block blob: %v", err) + } + var block types.Block + if err := rlp.DecodeBytes(rlpBlob, &block); err != nil { + Fatalf("Failed to decode block: %v", err) + } + cfg.SyncTarget = &block + } // Override any default configs for hard coded networks. switch { case ctx.Bool(MainnetFlag.Name): @@ -2027,6 +2057,13 @@ func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (ethapi.Backend Fatalf("Failed to register the Engine API service: %v", err) } stack.RegisterAPIs(tracers.APIs(backend.APIBackend)) + + // Register the auxiliary full-sync tester service in case the sync + // target is configured. + if cfg.SyncTarget != nil && cfg.SyncMode == downloader.FullSync { + ethcatalyst.RegisterFullSyncTester(stack, backend, cfg.SyncTarget) + log.Info("Registered full-sync tester", "number", cfg.SyncTarget.NumberU64(), "hash", cfg.SyncTarget.Hash()) + } return backend.APIBackend, backend } diff --git a/eth/catalyst/tester.go b/eth/catalyst/tester.go new file mode 100644 index 000000000..63ee5feb2 --- /dev/null +++ b/eth/catalyst/tester.go @@ -0,0 +1,100 @@ +// Copyright 2022 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 . + +package catalyst + +import ( + "sync" + "time" + + "github.com/ethereum/go-ethereum/core/beacon" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" +) + +// FullSyncTester is an auxiliary service that allows Geth to perform full sync +// alone without consensus-layer attached. Users must specify a valid block as +// the sync target. This tester can be applied to different networks, no matter +// it's pre-merge or post-merge, but only for full-sync. +type FullSyncTester struct { + api *ConsensusAPI + block *types.Block + closed chan struct{} + wg sync.WaitGroup +} + +// RegisterFullSyncTester registers the full-sync tester service into the node +// stack for launching and stopping the service controlled by node. +func RegisterFullSyncTester(stack *node.Node, backend *eth.Ethereum, block *types.Block) (*FullSyncTester, error) { + cl := &FullSyncTester{ + api: NewConsensusAPI(backend), + block: block, + closed: make(chan struct{}), + } + stack.RegisterLifecycle(cl) + return cl, nil +} + +// Start launches the full-sync tester by spinning up a background thread +// for keeping firing NewPayload-UpdateForkChoice combos with the provided +// target block, it may or may not trigger the beacon sync depends on if +// there are protocol peers connected. +func (tester *FullSyncTester) Start() error { + tester.wg.Add(1) + go func() { + defer tester.wg.Done() + + ticker := time.NewTicker(time.Second * 5) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + // Don't bother downloader in case it's already syncing. + if tester.api.eth.Downloader().Synchronising() { + continue + } + // Short circuit in case the target block is already stored + // locally. + if tester.api.eth.BlockChain().HasBlock(tester.block.Hash(), tester.block.NumberU64()) { + log.Info("Full-sync target reached", "number", tester.block.NumberU64(), "hash", tester.block.Hash()) + return + } + // Shoot out consensus events in order to trigger syncing. + data := beacon.BlockToExecutableData(tester.block) + tester.api.NewPayloadV1(*data) + tester.api.ForkchoiceUpdatedV1(beacon.ForkchoiceStateV1{ + HeadBlockHash: tester.block.Hash(), + SafeBlockHash: tester.block.Hash(), + FinalizedBlockHash: tester.block.Hash(), + }, nil) + case <-tester.closed: + return + } + } + }() + return nil +} + +// Stop stops the full-sync tester to stop all background activities. +// This function can only be called for one time. +func (tester *FullSyncTester) Stop() error { + close(tester.closed) + tester.wg.Wait() + return nil +} diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 756063393..e9651d041 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -32,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/ethdb" @@ -211,6 +212,10 @@ type Config struct { // OverrideTerminalTotalDifficultyPassed (TODO: remove after the fork) OverrideTerminalTotalDifficultyPassed *bool `toml:",omitempty"` + + // SyncTarget defines the target block of sync. It's only used for + // development purposes. + SyncTarget *types.Block } // CreateConsensusEngine creates a consensus engine for the given chain configuration. diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index 514facde0..a3dcf5a12 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/miner" @@ -63,6 +64,7 @@ func (c Config) MarshalTOML() (interface{}, error) { CheckpointOracle *params.CheckpointOracleConfig `toml:",omitempty"` OverrideTerminalTotalDifficulty *big.Int `toml:",omitempty"` OverrideTerminalTotalDifficultyPassed *bool `toml:",omitempty"` + FullSyncTarget *types.Block } var enc Config enc.Genesis = c.Genesis @@ -109,6 +111,7 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.CheckpointOracle = c.CheckpointOracle enc.OverrideTerminalTotalDifficulty = c.OverrideTerminalTotalDifficulty enc.OverrideTerminalTotalDifficultyPassed = c.OverrideTerminalTotalDifficultyPassed + enc.FullSyncTarget = c.SyncTarget return &enc, nil } @@ -159,6 +162,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { CheckpointOracle *params.CheckpointOracleConfig `toml:",omitempty"` OverrideTerminalTotalDifficulty *big.Int `toml:",omitempty"` OverrideTerminalTotalDifficultyPassed *bool `toml:",omitempty"` + FullSyncTarget *types.Block } var dec Config if err := unmarshal(&dec); err != nil { @@ -296,5 +300,8 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.OverrideTerminalTotalDifficultyPassed != nil { c.OverrideTerminalTotalDifficultyPassed = dec.OverrideTerminalTotalDifficultyPassed } + if dec.FullSyncTarget != nil { + c.SyncTarget = dec.FullSyncTarget + } return nil }