From 0c312be5ca03bb0b725e043c09ec51dea777a4fa Mon Sep 17 00:00:00 2001 From: HAOYUatHZ <37070449+HAOYUatHZ@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:02:50 +1000 Subject: [PATCH] feat(clique): allow shadowforking a clique network (#828) (#995) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(clique): allow shadowforking a clique network (#828) * minor fixes * fix `TestShadowFork` --------- Co-authored-by: Ă–mer Faruk Irmak --- cmd/geth/main.go | 1 + cmd/utils/flags.go | 10 +++ consensus/clique/clique.go | 30 +++++--- consensus/clique/clique_test.go | 91 ++++++++++++++++++++++++ consensus/misc/eip1559/eip1559_scroll.go | 3 + eth/backend.go | 2 + eth/ethconfig/config.go | 3 + eth/handler.go | 36 ++++++++-- eth/handler_test.go | 82 +++++++++++++++++++++ eth/peerset.go | 6 +- eth/sync.go | 12 +++- eth/sync_test.go | 2 +- params/config.go | 4 +- 13 files changed, 265 insertions(+), 17 deletions(-) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 0adc08b7b14d..c0cb3c982a71 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -152,6 +152,7 @@ var ( utils.L1DeploymentBlockFlag, utils.CircuitCapacityCheckEnabledFlag, utils.RollupVerifyEnabledFlag, + utils.ShadowforkPeersFlag, }, utils.NetworkFlags, utils.DatabaseFlags) rpcFlags = []cli.Flag{ diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 25e6f54169b1..9c5ddc95a92b 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1011,6 +1011,12 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. 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", + } ) var ( @@ -1811,6 +1817,10 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { setCircuitCapacityCheck(ctx, cfg) setEnableRollupVerify(ctx, cfg) setMaxBlockRange(ctx, cfg) + if ctx.IsSet(ShadowforkPeersFlag.Name) { + cfg.ShadowForkPeerIDs = ctx.StringSlice(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 8569d97da33f..b20414367c5d 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -68,6 +68,8 @@ var ( 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 +197,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.NewCache[common.Hash, *Snapshot](inmemorySnapshots) signatures := lru.NewCache[common.Hash, common.Address](inmemorySignatures) @@ -291,7 +294,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 } } @@ -376,6 +379,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 @@ -486,11 +497,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 } } @@ -535,7 +543,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 { @@ -683,10 +691,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 280a0f0765e8..89812b6aa3b1 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" @@ -27,6 +29,7 @@ import ( "github.com/scroll-tech/go-ethereum/core/vm" "github.com/scroll-tech/go-ethereum/crypto" "github.com/scroll-tech/go-ethereum/params" + "github.com/scroll-tech/go-ethereum/trie" ) // This test case is a repro of an annoying bug that took us forever to catch. @@ -123,3 +126,91 @@ 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{ + Config: params.AllCliqueProtocolChanges, + 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, trie.NewDatabase(db, trie.HashDefaults)) + + // Generate a batch of blocks, each properly signed + chain, _ := core.NewBlockChain(db, nil, genspec, nil, engine, vm.Config{}, nil, nil) + defer chain.Stop() + + forkedChain, _ := core.NewBlockChain(db, nil, genspec, nil, 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/eip1559_scroll.go b/consensus/misc/eip1559/eip1559_scroll.go index 2cd931666d31..145baf8bbc18 100644 --- a/consensus/misc/eip1559/eip1559_scroll.go +++ b/consensus/misc/eip1559/eip1559_scroll.go @@ -52,6 +52,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(33700000) // 0.0337 Gwei diff --git a/eth/backend.go b/eth/backend.go index 3e7549d675e5..a4c70ff80c02 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -267,6 +267,8 @@ func New(stack *node.Node, config *ethconfig.Config, l1Client sync_service.EthCl BloomCache: uint64(cacheLimit), EventMux: eth.eventMux, RequiredBlocks: config.RequiredBlocks, + + ShadowForkPeerIDs: config.ShadowForkPeerIDs, }); err != nil { return nil, err } diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index ed5d7cebc544..7fecce39c3db 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -178,6 +178,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 config. diff --git a/eth/handler.go b/eth/handler.go index 13901102a033..839a45ac33c0 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -20,6 +20,7 @@ import ( "errors" "math" "math/big" + "slices" "sync" "sync/atomic" "time" @@ -93,6 +94,8 @@ type handlerConfig struct { BloomCache uint64 // Megabytes to alloc for snap sync bloom EventMux *event.TypeMux // Legacy event mux, deprecate for `feed` RequiredBlocks map[uint64]common.Hash // Hard coded map of required block hashes for sync challenges + + ShadowForkPeerIDs []string // List of peer ids that take part in the shadow-fork } type handler struct { @@ -128,6 +131,8 @@ type handler struct { handlerStartCh chan struct{} handlerDoneCh chan struct{} + + shadowForkPeerIDs []string } // newHandler returns a handler for all Ethereum chain management protocol. @@ -149,6 +154,8 @@ func newHandler(config *handlerConfig) (*handler, error) { quitSync: make(chan struct{}), handlerDoneCh: make(chan struct{}), handlerStartCh: make(chan struct{}), + + shadowForkPeerIDs: config.ShadowForkPeerIDs, } if config.Sync == downloader.FullSync { // Currently in Scroll we only support full sync, @@ -269,7 +276,13 @@ func newHandler(config *handlerConfig) (*handler, error) { } return h.chain.InsertChain(blocks) } - 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) @@ -396,7 +409,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) + } // Create a notification channel for pending requests if the peer goes down dead := make(chan struct{}) @@ -567,7 +582,7 @@ 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 { @@ -620,7 +635,7 @@ func (h *handler) BroadcastTransactions(txs types.Transactions) { continue } - peers := h.peers.peersWithoutTransaction(tx.Hash()) + peers := onlyShadowForkPeers(h.shadowForkPeerIDs, h.peers.peersWithoutTransaction(tx.Hash())) var numDirect int switch { @@ -695,3 +710,16 @@ func (h *handler) enableSyncedFeatures() { h.chain.TrieDB().SetBufferSize(pathdb.DefaultBufferSize) } } + +// 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 bee5972717b5..22d382c0fece 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" @@ -183,3 +186,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 754b86a1e2f2..87e22bb3691f 100644 --- a/eth/peerset.go +++ b/eth/peerset.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "math/big" + "slices" "sync" "github.com/scroll-tech/go-ethereum/common" @@ -232,7 +233,7 @@ func (ps *peerSet) snapLen() int { // peerWithHighestTD retrieves the known peer with the currently highest total // difficulty, but below the given PoS switchover threshold. -func (ps *peerSet) peerWithHighestTD() *eth.Peer { +func (ps *peerSet) peerWithHighestTD(whileList []string) *eth.Peer { ps.lock.RLock() defer ps.lock.RUnlock() @@ -241,6 +242,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 19b7470f277a..cd6239040444 100644 --- a/eth/sync.go +++ b/eth/sync.go @@ -162,10 +162,20 @@ 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, pick the one with the highest TD, but avoid going // over the terminal total difficulty. Above that we expect the consensus // clients to direct the chain head to sync to. - peer := cs.handler.peers.peerWithHighestTD() + peer := cs.handler.peers.peerWithHighestTD(syncWhiteList) if peer == nil { return nil } diff --git a/eth/sync_test.go b/eth/sync_test.go index 4ededd04f6b1..bbf5d93fcf01 100644 --- a/eth/sync_test.go +++ b/eth/sync_test.go @@ -88,7 +88,7 @@ func testSnapSyncDisabling(t *testing.T, ethVer uint, snapVer uint) { time.Sleep(250 * time.Millisecond) // Check that snap sync was disabled - op := peerToSyncOp(downloader.SnapSync, empty.handler.peers.peerWithHighestTD()) + op := peerToSyncOp(downloader.SnapSync, empty.handler.peers.peerWithHighestTD([]string{})) if err := empty.handler.doSync(op); err != nil { t.Fatal("sync failed:", err) } diff --git a/params/config.go b/params/config.go index 96ef7372bc7f..22b61484eb9a 100644 --- a/params/config.go +++ b/params/config.go @@ -611,7 +611,9 @@ 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 + 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.