github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/p2p/test/sporking_test.go (about)

     1  package p2ptest_test
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/libp2p/go-libp2p/core/network"
     9  	"github.com/libp2p/go-libp2p/core/peer"
    10  	"github.com/libp2p/go-libp2p/core/peerstore"
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  
    14  	"github.com/onflow/flow-go/config"
    15  	"github.com/onflow/flow-go/model/flow"
    16  	libp2pmessage "github.com/onflow/flow-go/model/libp2p/message"
    17  	"github.com/onflow/flow-go/network/message"
    18  
    19  	"github.com/onflow/flow-go/network/p2p"
    20  	p2ptest "github.com/onflow/flow-go/network/p2p/test"
    21  
    22  	"github.com/onflow/flow-go/network/p2p/utils"
    23  
    24  	"github.com/onflow/flow-go/module/irrecoverable"
    25  	"github.com/onflow/flow-go/network/channels"
    26  	"github.com/onflow/flow-go/network/internal/p2pfixtures"
    27  	flowpubsub "github.com/onflow/flow-go/network/validator/pubsub"
    28  	"github.com/onflow/flow-go/utils/unittest"
    29  )
    30  
    31  // Tests in this file evaluate tests that the network layer behaves as expected after a spork.
    32  // All network related sporking requirements can be supported via libp2p directly,
    33  // without needing any additional support in Flow other than providing the new root block ID.
    34  // Sporking can be supported by two ways:
    35  // 1. Updating the network key of a node after it is moved from the old chain to the new chain
    36  // 2. Updating the Flow Libp2p protocol ID suffix to prevent one-to-one messaging across sporks and
    37  //    updating the channel suffix to prevent one-to-K messaging (PubSub) across sporks.
    38  // 1 and 2 both can independently ensure that nodes from the old chain cannot communicate with nodes on the new chain
    39  // These tests are just to reconfirm the network behaviour and provide a test bed for future tests for sporking, if needed
    40  
    41  // TestCrosstalkPreventionOnNetworkKeyChange tests that a node from the old chain cannot talk to a node in the new chain
    42  // if it's network key is updated while the libp2p protocol ID remains the same
    43  func TestCrosstalkPreventionOnNetworkKeyChange(t *testing.T) {
    44  	idProvider := unittest.NewUpdatableIDProvider(flow.IdentityList{})
    45  	ctx, cancel := context.WithCancel(context.Background())
    46  	defer cancel()
    47  
    48  	ctx1, cancel1 := context.WithCancel(ctx)
    49  	signalerCtx1 := irrecoverable.NewMockSignalerContext(t, ctx1)
    50  
    51  	ctx2, cancel2 := context.WithCancel(ctx)
    52  	signalerCtx2 := irrecoverable.NewMockSignalerContext(t, ctx2)
    53  
    54  	ctx2a, cancel2a := context.WithCancel(ctx)
    55  	signalerCtx2a := irrecoverable.NewMockSignalerContext(t, ctx2a)
    56  
    57  	// create and start node 1 on localhost and random port
    58  	node1key := p2pfixtures.NetworkingKeyFixtures(t)
    59  	sporkId := unittest.IdentifierFixture()
    60  
    61  	node1, id1 := p2ptest.NodeFixture(t,
    62  		sporkId,
    63  		"test_crosstalk_prevention_on_network_key_change",
    64  		idProvider,
    65  		p2ptest.WithNetworkingPrivateKey(node1key),
    66  	)
    67  	idProvider.SetIdentities(flow.IdentityList{&id1})
    68  
    69  	p2ptest.StartNode(t, signalerCtx1, node1)
    70  	defer p2ptest.StopNode(t, node1, cancel1)
    71  
    72  	t.Logf(" %s node started on %s", id1.NodeID.String(), id1.Address)
    73  	t.Logf("libp2p ID for %s: %s", id1.NodeID.String(), node1.ID())
    74  
    75  	// create and start node 2 on localhost and random port
    76  	node2key := p2ptest.NetworkingKeyFixtures(t)
    77  	node2, id2 := p2ptest.NodeFixture(t,
    78  		sporkId,
    79  		"test_crosstalk_prevention_on_network_key_change",
    80  		idProvider,
    81  		p2ptest.WithNetworkingPrivateKey(node2key),
    82  	)
    83  	idProvider.SetIdentities(flow.IdentityList{&id1, &id2})
    84  
    85  	p2ptest.StartNode(t, signalerCtx2, node2)
    86  
    87  	peerInfo2, err := utils.PeerAddressInfo(id2.IdentitySkeleton)
    88  	require.NoError(t, err)
    89  
    90  	// create stream from node 1 to node 2
    91  	node1.Host().Peerstore().AddAddrs(peerInfo2.ID, peerInfo2.Addrs, peerstore.AddressTTL)
    92  	err = node1.OpenAndWriteOnStream(context.Background(), peerInfo2.ID, t.Name(), func(stream network.Stream) error {
    93  		require.NotNil(t, stream)
    94  		return nil
    95  	})
    96  	require.NoError(t, err)
    97  
    98  	// Simulate a hard-spoon: node1 is on the old chain, but node2 is moved from the old chain to the new chain
    99  	// stop node 2 and start it again with a different networking key but on the same IP and port
   100  	p2ptest.StopNode(t, node2, cancel2)
   101  
   102  	// start node2 with the same name, ip and port but with the new key
   103  	node2keyNew := p2pfixtures.NetworkingKeyFixtures(t)
   104  	assert.False(t, node2key.Equals(node2keyNew))
   105  	node2, id2New := p2ptest.NodeFixture(t,
   106  		sporkId,
   107  		"test_crosstalk_prevention_on_network_key_change",
   108  		idProvider,
   109  		p2ptest.WithNetworkingPrivateKey(node2keyNew),
   110  		p2ptest.WithNetworkingAddress(id2.Address),
   111  	)
   112  	idProvider.SetIdentities(flow.IdentityList{&id1, &id2New})
   113  
   114  	p2ptest.StartNode(t, signalerCtx2a, node2)
   115  	defer p2ptest.StopNode(t, node2, cancel2a)
   116  
   117  	// make sure the node2 indeed came up on the old ip and port
   118  	assert.Equal(t, id2New.Address, id2.Address)
   119  
   120  	// attempt to create a stream from node 1 (old chain) to node 2 (new chain)
   121  	// this time it should fail since node 2 is using a different public key
   122  	// (and therefore has a different libp2p node id)
   123  	testOneToOneMessagingFails(t, node1, peerInfo2)
   124  }
   125  
   126  // TestOneToOneCrosstalkPrevention tests that a node from the old chain cannot talk directly to a node in the new chain
   127  // if the Flow libp2p protocol ID is updated while the network keys are kept the same.
   128  func TestOneToOneCrosstalkPrevention(t *testing.T) {
   129  	idProvider := unittest.NewUpdatableIDProvider(flow.IdentityList{})
   130  	ctx, cancel := context.WithCancel(context.Background())
   131  	defer cancel()
   132  
   133  	ctx1, cancel1 := context.WithCancel(ctx)
   134  	signalerCtx1 := irrecoverable.NewMockSignalerContext(t, ctx1)
   135  
   136  	ctx2, cancel2 := context.WithCancel(ctx)
   137  	signalerCtx2 := irrecoverable.NewMockSignalerContext(t, ctx2)
   138  
   139  	ctx2a, cancel2a := context.WithCancel(ctx)
   140  	signalerCtx2a := irrecoverable.NewMockSignalerContext(t, ctx2a)
   141  
   142  	sporkId1 := unittest.IdentifierFixture()
   143  
   144  	// create and start node 1 on localhost and random port
   145  	node1, id1 := p2ptest.NodeFixture(t, sporkId1, "test_one_to_one_crosstalk_prevention", idProvider)
   146  
   147  	p2ptest.StartNode(t, signalerCtx1, node1)
   148  	defer p2ptest.StopNode(t, node1, cancel1)
   149  
   150  	peerInfo1, err := utils.PeerAddressInfo(id1.IdentitySkeleton)
   151  	require.NoError(t, err)
   152  
   153  	// create and start node 2 on localhost and random port
   154  	node2, id2 := p2ptest.NodeFixture(t, sporkId1, "test_one_to_one_crosstalk_prevention", idProvider)
   155  
   156  	idProvider.SetIdentities(flow.IdentityList{&id1, &id2})
   157  	p2ptest.StartNode(t, signalerCtx2, node2)
   158  
   159  	// create stream from node 1 to node 2
   160  	node2.Host().Peerstore().AddAddrs(peerInfo1.ID, peerInfo1.Addrs, peerstore.AddressTTL)
   161  	err = node2.OpenAndWriteOnStream(context.Background(), peerInfo1.ID, t.Name(), func(stream network.Stream) error {
   162  		assert.NotNil(t, stream)
   163  		return nil
   164  	})
   165  	require.NoError(t, err)
   166  
   167  	// Simulate a hard-spoon: node1 is on the old chain, but node2 is moved from the old chain to the new chain
   168  	// stop node 2 and start it again with a different libp2p protocol id to listen for
   169  	p2ptest.StopNode(t, node2, cancel2)
   170  
   171  	// start node2 with the same address and root key but different root block id
   172  	node2, id2New := p2ptest.NodeFixture(t,
   173  		unittest.IdentifierFixture(), // update the flow root id for node 2. node1 is still listening on the old protocol
   174  		"test_one_to_one_crosstalk_prevention",
   175  		idProvider,
   176  		p2ptest.WithNetworkingAddress(id2.Address),
   177  	)
   178  	idProvider.SetIdentities(flow.IdentityList{&id1, &id2New})
   179  
   180  	p2ptest.StartNode(t, signalerCtx2a, node2)
   181  	defer p2ptest.StopNode(t, node2, cancel2a)
   182  
   183  	// make sure the node2 indeed came up on the old ip and port
   184  	assert.Equal(t, id2New.Address, id2.Address)
   185  
   186  	// attempt to create a stream from node 2 (new chain) to node 1 (old chain)
   187  	// this time it should fail since node 2 is listening on a different protocol
   188  	testOneToOneMessagingFails(t, node2, peerInfo1)
   189  }
   190  
   191  // TestOneToKCrosstalkPrevention tests that a node from the old chain cannot talk to a node in the new chain via PubSub
   192  // if the channel is updated while the network keys are kept the same.
   193  func TestOneToKCrosstalkPrevention(t *testing.T) {
   194  	idProvider := unittest.NewUpdatableIDProvider(flow.IdentityList{})
   195  	ctx, cancel := context.WithCancel(context.Background())
   196  	defer cancel()
   197  
   198  	ctx1, cancel1 := context.WithCancel(ctx)
   199  	signalerCtx1 := irrecoverable.NewMockSignalerContext(t, ctx1)
   200  
   201  	ctx2, cancel2 := context.WithCancel(ctx)
   202  	signalerCtx2 := irrecoverable.NewMockSignalerContext(t, ctx2)
   203  
   204  	// root id before spork
   205  	previousSporkId := unittest.IdentifierFixture()
   206  
   207  	// create and start node 1 on localhost and random port
   208  	cfg, err := config.DefaultConfig()
   209  	require.NoError(t, err)
   210  	// cross-talk prevention is intrinsically tied to how we encode topics, peer scoring adds another layer of protection by preventing unknown identifiers
   211  	// from joining the mesh. As this test simulates the scenario where a node is moved from the old chain to the new chain, we disable peer scoring
   212  	// to allow the node to join the mesh on the new chain, otherwise the node will be disconnected from the mesh due to peer scoring penalty for unknown identifiers.
   213  	cfg.NetworkConfig.GossipSub.PeerScoringEnabled = false
   214  	cfg.NetworkConfig.GossipSub.RpcInspector.Validation.InspectionProcess.Inspect.RejectUnstakedPeers = false
   215  	node1, id1 := p2ptest.NodeFixture(t,
   216  		previousSporkId,
   217  		"test_one_to_k_crosstalk_prevention",
   218  		idProvider,
   219  		p2ptest.OverrideFlowConfig(cfg),
   220  	)
   221  
   222  	p2ptest.StartNode(t, signalerCtx1, node1)
   223  	defer p2ptest.StopNode(t, node1, cancel1)
   224  	idProvider.SetIdentities(flow.IdentityList{&id1})
   225  
   226  	// create and start node 2 on localhost and random port with the same root block ID
   227  	node2, id2 := p2ptest.NodeFixture(t,
   228  		previousSporkId,
   229  		"test_one_to_k_crosstalk_prevention",
   230  		idProvider,
   231  	)
   232  
   233  	p2ptest.StartNode(t, signalerCtx2, node2)
   234  	defer p2ptest.StopNode(t, node2, cancel2)
   235  
   236  	pInfo2, err := utils.PeerAddressInfo(id2.IdentitySkeleton)
   237  	require.NoError(t, err)
   238  
   239  	// spork topic is derived by suffixing the channel with the root block ID
   240  	topicBeforeSpork := channels.TopicFromChannel(channels.TestNetworkChannel, previousSporkId)
   241  
   242  	logger := unittest.Logger()
   243  	topicValidator := flowpubsub.TopicValidator(logger, unittest.AllowAllPeerFilter())
   244  
   245  	// both nodes are initially on the same spork and subscribed to the same topic
   246  	_, err = node1.Subscribe(topicBeforeSpork, topicValidator)
   247  	require.NoError(t, err)
   248  	sub2, err := node2.Subscribe(topicBeforeSpork, topicValidator)
   249  	require.NoError(t, err)
   250  
   251  	// add node 2 as a peer of node 1
   252  	err = node1.ConnectToPeer(ctx, pInfo2)
   253  	require.NoError(t, err)
   254  
   255  	// let the two nodes form the mesh
   256  	time.Sleep(time.Second)
   257  
   258  	// assert that node 1 can successfully send a message to node 2 via PubSub
   259  	outgoingMessageScope, err := message.NewOutgoingScope(
   260  		flow.IdentifierList{unittest.IdentifierFixture()},
   261  		topicBeforeSpork,
   262  		&libp2pmessage.TestMessage{
   263  			Text: string("hello"),
   264  		},
   265  		unittest.NetworkCodec().Encode,
   266  		message.ProtocolTypePubSub)
   267  	require.NoError(t, err)
   268  
   269  	expectedReceivedData, err := outgoingMessageScope.Proto().Marshal()
   270  	require.NoError(t, err)
   271  
   272  	// send a 1-k message from source node to destination node
   273  	err = node1.Publish(ctx, outgoingMessageScope)
   274  	require.NoError(t, err)
   275  
   276  	// assert that the message is received by the destination node
   277  	unittest.AssertReturnsBefore(t, func() {
   278  		msg, err := sub2.Next(ctx)
   279  		require.NoError(t, err)
   280  		assert.Equal(t, expectedReceivedData, msg.Data)
   281  	},
   282  		// libp2p hearbeats every second, so at most the message should take 1 second
   283  		2*time.Second)
   284  
   285  	// new root id after spork
   286  	rootIDAfterSpork := unittest.IdentifierFixture()
   287  
   288  	// topic after the spork
   289  	topicAfterSpork := channels.TopicFromChannel(channels.TestNetworkChannel, rootIDAfterSpork)
   290  
   291  	// mimic that node1 is now part of the new spork while node2 remains on the old spork
   292  	// by unsubscribing node1 from 'topicBeforeSpork' and subscribing it to 'topicAfterSpork'
   293  	// and keeping node2 subscribed to topic 'topicBeforeSpork'
   294  	err = node1.Unsubscribe(topicBeforeSpork)
   295  	require.NoError(t, err)
   296  	_, err = node1.Subscribe(topicAfterSpork, topicValidator)
   297  	require.NoError(t, err)
   298  
   299  	// assert that node 1 can no longer send a message to node 2 via PubSub
   300  	outgoingMessageScope, err = message.NewOutgoingScope(
   301  		flow.IdentifierList{id2.NodeID},
   302  		topicAfterSpork,
   303  		&libp2pmessage.TestMessage{
   304  			Text: string("hello"),
   305  		},
   306  		unittest.NetworkCodec().Encode,
   307  		message.ProtocolTypePubSub)
   308  	require.NoError(t, err)
   309  
   310  	// send a 1-k message from source node to destination node
   311  	err = node1.Publish(ctx, outgoingMessageScope)
   312  	require.NoError(t, err)
   313  
   314  	// assert that the message is never received by the destination node
   315  	_ = unittest.RequireNeverReturnBefore(t, func() {
   316  		_, _ = sub2.Next(ctx)
   317  	},
   318  		// libp2p hearbeats every second, so at most the message should take 1 second
   319  		2*time.Second,
   320  		"nodes on different sporks were able to communicate")
   321  }
   322  
   323  func testOneToOneMessagingFails(t *testing.T, sourceNode p2p.LibP2PNode, peerInfo peer.AddrInfo) {
   324  	// create stream from source node to destination address
   325  	sourceNode.Host().Peerstore().AddAddrs(peerInfo.ID, peerInfo.Addrs, peerstore.AddressTTL)
   326  	ctx, cancel := context.WithCancel(context.Background())
   327  	defer cancel()
   328  	err := sourceNode.OpenAndWriteOnStream(ctx, peerInfo.ID, t.Name(), func(stream network.Stream) error {
   329  		// this callback should never be called
   330  		assert.Fail(t, "stream creation should have failed")
   331  		return nil
   332  	})
   333  	// assert that stream creation failed
   334  	require.Error(t, err)
   335  	// assert that it failed with the expected error
   336  	assert.Regexp(t, ".*failed to negotiate security protocol.*|.*protocols not supported.*", err)
   337  }