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] = &eth.ExecutionPayloadEnvelope{
    89  		ExecutionPayload: &eth.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 := &eth.ExecutionPayloadEnvelope{
    98  			ExecutionPayload: &eth.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  }