github.com/ethereum-optimism/optimism@v1.7.2/op-node/p2p/sync_test.go (about) 1 package p2p 2 3 import ( 4 "context" 5 "math/big" 6 "sync" 7 "testing" 8 "time" 9 10 "github.com/libp2p/go-libp2p/core/host" 11 "github.com/libp2p/go-libp2p/core/network" 12 "github.com/libp2p/go-libp2p/core/peer" 13 mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" 14 "github.com/stretchr/testify/require" 15 16 "github.com/ethereum/go-ethereum" 17 "github.com/ethereum/go-ethereum/common" 18 "github.com/ethereum/go-ethereum/core/types" 19 "github.com/ethereum/go-ethereum/log" 20 21 "github.com/ethereum-optimism/optimism/op-node/metrics" 22 "github.com/ethereum-optimism/optimism/op-node/rollup" 23 "github.com/ethereum-optimism/optimism/op-service/eth" 24 "github.com/ethereum-optimism/optimism/op-service/testlog" 25 ) 26 27 type mockPayloadFn func(n uint64) (*eth.ExecutionPayloadEnvelope, error) 28 29 func (fn mockPayloadFn) PayloadByNumber(_ context.Context, number uint64) (*eth.ExecutionPayloadEnvelope, error) { 30 return fn(number) 31 } 32 33 var _ L2Chain = mockPayloadFn(nil) 34 35 type syncTestData struct { 36 sync.RWMutex 37 payloads map[uint64]*eth.ExecutionPayloadEnvelope 38 } 39 40 func (s *syncTestData) getPayload(i uint64) (payload *eth.ExecutionPayloadEnvelope, ok bool) { 41 s.RLock() 42 defer s.RUnlock() 43 payload, ok = s.payloads[i] 44 return payload, ok 45 } 46 47 func (s *syncTestData) deletePayload(i uint64) { 48 s.Lock() 49 defer s.Unlock() 50 delete(s.payloads, i) 51 } 52 53 func (s *syncTestData) addPayload(payload *eth.ExecutionPayloadEnvelope) { 54 s.Lock() 55 defer s.Unlock() 56 s.payloads[uint64(payload.ExecutionPayload.BlockNumber)] = payload 57 } 58 59 func (s *syncTestData) getBlockRef(i uint64) eth.L2BlockRef { 60 s.RLock() 61 defer s.RUnlock() 62 return eth.L2BlockRef{ 63 Hash: s.payloads[i].ExecutionPayload.BlockHash, 64 Number: uint64(s.payloads[i].ExecutionPayload.BlockNumber), 65 ParentHash: s.payloads[i].ExecutionPayload.ParentHash, 66 Time: uint64(s.payloads[i].ExecutionPayload.Timestamp), 67 } 68 } 69 70 func setupSyncTestData(length uint64) (*rollup.Config, *syncTestData) { 71 // minimal rollup config to build mock blocks & verify their time. 72 cfg := &rollup.Config{ 73 Genesis: rollup.Genesis{ 74 L1: eth.BlockID{Hash: common.Hash{0xaa}}, 75 L2: eth.BlockID{Hash: common.Hash{0xbb}}, 76 L2Time: 9000, 77 }, 78 BlockTime: 2, 79 L2ChainID: big.NewInt(1234), 80 } 81 82 ecotoneBlock := length / 2 83 ecotoneTime := cfg.Genesis.L2Time + ecotoneBlock*cfg.BlockTime 84 cfg.EcotoneTime = &ecotoneTime 85 86 // create some simple fake test blocks 87 payloads := make(map[uint64]*eth.ExecutionPayloadEnvelope) 88 payloads[0] = ð.ExecutionPayloadEnvelope{ 89 ExecutionPayload: ð.ExecutionPayload{ 90 Timestamp: eth.Uint64Quantity(cfg.Genesis.L2Time), 91 }, 92 } 93 94 payloads[0].ExecutionPayload.BlockHash, _ = payloads[0].CheckBlockHash() 95 for i := uint64(1); i <= length; i++ { 96 timestamp := cfg.Genesis.L2Time + i*cfg.BlockTime 97 payload := ð.ExecutionPayloadEnvelope{ 98 ExecutionPayload: ð.ExecutionPayload{ 99 ParentHash: payloads[i-1].ExecutionPayload.BlockHash, 100 BlockNumber: eth.Uint64Quantity(i), 101 Timestamp: eth.Uint64Quantity(timestamp), 102 }, 103 } 104 105 if cfg.IsEcotone(timestamp) { 106 hash := common.BigToHash(big.NewInt(int64(i))) 107 payload.ParentBeaconBlockRoot = &hash 108 109 zero := eth.Uint64Quantity(0) 110 payload.ExecutionPayload.ExcessBlobGas = &zero 111 payload.ExecutionPayload.BlobGasUsed = &zero 112 113 w := types.Withdrawals{} 114 payload.ExecutionPayload.Withdrawals = &w 115 } 116 117 payload.ExecutionPayload.BlockHash, _ = payload.CheckBlockHash() 118 payloads[i] = payload 119 } 120 121 return cfg, &syncTestData{payloads: payloads} 122 } 123 124 func TestSinglePeerSync(t *testing.T) { 125 t.Parallel() // Takes a while, but can run in parallel 126 127 log := testlog.Logger(t, log.LevelError) 128 129 cfg, payloads := setupSyncTestData(25) 130 131 // Serving payloads: just load them from the map, if they exist 132 servePayload := mockPayloadFn(func(n uint64) (*eth.ExecutionPayloadEnvelope, error) { 133 p, ok := payloads.getPayload(n) 134 if !ok { 135 return nil, ethereum.NotFound 136 } 137 return p, nil 138 }) 139 140 // collect received payloads in a buffered channel, so we can verify we get everything 141 received := make(chan *eth.ExecutionPayloadEnvelope, 100) 142 receivePayload := receivePayloadFn(func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope) error { 143 received <- payload 144 return nil 145 }) 146 147 // Setup 2 minimal test hosts to attach the sync protocol to 148 mnet, err := mocknet.FullMeshConnected(2) 149 require.NoError(t, err, "failed to setup mocknet") 150 defer mnet.Close() 151 hosts := mnet.Hosts() 152 hostA, hostB := hosts[0], hosts[1] 153 require.Equal(t, hostA.Network().Connectedness(hostB.ID()), network.Connected) 154 155 ctx, cancel := context.WithCancel(context.Background()) 156 defer cancel() 157 158 // Setup host A as the server 159 srv := NewReqRespServer(cfg, servePayload, metrics.NoopMetrics) 160 payloadByNumber := MakeStreamHandler(ctx, log.New("role", "server"), srv.HandleSyncRequest) 161 hostA.SetStreamHandler(PayloadByNumberProtocolID(cfg.L2ChainID), payloadByNumber) 162 163 // Setup host B as the client 164 cl := NewSyncClient(log.New("role", "client"), cfg, hostB.NewStream, receivePayload, metrics.NoopMetrics, &NoopApplicationScorer{}) 165 166 // Setup host B (client) to sync from its peer Host A (server) 167 cl.AddPeer(hostA.ID()) 168 cl.Start() 169 defer cl.Close() 170 171 // request to start syncing between 10 and 20 172 require.NoError(t, cl.RequestL2Range(ctx, payloads.getBlockRef(10), payloads.getBlockRef(20))) 173 174 // and wait for the sync results to come in (in reverse order) 175 for i := uint64(19); i > 10; i-- { 176 p := <-received 177 require.Equal(t, uint64(p.ExecutionPayload.BlockNumber), i, "expecting payloads in order") 178 exp, ok := payloads.getPayload(uint64(p.ExecutionPayload.BlockNumber)) 179 require.True(t, ok, "expecting known payload") 180 require.Equal(t, exp.ExecutionPayload.BlockHash, p.ExecutionPayload.BlockHash, "expecting the correct payload") 181 182 require.Equal(t, exp.ParentBeaconBlockRoot, p.ParentBeaconBlockRoot) 183 if cfg.IsEcotone(uint64(p.ExecutionPayload.Timestamp)) { 184 require.NotNil(t, p.ParentBeaconBlockRoot) 185 } else { 186 require.Nil(t, p.ParentBeaconBlockRoot) 187 } 188 } 189 } 190 191 func TestMultiPeerSync(t *testing.T) { 192 t.Parallel() // Takes a while, but can run in parallel 193 194 log := testlog.Logger(t, log.LevelDebug) 195 196 cfg, payloads := setupSyncTestData(100) 197 198 // Buffered channel of all blocks requested from any client. 199 requested := make(chan uint64, 100) 200 201 setupPeer := func(ctx context.Context, h host.Host) (*SyncClient, chan *eth.ExecutionPayloadEnvelope) { 202 // Serving payloads: just load them from the map, if they exist 203 servePayload := mockPayloadFn(func(n uint64) (*eth.ExecutionPayloadEnvelope, error) { 204 requested <- n 205 p, ok := payloads.getPayload(n) 206 if !ok { 207 return nil, ethereum.NotFound 208 } 209 return p, nil 210 }) 211 212 // collect received payloads in a buffered channel, so we can verify we get everything 213 received := make(chan *eth.ExecutionPayloadEnvelope, 100) 214 receivePayload := receivePayloadFn(func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope) error { 215 received <- payload 216 return nil 217 }) 218 219 // Setup as server 220 srv := NewReqRespServer(cfg, servePayload, metrics.NoopMetrics) 221 payloadByNumber := MakeStreamHandler(ctx, log.New("serve", "payloads_by_number"), srv.HandleSyncRequest) 222 h.SetStreamHandler(PayloadByNumberProtocolID(cfg.L2ChainID), payloadByNumber) 223 224 cl := NewSyncClient(log.New("role", "client"), cfg, h.NewStream, receivePayload, metrics.NoopMetrics, &NoopApplicationScorer{}) 225 return cl, received 226 } 227 228 // Setup 3 minimal test hosts to attach the sync protocol to 229 mnet, err := mocknet.FullMeshConnected(3) 230 require.NoError(t, err, "failed to setup mocknet") 231 defer mnet.Close() 232 hosts := mnet.Hosts() 233 hostA, hostB, hostC := hosts[0], hosts[1], hosts[2] 234 require.Equal(t, hostA.Network().Connectedness(hostB.ID()), network.Connected) 235 236 ctx, cancel := context.WithCancel(context.Background()) 237 defer cancel() 238 239 clA, recvA := setupPeer(ctx, hostA) 240 clB, recvB := setupPeer(ctx, hostB) 241 clC, _ := setupPeer(ctx, hostC) 242 243 // Make them all sync from each other 244 clA.AddPeer(hostB.ID()) 245 clA.AddPeer(hostC.ID()) 246 clA.Start() 247 defer clA.Close() 248 clB.AddPeer(hostA.ID()) 249 clB.AddPeer(hostC.ID()) 250 clB.Start() 251 defer clB.Close() 252 clC.AddPeer(hostA.ID()) 253 clC.AddPeer(hostB.ID()) 254 clC.Start() 255 defer clC.Close() 256 257 // request to start syncing between 10 and 90 258 require.NoError(t, clA.RequestL2Range(ctx, payloads.getBlockRef(10), payloads.getBlockRef(90))) 259 260 // With such large range to request we are going to hit the rate-limits of B and C, 261 // but that means we'll balance the work between the peers. 262 for i := uint64(89); i > 10; i-- { // wait for all payloads 263 e := <-recvA 264 p := e.ExecutionPayload 265 exp, ok := payloads.getPayload(uint64(p.BlockNumber)) 266 require.True(t, ok, "expecting known payload") 267 require.Equal(t, exp.ExecutionPayload.BlockHash, p.BlockHash, "expecting the correct payload") 268 } 269 270 // now see if B can sync a range, and fill the gap with a re-request 271 bl25, _ := payloads.getPayload(25) // temporarily remove it from the available payloads. This will create a gap 272 payloads.deletePayload(25) 273 require.NoError(t, clB.RequestL2Range(ctx, payloads.getBlockRef(20), payloads.getBlockRef(30))) 274 for i := uint64(29); i > 25; i-- { 275 p := <-recvB 276 exp, ok := payloads.getPayload(uint64(p.ExecutionPayload.BlockNumber)) 277 require.True(t, ok, "expecting known payload") 278 require.Equal(t, exp.ExecutionPayload.BlockHash, p.ExecutionPayload.BlockHash, "expecting the correct payload") 279 } 280 // Wait for the request for block 25 to be made 281 ctx, cancelFunc := context.WithTimeout(context.Background(), 30*time.Second) 282 defer cancelFunc() 283 requestMade := false 284 for requestMade != true { 285 select { 286 case blockNum := <-requested: 287 if blockNum == 25 { 288 requestMade = true 289 } 290 case <-ctx.Done(): 291 t.Fatal("Did not request block 25 in a reasonable time") 292 } 293 } 294 // the request for 25 should fail. See: 295 // server: WARN peer requested unknown block by number num=25 296 // client: WARN failed p2p sync request num=25 err="peer failed to serve request with code 1" 297 require.Zero(t, len(recvB), "there is a gap, should not see other payloads yet") 298 // Add back the block 299 payloads.addPayload(bl25) 300 // race-condition fix: the request for 25 is expected to error, but is marked as complete in the peer-loop. 301 // But the re-request checks the status in the main loop, and it may thus look like it's still in-flight, 302 // and thus not run the new request. 303 // Wait till the failed request is recognized as marked as done, so the re-request actually runs. 304 ctx, cancelFunc = context.WithTimeout(context.Background(), 30*time.Second) 305 defer cancelFunc() 306 for { 307 isInFlight, err := clB.isInFlight(ctx, 25) 308 require.NoError(t, err) 309 if !isInFlight { 310 break 311 } 312 time.Sleep(time.Second) 313 } 314 // And request a range again, 25 is there now, and 21-24 should follow quickly (some may already have been fetched and wait in quarantine) 315 require.NoError(t, clB.RequestL2Range(ctx, payloads.getBlockRef(20), payloads.getBlockRef(26))) 316 for i := uint64(25); i > 20; i-- { 317 p := <-recvB 318 exp, ok := payloads.getPayload(uint64(p.ExecutionPayload.BlockNumber)) 319 require.True(t, ok, "expecting known payload") 320 require.Equal(t, exp.ExecutionPayload.BlockHash, p.ExecutionPayload.BlockHash, "expecting the correct payload") 321 require.Equal(t, exp.ParentBeaconBlockRoot, p.ParentBeaconBlockRoot) 322 if cfg.IsEcotone(uint64(p.ExecutionPayload.Timestamp)) { 323 require.NotNil(t, p.ParentBeaconBlockRoot) 324 } else { 325 require.Nil(t, p.ParentBeaconBlockRoot) 326 } 327 } 328 } 329 330 func TestNetworkNotifyAddPeerAndRemovePeer(t *testing.T) { 331 t.Parallel() 332 log := testlog.Logger(t, log.LevelDebug) 333 334 cfg, _ := setupSyncTestData(25) 335 336 confA := TestingConfig(t) 337 confB := TestingConfig(t) 338 hostA, err := confA.Host(log.New("host", "A"), nil, metrics.NoopMetrics) 339 require.NoError(t, err, "failed to launch host A") 340 defer hostA.Close() 341 hostB, err := confB.Host(log.New("host", "B"), nil, metrics.NoopMetrics) 342 require.NoError(t, err, "failed to launch host B") 343 defer hostB.Close() 344 345 syncCl := NewSyncClient(log, cfg, hostA.NewStream, func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope) error { 346 return nil 347 }, metrics.NoopMetrics, &NoopApplicationScorer{}) 348 349 waitChan := make(chan struct{}, 1) 350 hostA.Network().Notify(&network.NotifyBundle{ 351 ConnectedF: func(nw network.Network, conn network.Conn) { 352 syncCl.AddPeer(conn.RemotePeer()) 353 waitChan <- struct{}{} 354 }, 355 DisconnectedF: func(nw network.Network, conn network.Conn) { 356 // only when no connection is available, we can remove the peer 357 if nw.Connectedness(conn.RemotePeer()) == network.NotConnected { 358 syncCl.RemovePeer(conn.RemotePeer()) 359 } 360 waitChan <- struct{}{} 361 }, 362 }) 363 syncCl.Start() 364 365 err = hostA.Connect(context.Background(), peer.AddrInfo{ID: hostB.ID(), Addrs: hostB.Addrs()}) 366 require.NoError(t, err, "failed to connect to peer B from peer A") 367 require.Equal(t, hostA.Network().Connectedness(hostB.ID()), network.Connected) 368 369 //wait for async add process done 370 <-waitChan 371 _, ok := syncCl.peers[hostB.ID()] 372 require.True(t, ok, "peerB should exist in syncClient") 373 374 err = hostA.Network().ClosePeer(hostB.ID()) 375 require.NoError(t, err, "close peer fail") 376 377 //wait for async removing process done 378 <-waitChan 379 _, peerBExist3 := syncCl.peers[hostB.ID()] 380 require.True(t, !peerBExist3, "peerB should not exist in syncClient") 381 }