Skip to content

Commit 0c312be

Browse files
feat(clique): allow shadowforking a clique network (#828) (#995)
* feat(clique): allow shadowforking a clique network (#828) * minor fixes * fix `TestShadowFork` --------- Co-authored-by: Ömer Faruk Irmak <[email protected]>
1 parent d46e37b commit 0c312be

File tree

13 files changed

+265
-17
lines changed

13 files changed

+265
-17
lines changed

cmd/geth/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ var (
152152
utils.L1DeploymentBlockFlag,
153153
utils.CircuitCapacityCheckEnabledFlag,
154154
utils.RollupVerifyEnabledFlag,
155+
utils.ShadowforkPeersFlag,
155156
}, utils.NetworkFlags, utils.DatabaseFlags)
156157

157158
rpcFlags = []cli.Flag{

cmd/utils/flags.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,12 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server.
10111011
Name: "rpc.getlogs.maxrange",
10121012
Usage: "Limit max fetched block range for `eth_getLogs` method",
10131013
}
1014+
1015+
// Shadowfork peers
1016+
ShadowforkPeersFlag = &cli.StringSliceFlag{
1017+
Name: "net.shadowforkpeers",
1018+
Usage: "peer ids of shadow fork peers",
1019+
}
10141020
)
10151021

10161022
var (
@@ -1811,6 +1817,10 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
18111817
setCircuitCapacityCheck(ctx, cfg)
18121818
setEnableRollupVerify(ctx, cfg)
18131819
setMaxBlockRange(ctx, cfg)
1820+
if ctx.IsSet(ShadowforkPeersFlag.Name) {
1821+
cfg.ShadowForkPeerIDs = ctx.StringSlice(ShadowforkPeersFlag.Name)
1822+
log.Info("Shadow fork peers", "ids", cfg.ShadowForkPeerIDs)
1823+
}
18141824

18151825
// Cap the cache allowance and tune the garbage collector
18161826
mem, err := gopsutil.VirtualMemory()

consensus/clique/clique.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ var (
6868

6969
diffInTurn = big.NewInt(2) // Block difficulty for in-turn signatures
7070
diffNoTurn = big.NewInt(1) // Block difficulty for out-of-turn signatures
71+
72+
diffShadowFork = diffNoTurn
7173
)
7274

7375
// 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 {
195197
if conf.Epoch == 0 {
196198
conf.Epoch = epochLength
197199
}
200+
198201
// Allocate the snapshot caches and create the engine
199202
recents := lru.NewCache[common.Hash, *Snapshot](inmemorySnapshots)
200203
signatures := lru.NewCache[common.Hash, common.Address](inmemorySignatures)
@@ -291,7 +294,7 @@ func (c *Clique) verifyHeader(chain consensus.ChainHeaderReader, header *types.H
291294
}
292295
// Ensure that the block's difficulty is meaningful (may not be correct at this point)
293296
if number > 0 {
294-
if header.Difficulty == nil || (header.Difficulty.Cmp(diffInTurn) != 0 && header.Difficulty.Cmp(diffNoTurn) != 0) {
297+
if header.Difficulty == nil || (header.Difficulty.Cmp(diffInTurn) != 0 && header.Difficulty.Cmp(diffNoTurn) != 0 && header.Difficulty.Cmp(diffShadowFork) != 0) {
295298
return errInvalidDifficulty
296299
}
297300
}
@@ -376,6 +379,14 @@ func (c *Clique) snapshot(chain consensus.ChainHeaderReader, number uint64, hash
376379
snap *Snapshot
377380
)
378381
for snap == nil {
382+
if c.config.ShadowForkHeight > 0 && number == c.config.ShadowForkHeight {
383+
c.signatures.Purge()
384+
c.recents.Purge()
385+
c.proposals = make(map[common.Address]bool)
386+
snap = newSnapshot(c.config, c.signatures, number, hash, []common.Address{c.config.ShadowForkSigner})
387+
break
388+
}
389+
379390
// If an in-memory snapshot was found, use that
380391
if s, ok := c.recents.Get(hash); ok {
381392
snap = s
@@ -486,11 +497,8 @@ func (c *Clique) verifySeal(snap *Snapshot, header *types.Header, parents []*typ
486497
}
487498
// Ensure that the difficulty corresponds to the turn-ness of the signer
488499
if !c.fakeDiff {
489-
inturn := snap.inturn(header.Number.Uint64(), signer)
490-
if inturn && header.Difficulty.Cmp(diffInTurn) != 0 {
491-
return errWrongDifficulty
492-
}
493-
if !inturn && header.Difficulty.Cmp(diffNoTurn) != 0 {
500+
expected := c.calcDifficulty(snap, signer)
501+
if header.Difficulty.Cmp(expected) != 0 {
494502
return errWrongDifficulty
495503
}
496504
}
@@ -535,7 +543,7 @@ func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header
535543
c.lock.RUnlock()
536544

537545
// Set the correct difficulty
538-
header.Difficulty = calcDifficulty(snap, signer)
546+
header.Difficulty = c.calcDifficulty(snap, signer)
539547

540548
// Ensure the extra data has all its components
541549
if len(header.Extra) < extraVanity {
@@ -683,10 +691,14 @@ func (c *Clique) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64,
683691
c.lock.RLock()
684692
signer := c.signer
685693
c.lock.RUnlock()
686-
return calcDifficulty(snap, signer)
694+
return c.calcDifficulty(snap, signer)
687695
}
688696

689-
func calcDifficulty(snap *Snapshot, signer common.Address) *big.Int {
697+
func (c *Clique) calcDifficulty(snap *Snapshot, signer common.Address) *big.Int {
698+
if c.config.ShadowForkHeight > 0 && snap.Number >= c.config.ShadowForkHeight {
699+
// if we are past shadow fork point, set a low difficulty so that mainnet nodes don't try to switch to forked chain
700+
return new(big.Int).Set(diffShadowFork)
701+
}
690702
if snap.inturn(snap.Number+1, signer) {
691703
return new(big.Int).Set(diffInTurn)
692704
}

consensus/clique/clique_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
package clique
1818

1919
import (
20+
"bytes"
2021
"math/big"
22+
"strings"
2123
"testing"
2224

2325
"github.com/scroll-tech/go-ethereum/common"
@@ -27,6 +29,7 @@ import (
2729
"github.com/scroll-tech/go-ethereum/core/vm"
2830
"github.com/scroll-tech/go-ethereum/crypto"
2931
"github.com/scroll-tech/go-ethereum/params"
32+
"github.com/scroll-tech/go-ethereum/trie"
3033
)
3134

3235
// 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) {
123126
t.Errorf("have %x, want %x", have, want)
124127
}
125128
}
129+
130+
func TestShadowFork(t *testing.T) {
131+
engineConf := *params.AllCliqueProtocolChanges.Clique
132+
engineConf.Epoch = 2
133+
forkedEngineConf := engineConf
134+
forkedEngineConf.ShadowForkHeight = 3
135+
shadowForkKey, _ := crypto.HexToECDSA(strings.Repeat("11", 32))
136+
shadowForkAddr := crypto.PubkeyToAddress(shadowForkKey.PublicKey)
137+
forkedEngineConf.ShadowForkSigner = shadowForkAddr
138+
139+
// Initialize a Clique chain with a single signer
140+
var (
141+
db = rawdb.NewMemoryDatabase()
142+
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
143+
addr = crypto.PubkeyToAddress(key.PublicKey)
144+
engine = New(&engineConf, db)
145+
signer = new(types.HomesteadSigner)
146+
forkedEngine = New(&forkedEngineConf, db)
147+
)
148+
genspec := &core.Genesis{
149+
Config: params.AllCliqueProtocolChanges,
150+
ExtraData: make([]byte, extraVanity+common.AddressLength+extraSeal),
151+
Alloc: map[common.Address]core.GenesisAccount{
152+
addr: {Balance: big.NewInt(10000000000000000)},
153+
},
154+
BaseFee: big.NewInt(params.InitialBaseFee),
155+
}
156+
copy(genspec.ExtraData[extraVanity:], addr[:])
157+
genesis := genspec.MustCommit(db, trie.NewDatabase(db, trie.HashDefaults))
158+
159+
// Generate a batch of blocks, each properly signed
160+
chain, _ := core.NewBlockChain(db, nil, genspec, nil, engine, vm.Config{}, nil, nil)
161+
defer chain.Stop()
162+
163+
forkedChain, _ := core.NewBlockChain(db, nil, genspec, nil, forkedEngine, vm.Config{}, nil, nil)
164+
defer forkedChain.Stop()
165+
166+
blocks, _ := core.GenerateChain(params.AllCliqueProtocolChanges, genesis, forkedEngine, db, 16, func(i int, block *core.BlockGen) {
167+
// The chain maker doesn't have access to a chain, so the difficulty will be
168+
// lets unset (nil). Set it here to the correct value.
169+
if block.Number().Uint64() > forkedEngineConf.ShadowForkHeight {
170+
block.SetDifficulty(diffShadowFork)
171+
} else {
172+
block.SetDifficulty(diffInTurn)
173+
}
174+
175+
tx, err := types.SignTx(types.NewTransaction(block.TxNonce(addr), common.Address{0x00}, new(big.Int), params.TxGas, block.BaseFee(), nil), signer, key)
176+
if err != nil {
177+
panic(err)
178+
}
179+
block.AddTxWithChain(chain, tx)
180+
})
181+
for i, block := range blocks {
182+
header := block.Header()
183+
if i > 0 {
184+
header.ParentHash = blocks[i-1].Hash()
185+
}
186+
187+
signingAddr, signingKey := addr, key
188+
if header.Number.Uint64() > forkedEngineConf.ShadowForkHeight {
189+
// start signing with shadow fork authority key
190+
signingAddr, signingKey = shadowForkAddr, shadowForkKey
191+
}
192+
193+
header.Extra = make([]byte, extraVanity)
194+
if header.Number.Uint64()%engineConf.Epoch == 0 {
195+
header.Extra = append(header.Extra, signingAddr.Bytes()...)
196+
}
197+
header.Extra = append(header.Extra, bytes.Repeat([]byte{0}, extraSeal)...)
198+
199+
sig, _ := crypto.Sign(SealHash(header).Bytes(), signingKey)
200+
copy(header.Extra[len(header.Extra)-extraSeal:], sig)
201+
blocks[i] = block.WithSeal(header)
202+
}
203+
204+
if _, err := chain.InsertChain(blocks); err == nil {
205+
t.Fatalf("should've failed to insert some blocks to canonical chain")
206+
}
207+
if chain.CurrentHeader().Number.Uint64() != forkedEngineConf.ShadowForkHeight {
208+
t.Fatalf("unexpected canonical chain height")
209+
}
210+
if _, err := forkedChain.InsertChain(blocks); err != nil {
211+
t.Fatalf("failed to insert blocks to forked chain: %v %d", err, forkedChain.CurrentHeader().Number)
212+
}
213+
if forkedChain.CurrentHeader().Number.Uint64() != uint64(len(blocks)) {
214+
t.Fatalf("unexpected forked chain height")
215+
}
216+
}

consensus/misc/eip1559/eip1559_scroll.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ func VerifyEIP1559Header(config *params.ChainConfig, parent, header *types.Heade
5252

5353
// CalcBaseFee calculates the basefee of the header.
5454
func CalcBaseFee(config *params.ChainConfig, parent *types.Header, parentL1BaseFee *big.Int) *big.Int {
55+
if config.Clique != nil && config.Clique.ShadowForkHeight != 0 && parent.Number.Uint64() >= config.Clique.ShadowForkHeight {
56+
return big.NewInt(10000000) // 0.01 Gwei
57+
}
5558
l2SequencerFee := big.NewInt(1000000) // 0.001 Gwei
5659
provingFee := big.NewInt(33700000) // 0.0337 Gwei
5760

eth/backend.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,8 @@ func New(stack *node.Node, config *ethconfig.Config, l1Client sync_service.EthCl
267267
BloomCache: uint64(cacheLimit),
268268
EventMux: eth.eventMux,
269269
RequiredBlocks: config.RequiredBlocks,
270+
271+
ShadowForkPeerIDs: config.ShadowForkPeerIDs,
270272
}); err != nil {
271273
return nil, err
272274
}

eth/ethconfig/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ type Config struct {
178178

179179
// Max block range for eth_getLogs api method
180180
MaxBlockRange int64
181+
182+
// List of peer ids that take part in the shadow-fork
183+
ShadowForkPeerIDs []string
181184
}
182185

183186
// CreateConsensusEngine creates a consensus engine for the given chain config.

eth/handler.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"errors"
2121
"math"
2222
"math/big"
23+
"slices"
2324
"sync"
2425
"sync/atomic"
2526
"time"
@@ -93,6 +94,8 @@ type handlerConfig struct {
9394
BloomCache uint64 // Megabytes to alloc for snap sync bloom
9495
EventMux *event.TypeMux // Legacy event mux, deprecate for `feed`
9596
RequiredBlocks map[uint64]common.Hash // Hard coded map of required block hashes for sync challenges
97+
98+
ShadowForkPeerIDs []string // List of peer ids that take part in the shadow-fork
9699
}
97100

98101
type handler struct {
@@ -128,6 +131,8 @@ type handler struct {
128131

129132
handlerStartCh chan struct{}
130133
handlerDoneCh chan struct{}
134+
135+
shadowForkPeerIDs []string
131136
}
132137

133138
// newHandler returns a handler for all Ethereum chain management protocol.
@@ -149,6 +154,8 @@ func newHandler(config *handlerConfig) (*handler, error) {
149154
quitSync: make(chan struct{}),
150155
handlerDoneCh: make(chan struct{}),
151156
handlerStartCh: make(chan struct{}),
157+
158+
shadowForkPeerIDs: config.ShadowForkPeerIDs,
152159
}
153160
if config.Sync == downloader.FullSync {
154161
// Currently in Scroll we only support full sync,
@@ -269,7 +276,13 @@ func newHandler(config *handlerConfig) (*handler, error) {
269276
}
270277
return h.chain.InsertChain(blocks)
271278
}
272-
h.blockFetcher = fetcher.NewBlockFetcher(false, nil, h.chain.GetBlockByHash, validator, h.BroadcastBlock, heighter, nil, inserter, h.removePeer)
279+
280+
fetcherDropPeerFunc := h.removePeer
281+
// If we are shadowforking, don't drop peers.
282+
if config.ShadowForkPeerIDs != nil {
283+
fetcherDropPeerFunc = func(id string) {}
284+
}
285+
h.blockFetcher = fetcher.NewBlockFetcher(false, nil, h.chain.GetBlockByHash, validator, h.BroadcastBlock, heighter, nil, inserter, fetcherDropPeerFunc)
273286

274287
fetchTx := func(peer string, hashes []common.Hash) error {
275288
p := h.peers.peer(peer)
@@ -396,7 +409,9 @@ func (h *handler) runEthPeer(peer *eth.Peer, handler eth.Handler) error {
396409

397410
// Propagate existing transactions. new transactions appearing
398411
// after this will be sent via broadcasts.
399-
h.syncTransactions(peer)
412+
if h.shadowForkPeerIDs == nil || slices.Contains(h.shadowForkPeerIDs, peer.ID()) {
413+
h.syncTransactions(peer)
414+
}
400415

401416
// Create a notification channel for pending requests if the peer goes down
402417
dead := make(chan struct{})
@@ -567,7 +582,7 @@ func (h *handler) BroadcastBlock(block *types.Block, propagate bool) {
567582
}
568583
}
569584
hash := block.Hash()
570-
peers := h.peers.peersWithoutBlock(hash)
585+
peers := onlyShadowForkPeers(h.shadowForkPeerIDs, h.peers.peersWithoutBlock(hash))
571586

572587
// If propagation is requested, send to a subset of the peer
573588
if propagate {
@@ -620,7 +635,7 @@ func (h *handler) BroadcastTransactions(txs types.Transactions) {
620635
continue
621636
}
622637

623-
peers := h.peers.peersWithoutTransaction(tx.Hash())
638+
peers := onlyShadowForkPeers(h.shadowForkPeerIDs, h.peers.peersWithoutTransaction(tx.Hash()))
624639

625640
var numDirect int
626641
switch {
@@ -695,3 +710,16 @@ func (h *handler) enableSyncedFeatures() {
695710
h.chain.TrieDB().SetBufferSize(pathdb.DefaultBufferSize)
696711
}
697712
}
713+
714+
// onlyShadowForkPeers filters out peers that are not part of the shadow fork
715+
func onlyShadowForkPeers[peerT interface {
716+
ID() string
717+
}](shadowForkPeerIDs []string, peers []peerT) []peerT {
718+
if shadowForkPeerIDs == nil {
719+
return peers
720+
}
721+
722+
return slices.DeleteFunc(peers, func(peer peerT) bool {
723+
return !slices.Contains(shadowForkPeerIDs, peer.ID())
724+
})
725+
}

0 commit comments

Comments
 (0)