diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 8ba46d38fe84..50f6ceda49b1 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -169,6 +169,7 @@ var ( utils.L1DeploymentBlockFlag, utils.CircuitCapacityCheckEnabledFlag, utils.RollupVerifyEnabledFlag, + utils.ShadowforkPeersFlag, } rpcFlags = []cli.Flag{ diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index 1fa00a384e2b..26818ddfd14d 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -237,6 +237,7 @@ var AppHelpFlagGroups = []flags.FlagGroup{ utils.BloomFilterSizeFlag, cli.HelpFlag, utils.CatalystFlag, + utils.ShadowforkPeersFlag, }, }, } diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index b547fe25204e..07605e42a839 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -858,6 +858,12 @@ var ( Name: "rpc.getlogs.maxrange", Usage: "Limit max fetched block range for `eth_getLogs` method", } + + // Shadowfork peers + ShadowforkPeersFlag = cli.StringSliceFlag{ + Name: "net.shadowforkpeers", + Usage: "peer ids of shadow fork peers", + } ) // MakeDataDir retrieves the currently requested data directory, terminating @@ -1651,6 +1657,10 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { setCircuitCapacityCheck(ctx, cfg) setEnableRollupVerify(ctx, cfg) setMaxBlockRange(ctx, cfg) + if ctx.GlobalIsSet(ShadowforkPeersFlag.Name) { + cfg.ShadowForkPeerIDs = ctx.GlobalStringSlice(ShadowforkPeersFlag.Name) + log.Info("Shadow fork peers", "ids", cfg.ShadowForkPeerIDs) + } // Cap the cache allowance and tune the garbage collector mem, err := gopsutil.VirtualMemory() diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index bd92b525b980..448cf75efab0 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -66,8 +66,9 @@ var ( uncleHash = types.CalcUncleHash(nil) // Always Keccak256(RLP([])) as uncles are meaningless outside of PoW. - diffInTurn = big.NewInt(2) // Block difficulty for in-turn signatures - diffNoTurn = big.NewInt(1) // Block difficulty for out-of-turn signatures + diffInTurn = big.NewInt(2) // Block difficulty for in-turn signatures + diffNoTurn = big.NewInt(1) // Block difficulty for out-of-turn signatures + diffShadowFork = diffNoTurn ) // Various error messages to mark blocks invalid. These should be private to @@ -195,6 +196,7 @@ func New(config *params.CliqueConfig, db ethdb.Database) *Clique { if conf.Epoch == 0 { conf.Epoch = epochLength } + // Allocate the snapshot caches and create the engine recents, _ := lru.NewARC(inmemorySnapshots) signatures, _ := lru.NewARC(inmemorySignatures) @@ -291,7 +293,7 @@ func (c *Clique) verifyHeader(chain consensus.ChainHeaderReader, header *types.H } // Ensure that the block's difficulty is meaningful (may not be correct at this point) if number > 0 { - if header.Difficulty == nil || (header.Difficulty.Cmp(diffInTurn) != 0 && header.Difficulty.Cmp(diffNoTurn) != 0) { + if header.Difficulty == nil || (header.Difficulty.Cmp(diffInTurn) != 0 && header.Difficulty.Cmp(diffNoTurn) != 0 && header.Difficulty.Cmp(diffShadowFork) != 0) { return errInvalidDifficulty } } @@ -375,6 +377,14 @@ func (c *Clique) snapshot(chain consensus.ChainHeaderReader, number uint64, hash snap *Snapshot ) for snap == nil { + if c.config.ShadowForkHeight > 0 && number == c.config.ShadowForkHeight { + c.signatures.Purge() + c.recents.Purge() + c.proposals = make(map[common.Address]bool) + snap = newSnapshot(c.config, c.signatures, number, hash, []common.Address{c.config.ShadowForkSigner}) + break + } + // If an in-memory snapshot was found, use that if s, ok := c.recents.Get(hash); ok { snap = s.(*Snapshot) @@ -485,11 +495,8 @@ func (c *Clique) verifySeal(snap *Snapshot, header *types.Header, parents []*typ } // Ensure that the difficulty corresponds to the turn-ness of the signer if !c.fakeDiff { - inturn := snap.inturn(header.Number.Uint64(), signer) - if inturn && header.Difficulty.Cmp(diffInTurn) != 0 { - return errWrongDifficulty - } - if !inturn && header.Difficulty.Cmp(diffNoTurn) != 0 { + expected := c.calcDifficulty(snap, signer) + if header.Difficulty.Cmp(expected) != 0 { return errWrongDifficulty } } @@ -534,7 +541,7 @@ func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header c.lock.RUnlock() // Set the correct difficulty - header.Difficulty = calcDifficulty(snap, signer) + header.Difficulty = c.calcDifficulty(snap, signer) // Ensure the extra data has all its components if len(header.Extra) < extraVanity { @@ -678,10 +685,14 @@ func (c *Clique) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, c.lock.RLock() signer := c.signer c.lock.RUnlock() - return calcDifficulty(snap, signer) + return c.calcDifficulty(snap, signer) } -func calcDifficulty(snap *Snapshot, signer common.Address) *big.Int { +func (c *Clique) calcDifficulty(snap *Snapshot, signer common.Address) *big.Int { + if c.config.ShadowForkHeight > 0 && snap.Number >= c.config.ShadowForkHeight { + // if we are past shadow fork point, set a low difficulty so that mainnet nodes don't try to switch to forked chain + return new(big.Int).Set(diffShadowFork) + } if snap.inturn(snap.Number+1, signer) { return new(big.Int).Set(diffInTurn) } diff --git a/consensus/clique/clique_test.go b/consensus/clique/clique_test.go index 4faa93650175..67e0e8c620f7 100644 --- a/consensus/clique/clique_test.go +++ b/consensus/clique/clique_test.go @@ -17,7 +17,9 @@ package clique import ( + "bytes" "math/big" + "strings" "testing" "github.com/scroll-tech/go-ethereum/common" @@ -125,3 +127,90 @@ func TestSealHash(t *testing.T) { t.Errorf("have %x, want %x", have, want) } } + +func TestShadowFork(t *testing.T) { + engineConf := *params.AllCliqueProtocolChanges.Clique + engineConf.Epoch = 2 + forkedEngineConf := engineConf + forkedEngineConf.ShadowForkHeight = 3 + shadowForkKey, _ := crypto.HexToECDSA(strings.Repeat("11", 32)) + shadowForkAddr := crypto.PubkeyToAddress(shadowForkKey.PublicKey) + forkedEngineConf.ShadowForkSigner = shadowForkAddr + + // Initialize a Clique chain with a single signer + var ( + db = rawdb.NewMemoryDatabase() + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + addr = crypto.PubkeyToAddress(key.PublicKey) + engine = New(&engineConf, db) + signer = new(types.HomesteadSigner) + forkedEngine = New(&forkedEngineConf, db) + ) + genspec := &core.Genesis{ + ExtraData: make([]byte, extraVanity+common.AddressLength+extraSeal), + Alloc: map[common.Address]core.GenesisAccount{ + addr: {Balance: big.NewInt(10000000000000000)}, + }, + BaseFee: big.NewInt(params.InitialBaseFee), + } + copy(genspec.ExtraData[extraVanity:], addr[:]) + genesis := genspec.MustCommit(db) + + // Generate a batch of blocks, each properly signed + chain, _ := core.NewBlockChain(db, nil, params.AllCliqueProtocolChanges, engine, vm.Config{}, nil, nil) + defer chain.Stop() + + forkedChain, _ := core.NewBlockChain(db, nil, params.AllCliqueProtocolChanges, forkedEngine, vm.Config{}, nil, nil) + defer forkedChain.Stop() + + blocks, _ := core.GenerateChain(params.AllCliqueProtocolChanges, genesis, forkedEngine, db, 16, func(i int, block *core.BlockGen) { + // The chain maker doesn't have access to a chain, so the difficulty will be + // lets unset (nil). Set it here to the correct value. + if block.Number().Uint64() > forkedEngineConf.ShadowForkHeight { + block.SetDifficulty(diffShadowFork) + } else { + block.SetDifficulty(diffInTurn) + } + + tx, err := types.SignTx(types.NewTransaction(block.TxNonce(addr), common.Address{0x00}, new(big.Int), params.TxGas, block.BaseFee(), nil), signer, key) + if err != nil { + panic(err) + } + block.AddTxWithChain(chain, tx) + }) + for i, block := range blocks { + header := block.Header() + if i > 0 { + header.ParentHash = blocks[i-1].Hash() + } + + signingAddr, signingKey := addr, key + if header.Number.Uint64() > forkedEngineConf.ShadowForkHeight { + // start signing with shadow fork authority key + signingAddr, signingKey = shadowForkAddr, shadowForkKey + } + + header.Extra = make([]byte, extraVanity) + if header.Number.Uint64()%engineConf.Epoch == 0 { + header.Extra = append(header.Extra, signingAddr.Bytes()...) + } + header.Extra = append(header.Extra, bytes.Repeat([]byte{0}, extraSeal)...) + + sig, _ := crypto.Sign(SealHash(header).Bytes(), signingKey) + copy(header.Extra[len(header.Extra)-extraSeal:], sig) + blocks[i] = block.WithSeal(header) + } + + if _, err := chain.InsertChain(blocks); err == nil { + t.Fatalf("should've failed to insert some blocks to canonical chain") + } + if chain.CurrentHeader().Number.Uint64() != forkedEngineConf.ShadowForkHeight { + t.Fatalf("unexpected canonical chain height") + } + if _, err := forkedChain.InsertChain(blocks); err != nil { + t.Fatalf("failed to insert blocks to forked chain: %v %d", err, forkedChain.CurrentHeader().Number) + } + if forkedChain.CurrentHeader().Number.Uint64() != uint64(len(blocks)) { + t.Fatalf("unexpected forked chain height") + } +} diff --git a/consensus/misc/eip1559.go b/consensus/misc/eip1559.go index 1e6b54389f86..3d6458547a8f 100644 --- a/consensus/misc/eip1559.go +++ b/consensus/misc/eip1559.go @@ -51,6 +51,9 @@ func VerifyEip1559Header(config *params.ChainConfig, parent, header *types.Heade // CalcBaseFee calculates the basefee of the header. func CalcBaseFee(config *params.ChainConfig, parent *types.Header, parentL1BaseFee *big.Int) *big.Int { + if config.Clique != nil && config.Clique.ShadowForkHeight != 0 && parent.Number.Uint64() >= config.Clique.ShadowForkHeight { + return big.NewInt(10000000) // 0.01 Gwei + } l2SequencerFee := big.NewInt(1000000) // 0.001 Gwei provingFee := big.NewInt(47700000) // 0.0477 Gwei diff --git a/eth/backend.go b/eth/backend.go index 280e956b619e..26e24293535f 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -239,15 +239,16 @@ func New(stack *node.Node, config *ethconfig.Config, l1Client sync_service.EthCl checkpoint = params.TrustedCheckpoints[genesisHash] } if eth.handler, err = newHandler(&handlerConfig{ - Database: chainDb, - Chain: eth.blockchain, - TxPool: eth.txPool, - Network: config.NetworkId, - Sync: config.SyncMode, - BloomCache: uint64(cacheLimit), - EventMux: eth.eventMux, - Checkpoint: checkpoint, - Whitelist: config.Whitelist, + Database: chainDb, + Chain: eth.blockchain, + TxPool: eth.txPool, + Network: config.NetworkId, + Sync: config.SyncMode, + BloomCache: uint64(cacheLimit), + EventMux: eth.eventMux, + Checkpoint: checkpoint, + Whitelist: config.Whitelist, + ShadowForkPeerIDs: config.ShadowForkPeerIDs, }); err != nil { return nil, err } diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 4205c040873e..ff46a725a443 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -214,6 +214,9 @@ type Config struct { // Max block range for eth_getLogs api method MaxBlockRange int64 + + // List of peer ids that take part in the shadow-fork + ShadowForkPeerIDs []string } // CreateConsensusEngine creates a consensus engine for the given chain configuration. diff --git a/eth/handler.go b/eth/handler.go index 91adb2a62b5b..6939a4d6823b 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -20,6 +20,7 @@ import ( "errors" "math" "math/big" + "slices" "sync" "sync/atomic" "time" @@ -76,15 +77,16 @@ type txPool interface { // handlerConfig is the collection of initialization parameters to create a full // node network handler. type handlerConfig struct { - Database ethdb.Database // Database for direct sync insertions - Chain *core.BlockChain // Blockchain to serve data from - TxPool txPool // Transaction pool to propagate from - Network uint64 // Network identifier to adfvertise - Sync downloader.SyncMode // Whether to fast or full sync - BloomCache uint64 // Megabytes to alloc for fast sync bloom - EventMux *event.TypeMux // Legacy event mux, deprecate for `feed` - Checkpoint *params.TrustedCheckpoint // Hard coded checkpoint for sync challenges - Whitelist map[uint64]common.Hash // Hard coded whitelist for sync challenged + Database ethdb.Database // Database for direct sync insertions + Chain *core.BlockChain // Blockchain to serve data from + TxPool txPool // Transaction pool to propagate from + Network uint64 // Network identifier to adfvertise + Sync downloader.SyncMode // Whether to fast or full sync + BloomCache uint64 // Megabytes to alloc for fast sync bloom + EventMux *event.TypeMux // Legacy event mux, deprecate for `feed` + Checkpoint *params.TrustedCheckpoint // Hard coded checkpoint for sync challenges + Whitelist map[uint64]common.Hash // Hard coded whitelist for sync challenged + ShadowForkPeerIDs []string // List of peer ids that take part in the shadow-fork } type handler struct { @@ -122,6 +124,8 @@ type handler struct { chainSync *chainSyncer wg sync.WaitGroup peerWG sync.WaitGroup + + shadowForkPeerIDs []string } // newHandler returns a handler for all Ethereum chain management protocol. @@ -131,15 +135,16 @@ func newHandler(config *handlerConfig) (*handler, error) { config.EventMux = new(event.TypeMux) // Nicety initialization for tests } h := &handler{ - networkID: config.Network, - forkFilter: forkid.NewFilter(config.Chain), - eventMux: config.EventMux, - database: config.Database, - txpool: config.TxPool, - chain: config.Chain, - peers: newPeerSet(), - whitelist: config.Whitelist, - quitSync: make(chan struct{}), + networkID: config.Network, + forkFilter: forkid.NewFilter(config.Chain), + eventMux: config.EventMux, + database: config.Database, + txpool: config.TxPool, + chain: config.Chain, + peers: newPeerSet(), + whitelist: config.Whitelist, + quitSync: make(chan struct{}), + shadowForkPeerIDs: config.ShadowForkPeerIDs, } if config.Sync == downloader.FullSync { // The database seems empty as the current block is the genesis. Yet the fast @@ -182,6 +187,7 @@ func newHandler(config *handlerConfig) (*handler, error) { if atomic.LoadUint32(&h.fastSync) == 1 && atomic.LoadUint32(&h.snapSync) == 0 { h.stateBloom = trie.NewSyncBloom(config.BloomCache, config.Database) } + h.downloader = downloader.New(h.checkpointNumber, config.Database, h.stateBloom, h.eventMux, h.chain, nil, h.removePeer) // Construct the fetcher (short sync) @@ -217,7 +223,13 @@ func newHandler(config *handlerConfig) (*handler, error) { } return n, err } - h.blockFetcher = fetcher.NewBlockFetcher(false, nil, h.chain.GetBlockByHash, validator, h.BroadcastBlock, heighter, nil, inserter, h.removePeer) + + fetcherDropPeerFunc := h.removePeer + // If we are shadowforking, don't drop peers. + if config.ShadowForkPeerIDs != nil { + fetcherDropPeerFunc = func(id string) {} + } + h.blockFetcher = fetcher.NewBlockFetcher(false, nil, h.chain.GetBlockByHash, validator, h.BroadcastBlock, heighter, nil, inserter, fetcherDropPeerFunc) fetchTx := func(peer string, hashes []common.Hash) error { p := h.peers.peer(peer) @@ -306,7 +318,9 @@ func (h *handler) runEthPeer(peer *eth.Peer, handler eth.Handler) error { // Propagate existing transactions. new transactions appearing // after this will be sent via broadcasts. - h.syncTransactions(peer) + if h.shadowForkPeerIDs == nil || slices.Contains(h.shadowForkPeerIDs, peer.ID()) { + h.syncTransactions(peer) + } // If we have a trusted CHT, reject all peers below that (avoid fast sync eclipse) if h.checkpointHash != (common.Hash{}) { @@ -433,7 +447,7 @@ func (h *handler) Stop() { // will only announce its availability (depending what's requested). func (h *handler) BroadcastBlock(block *types.Block, propagate bool) { hash := block.Hash() - peers := h.peers.peersWithoutBlock(hash) + peers := onlyShadowForkPeers(h.shadowForkPeerIDs, h.peers.peersWithoutBlock(hash)) // If propagation is requested, send to a subset of the peer if propagate { @@ -483,7 +497,7 @@ func (h *handler) BroadcastTransactions(txs types.Transactions) { if tx.IsL1MessageTx() { continue } - peers := h.peers.peersWithoutTransaction(tx.Hash()) + peers := onlyShadowForkPeers(h.shadowForkPeerIDs, h.peers.peersWithoutTransaction(tx.Hash())) // Send the tx unconditionally to a subset of our peers numDirect := int(math.Sqrt(float64(len(peers)))) for _, peer := range peers[:numDirect] { @@ -533,3 +547,16 @@ func (h *handler) txBroadcastLoop() { } } } + +// onlyShadowForkPeers filters out peers that are not part of the shadow fork +func onlyShadowForkPeers[peerT interface { + ID() string +}](shadowForkPeerIDs []string, peers []peerT) []peerT { + if shadowForkPeerIDs == nil { + return peers + } + + return slices.DeleteFunc(peers, func(peer peerT) bool { + return !slices.Contains(shadowForkPeerIDs, peer.ID()) + }) +} diff --git a/eth/handler_test.go b/eth/handler_test.go index b9931a4b38e9..54118133aeda 100644 --- a/eth/handler_test.go +++ b/eth/handler_test.go @@ -20,6 +20,9 @@ import ( "math/big" "sort" "sync" + "testing" + + "github.com/stretchr/testify/require" "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/consensus/ethash" @@ -168,3 +171,82 @@ func (b *testHandler) close() { b.handler.Stop() b.chain.Stop() } + +type testPeer struct { + id string +} + +func (p testPeer) ID() string { + return p.id +} + +func TestOnlyShadowForkPeers(t *testing.T) { + + tests := map[string]struct { + shadowForkPeerIDs []string + peers []testPeer + expectedPeerIDs []string + }{ + "nil peers": { + shadowForkPeerIDs: nil, + peers: nil, + expectedPeerIDs: []string{}, + }, + "empty peers": { + shadowForkPeerIDs: nil, + peers: []testPeer{}, + expectedPeerIDs: []string{}, + }, + "no fork": { + shadowForkPeerIDs: nil, + peers: []testPeer{ + { + id: "peer1", + }, + { + id: "peer2", + }, + }, + expectedPeerIDs: []string{ + "peer1", + "peer2", + }, + }, + "some shadow fork peers": { + shadowForkPeerIDs: []string{"peer2"}, + peers: []testPeer{ + { + id: "peer1", + }, + { + id: "peer2", + }, + }, + expectedPeerIDs: []string{ + "peer2", + }, + }, + "no shadow fork peers": { + shadowForkPeerIDs: []string{"peer2"}, + peers: []testPeer{ + { + id: "peer1", + }, + { + id: "peer3", + }, + }, + expectedPeerIDs: []string{}, + }, + } + + for desc, test := range tests { + t.Run(desc, func(t *testing.T) { + gotIds := []string{} + for _, peer := range onlyShadowForkPeers(test.shadowForkPeerIDs, test.peers) { + gotIds = append(gotIds, peer.ID()) + } + require.Equal(t, gotIds, test.expectedPeerIDs) + }) + } +} diff --git a/eth/peerset.go b/eth/peerset.go index fae769c3afda..14ee4df43607 100644 --- a/eth/peerset.go +++ b/eth/peerset.go @@ -19,6 +19,7 @@ package eth import ( "errors" "math/big" + "slices" "sync" "github.com/scroll-tech/go-ethereum/common" @@ -231,7 +232,7 @@ func (ps *peerSet) snapLen() int { // peerWithHighestTD retrieves the known peer with the currently highest total // difficulty. -func (ps *peerSet) peerWithHighestTD() *eth.Peer { +func (ps *peerSet) peerWithHighestTD(whileList []string) *eth.Peer { ps.lock.RLock() defer ps.lock.RUnlock() @@ -240,6 +241,9 @@ func (ps *peerSet) peerWithHighestTD() *eth.Peer { bestTd *big.Int ) for _, p := range ps.peers { + if whileList != nil && !slices.Contains(whileList, p.ID()) { + continue + } if _, td := p.Head(); bestPeer == nil || td.Cmp(bestTd) > 0 { bestPeer, bestTd = p.Peer, td } diff --git a/eth/sync.go b/eth/sync.go index 5d5f0327d77d..06584696411f 100644 --- a/eth/sync.go +++ b/eth/sync.go @@ -156,8 +156,18 @@ func (cs *chainSyncer) nextSyncOp() *chainSyncOp { if cs.handler.peers.len() < minPeers { return nil } + + var syncWhiteList []string + chainConfig := cs.handler.chain.Config() + currentHeight := cs.handler.chain.CurrentHeader().Number.Uint64() + if chainConfig.Clique != nil { + shadowForkHeight := chainConfig.Clique.ShadowForkHeight + if shadowForkHeight != 0 && currentHeight >= shadowForkHeight { + syncWhiteList = cs.handler.shadowForkPeerIDs + } + } // We have enough peers, check TD - peer := cs.handler.peers.peerWithHighestTD() + peer := cs.handler.peers.peerWithHighestTD(syncWhiteList) if peer == nil { return nil } diff --git a/params/config.go b/params/config.go index a9ce6a2e33b7..1fcd4c748f80 100644 --- a/params/config.go +++ b/params/config.go @@ -718,9 +718,11 @@ func (c *EthashConfig) String() string { // CliqueConfig is the consensus engine configs for proof-of-authority based sealing. type CliqueConfig struct { - Period uint64 `json:"period"` // Number of seconds between blocks to enforce - Epoch uint64 `json:"epoch"` // Epoch length to reset votes and checkpoint - RelaxedPeriod bool `json:"relaxed_period"` // Relaxes the period to be just an upper bound + Period uint64 `json:"period"` // Number of seconds between blocks to enforce + Epoch uint64 `json:"epoch"` // Epoch length to reset votes and checkpoint + RelaxedPeriod bool `json:"relaxed_period"` // Relaxes the period to be just an upper bound + ShadowForkHeight uint64 `json:"shadow_fork_height"` // Allows shadow forking consensus layer at given height + ShadowForkSigner common.Address `json:"shadow_fork_signer"` // Sets the address to be the authorized signer after the shadow fork } // String implements the stringer interface, returning the consensus engine details. diff --git a/params/version.go b/params/version.go index 8779b0e64665..fe87555ffb66 100644 --- a/params/version.go +++ b/params/version.go @@ -24,7 +24,7 @@ import ( const ( VersionMajor = 5 // Major version component of the current release VersionMinor = 5 // Minor version component of the current release - VersionPatch = 18 // Patch version component of the current release + VersionPatch = 19 // Patch version component of the current release VersionMeta = "mainnet" // Version metadata to append to the version string )