github.com/onflow/flow-go@v0.33.17/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)
    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)
   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  	node1, id1 := p2ptest.NodeFixture(t,
   215  		previousSporkId,
   216  		"test_one_to_k_crosstalk_prevention",
   217  		idProvider,
   218  		p2ptest.OverrideFlowConfig(cfg),
   219  	)
   220  
   221  	p2ptest.StartNode(t, signalerCtx1, node1)
   222  	defer p2ptest.StopNode(t, node1, cancel1)
   223  	idProvider.SetIdentities(flow.IdentityList{&id1})
   224  
   225  	// create and start node 2 on localhost and random port with the same root block ID
   226  	node2, id2 := p2ptest.NodeFixture(t,
   227  		previousSporkId,
   228  		"test_one_to_k_crosstalk_prevention",
   229  		idProvider,
   230  	)
   231  
   232  	p2ptest.StartNode(t, signalerCtx2, node2)
   233  	defer p2ptest.StopNode(t, node2, cancel2)
   234  
   235  	pInfo2, err := utils.PeerAddressInfo(id2)
   236  	require.NoError(t, err)
   237  
   238  	// spork topic is derived by suffixing the channel with the root block ID
   239  	topicBeforeSpork := channels.TopicFromChannel(channels.TestNetworkChannel, previousSporkId)
   240  
   241  	logger := unittest.Logger()
   242  	topicValidator := flowpubsub.TopicValidator(logger, unittest.AllowAllPeerFilter())
   243  
   244  	// both nodes are initially on the same spork and subscribed to the same topic
   245  	_, err = node1.Subscribe(topicBeforeSpork, topicValidator)
   246  	require.NoError(t, err)
   247  	sub2, err := node2.Subscribe(topicBeforeSpork, topicValidator)
   248  	require.NoError(t, err)
   249  
   250  	// add node 2 as a peer of node 1
   251  	err = node1.ConnectToPeer(ctx, pInfo2)
   252  	require.NoError(t, err)
   253  
   254  	// let the two nodes form the mesh
   255  	time.Sleep(time.Second)
   256  
   257  	// assert that node 1 can successfully send a message to node 2 via PubSub
   258  	outgoingMessageScope, err := message.NewOutgoingScope(
   259  		flow.IdentifierList{unittest.IdentifierFixture()},
   260  		topicBeforeSpork,
   261  		&libp2pmessage.TestMessage{
   262  			Text: string("hello"),
   263  		},
   264  		unittest.NetworkCodec().Encode,
   265  		message.ProtocolTypePubSub)
   266  	require.NoError(t, err)
   267  
   268  	expectedReceivedData, err := outgoingMessageScope.Proto().Marshal()
   269  	require.NoError(t, err)
   270  
   271  	// send a 1-k message from source node to destination node
   272  	err = node1.Publish(ctx, outgoingMessageScope)
   273  	require.NoError(t, err)
   274  
   275  	// assert that the message is received by the destination node
   276  	unittest.AssertReturnsBefore(t, func() {
   277  		msg, err := sub2.Next(ctx)
   278  		require.NoError(t, err)
   279  		assert.Equal(t, expectedReceivedData, msg.Data)
   280  	},
   281  		// libp2p hearbeats every second, so at most the message should take 1 second
   282  		2*time.Second)
   283  
   284  	// new root id after spork
   285  	rootIDAfterSpork := unittest.IdentifierFixture()
   286  
   287  	// topic after the spork
   288  	topicAfterSpork := channels.TopicFromChannel(channels.TestNetworkChannel, rootIDAfterSpork)
   289  
   290  	// mimic that node1 is now part of the new spork while node2 remains on the old spork
   291  	// by unsubscribing node1 from 'topicBeforeSpork' and subscribing it to 'topicAfterSpork'
   292  	// and keeping node2 subscribed to topic 'topicBeforeSpork'
   293  	err = node1.Unsubscribe(topicBeforeSpork)
   294  	require.NoError(t, err)
   295  	_, err = node1.Subscribe(topicAfterSpork, topicValidator)
   296  	require.NoError(t, err)
   297  
   298  	// assert that node 1 can no longer send a message to node 2 via PubSub
   299  	outgoingMessageScope, err = message.NewOutgoingScope(
   300  		flow.IdentifierList{id2.NodeID},
   301  		topicAfterSpork,
   302  		&libp2pmessage.TestMessage{
   303  			Text: string("hello"),
   304  		},
   305  		unittest.NetworkCodec().Encode,
   306  		message.ProtocolTypePubSub)
   307  	require.NoError(t, err)
   308  
   309  	// send a 1-k message from source node to destination node
   310  	err = node1.Publish(ctx, outgoingMessageScope)
   311  	require.NoError(t, err)
   312  
   313  	// assert that the message is never received by the destination node
   314  	_ = unittest.RequireNeverReturnBefore(t, func() {
   315  		_, _ = sub2.Next(ctx)
   316  	},
   317  		// libp2p hearbeats every second, so at most the message should take 1 second
   318  		2*time.Second,
   319  		"nodes on different sporks were able to communicate")
   320  }
   321  
   322  func testOneToOneMessagingFails(t *testing.T, sourceNode p2p.LibP2PNode, peerInfo peer.AddrInfo) {
   323  	// create stream from source node to destination address
   324  	sourceNode.Host().Peerstore().AddAddrs(peerInfo.ID, peerInfo.Addrs, peerstore.AddressTTL)
   325  	ctx, cancel := context.WithCancel(context.Background())
   326  	defer cancel()
   327  	err := sourceNode.OpenAndWriteOnStream(ctx, peerInfo.ID, t.Name(), func(stream network.Stream) error {
   328  		// this callback should never be called
   329  		assert.Fail(t, "stream creation should have failed")
   330  		return nil
   331  	})
   332  	// assert that stream creation failed
   333  	require.Error(t, err)
   334  	// assert that it failed with the expected error
   335  	assert.Regexp(t, ".*failed to negotiate security protocol.*|.*protocols not supported.*", err)
   336  }