-
Notifications
You must be signed in to change notification settings - Fork 171
/
l1_replica.go
254 lines (225 loc) · 7.31 KB
/
l1_replica.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
package actions
import (
"errors"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/eth/tracers"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-node/testutils"
)
// L1CanonSrc is used to sync L1 from another node.
// The other node always has the canonical chain.
// May be nil if there is nothing to sync from
type L1CanonSrc func(num uint64) *types.Block
// L1Replica is an instrumented in-memory L1 geth node that:
// - can sync from the given canonical L1 blocks source
// - can rewind the chain back (for reorgs)
// - can provide an RPC with mock errors
type L1Replica struct {
log log.Logger
node *node.Node
eth *eth.Ethereum
// L1 evm / chain
l1Chain *core.BlockChain
l1Database ethdb.Database
l1Cfg *core.Genesis
l1Signer types.Signer
failL1RPC func() error // mock error
}
// NewL1Replica constructs a L1Replica starting at the given genesis.
func NewL1Replica(t Testing, log log.Logger, genesis *core.Genesis) *L1Replica {
ethCfg := ðconfig.Config{
NetworkId: genesis.Config.ChainID.Uint64(),
Genesis: genesis,
RollupDisableTxPoolGossip: true,
}
nodeCfg := &node.Config{
Name: "l1-geth",
WSHost: "127.0.0.1",
WSPort: 0,
WSModules: []string{"debug", "admin", "eth", "txpool", "net", "rpc", "web3", "personal"},
HTTPModules: []string{"debug", "admin", "eth", "txpool", "net", "rpc", "web3", "personal"},
DataDir: "", // in-memory
P2P: p2p.Config{
NoDiscovery: true,
NoDial: true,
},
}
n, err := node.New(nodeCfg)
require.NoError(t, err)
t.Cleanup(func() {
_ = n.Close()
})
backend, err := eth.New(n, ethCfg)
require.NoError(t, err)
backend.Merger().FinalizePoS()
n.RegisterAPIs(tracers.APIs(backend.APIBackend))
require.NoError(t, n.Start(), "failed to start L1 geth node")
return &L1Replica{
log: log,
node: n,
eth: backend,
l1Chain: backend.BlockChain(),
l1Database: backend.ChainDb(),
l1Cfg: genesis,
l1Signer: types.LatestSigner(genesis.Config),
failL1RPC: nil,
}
}
// ActL1RewindToParent rewinds the L1 chain to parent block of head
func (s *L1Replica) ActL1RewindToParent(t Testing) {
s.ActL1RewindDepth(1)(t)
}
func (s *L1Replica) ActL1RewindDepth(depth uint64) Action {
return func(t Testing) {
if depth == 0 {
return
}
head := s.l1Chain.CurrentHeader().Number.Uint64()
if head < depth {
t.InvalidAction("cannot rewind L1 past genesis (current: %d, rewind depth: %d)", head, depth)
return
}
finalized := s.l1Chain.CurrentFinalBlock()
if finalized != nil && head < finalized.Number.Uint64()+depth {
t.InvalidAction("cannot rewind head of chain past finalized block %d with rewind depth %d", finalized.Number.Uint64(), depth)
return
}
if err := s.l1Chain.SetHead(head - depth); err != nil {
t.Fatalf("failed to rewind L1 chain to nr %d: %v", head-depth, err)
}
}
}
// ActL1Sync processes the next canonical L1 block,
// or rewinds one block if the canonical block cannot be applied to the head.
func (s *L1Replica) ActL1Sync(canonL1 func(num uint64) *types.Block) Action {
return func(t Testing) {
selfHead := s.l1Chain.CurrentHeader()
n := selfHead.Number.Uint64()
expected := canonL1(n)
if expected == nil || selfHead.Hash() != expected.Hash() {
s.ActL1RewindToParent(t)
return
}
next := canonL1(n + 1)
if next == nil {
t.InvalidAction("already fully synced to head %s (%d), n+1 is not there", selfHead.Hash(), n)
return
}
if next.ParentHash() != selfHead.Hash() {
// canonical chain must be set up wrong - with actions one by one it is not supposed to reorg during a single sync step.
t.Fatalf("canonical L1 source reorged unexpectedly from %s (num %d) to next block %s (parent %s)", n, selfHead.Hash(), next.Hash(), next.ParentHash())
}
_, err := s.l1Chain.InsertChain([]*types.Block{next})
require.NoError(t, err, "L1 replica could not sync next canonical L1 block %s (%d)", next.Hash(), next.NumberU64())
}
}
func (s *L1Replica) CanonL1Chain() func(num uint64) *types.Block {
return s.l1Chain.GetBlockByNumber
}
// ActL1RPCFail makes the next L1 RPC request to this node fail
func (s *L1Replica) ActL1RPCFail(t Testing) {
failed := false
s.failL1RPC = func() error {
if failed {
return nil
}
failed = true
return errors.New("mock L1 RPC error")
}
}
func (s *L1Replica) MockL1RPCErrors(fn func() error) {
s.failL1RPC = fn
}
func (s *L1Replica) EthClient() *ethclient.Client {
cl, _ := s.node.Attach() // never errors
return ethclient.NewClient(cl)
}
func (s *L1Replica) RPCClient() client.RPC {
cl, _ := s.node.Attach() // never errors
return testutils.RPCErrFaker{
RPC: client.NewBaseRPCClient(cl),
ErrFn: func() error {
if s.failL1RPC != nil {
return s.failL1RPC()
} else {
return nil
}
},
}
}
func (s *L1Replica) L1Client(t Testing, cfg *rollup.Config) *sources.L1Client {
l1F, err := sources.NewL1Client(s.RPCClient(), s.log, nil, sources.L1ClientDefaultConfig(cfg, false, sources.RPCKindBasic))
require.NoError(t, err)
return l1F
}
func (s *L1Replica) UnsafeNum() uint64 {
head := s.l1Chain.CurrentBlock()
headNum := uint64(0)
if head != nil {
headNum = head.Number.Uint64()
}
return headNum
}
func (s *L1Replica) SafeNum() uint64 {
safe := s.l1Chain.CurrentSafeBlock()
safeNum := uint64(0)
if safe != nil {
safeNum = safe.Number.Uint64()
}
return safeNum
}
func (s *L1Replica) FinalizedNum() uint64 {
finalized := s.l1Chain.CurrentFinalBlock()
finalizedNum := uint64(0)
if finalized != nil {
finalizedNum = finalized.Number.Uint64()
}
return finalizedNum
}
// ActL1Finalize finalizes a later block, which must be marked as safe before doing so (see ActL1SafeNext).
func (s *L1Replica) ActL1Finalize(t Testing, num uint64) {
safeNum := s.SafeNum()
finalizedNum := s.FinalizedNum()
if safeNum < num {
t.InvalidAction("need to move forward safe block before moving finalized block")
return
}
newFinalized := s.l1Chain.GetHeaderByNumber(num)
if newFinalized == nil {
t.Fatalf("expected block at %d after finalized L1 block %d, safe head is ahead", num, finalizedNum)
}
s.l1Chain.SetFinalized(newFinalized)
}
// ActL1FinalizeNext finalizes the next block, which must be marked as safe before doing so (see ActL1SafeNext).
func (s *L1Replica) ActL1FinalizeNext(t Testing) {
n := s.FinalizedNum() + 1
s.ActL1Finalize(t, n)
}
// ActL1Safe marks the given unsafe block as safe.
func (s *L1Replica) ActL1Safe(t Testing, num uint64) {
newSafe := s.l1Chain.GetHeaderByNumber(num)
if newSafe == nil {
t.InvalidAction("could not find L1 block %d, cannot label it as safe", num)
return
}
s.l1Chain.SetSafe(newSafe)
}
// ActL1SafeNext marks the next unsafe block as safe.
func (s *L1Replica) ActL1SafeNext(t Testing) {
n := s.SafeNum() + 1
s.ActL1Safe(t, n)
}
func (s *L1Replica) Close() error {
return s.node.Close()
}