package p2p_test import ( "context" "errors" "strings" "testing" "time" "github.com/fortytw2/leaktest" "github.com/stretchr/testify/require" dbm "github.com/tendermint/tm-db" "github.com/tendermint/tendermint/internal/p2p" "github.com/tendermint/tendermint/types" ) // FIXME: We should probably have some randomized property-based tests for the // PeerManager too, which runs a bunch of random operations with random peers // and ensures certain invariants always hold. The logic can be complex, with // many interactions, and it's hard to cover all scenarios with handwritten // tests. func TestPeerManagerOptions_Validate(t *testing.T) { nodeID := types.NodeID("00112233445566778899aabbccddeeff00112233") testcases := map[string]struct { options p2p.PeerManagerOptions ok bool }{ "zero options is valid": {p2p.PeerManagerOptions{}, true}, // PersistentPeers "valid PersistentPeers NodeID": {p2p.PeerManagerOptions{ PersistentPeers: []types.NodeID{"00112233445566778899aabbccddeeff00112233"}, }, true}, "invalid PersistentPeers NodeID": {p2p.PeerManagerOptions{ PersistentPeers: []types.NodeID{"foo"}, }, false}, "uppercase PersistentPeers NodeID": {p2p.PeerManagerOptions{ PersistentPeers: []types.NodeID{"00112233445566778899AABBCCDDEEFF00112233"}, }, false}, "PersistentPeers at MaxConnected": {p2p.PeerManagerOptions{ PersistentPeers: []types.NodeID{nodeID, nodeID, nodeID}, MaxConnected: 3, }, true}, "PersistentPeers above MaxConnected": {p2p.PeerManagerOptions{ PersistentPeers: []types.NodeID{nodeID, nodeID, nodeID}, MaxConnected: 2, }, false}, "PersistentPeers above MaxConnected below MaxConnectedUpgrade": {p2p.PeerManagerOptions{ PersistentPeers: []types.NodeID{nodeID, nodeID, nodeID}, MaxConnected: 2, MaxConnectedUpgrade: 2, }, false}, // MaxPeers "MaxPeers without MaxConnected": {p2p.PeerManagerOptions{ MaxPeers: 3, }, false}, "MaxPeers below MaxConnected+MaxConnectedUpgrade": {p2p.PeerManagerOptions{ MaxPeers: 2, MaxConnected: 2, MaxConnectedUpgrade: 1, }, false}, "MaxPeers at MaxConnected+MaxConnectedUpgrade": {p2p.PeerManagerOptions{ MaxPeers: 3, MaxConnected: 2, MaxConnectedUpgrade: 1, }, true}, // MaxRetryTime "MaxRetryTime below MinRetryTime": {p2p.PeerManagerOptions{ MinRetryTime: 7 * time.Second, MaxRetryTime: 5 * time.Second, }, false}, "MaxRetryTime at MinRetryTime": {p2p.PeerManagerOptions{ MinRetryTime: 5 * time.Second, MaxRetryTime: 5 * time.Second, }, true}, "MaxRetryTime without MinRetryTime": {p2p.PeerManagerOptions{ MaxRetryTime: 5 * time.Second, }, false}, // MaxRetryTimePersistent "MaxRetryTimePersistent below MinRetryTime": {p2p.PeerManagerOptions{ MinRetryTime: 7 * time.Second, MaxRetryTimePersistent: 5 * time.Second, }, false}, "MaxRetryTimePersistent at MinRetryTime": {p2p.PeerManagerOptions{ MinRetryTime: 5 * time.Second, MaxRetryTimePersistent: 5 * time.Second, }, true}, "MaxRetryTimePersistent without MinRetryTime": {p2p.PeerManagerOptions{ MaxRetryTimePersistent: 5 * time.Second, }, false}, } for name, tc := range testcases { tc := tc t.Run(name, func(t *testing.T) { err := tc.options.Validate() if tc.ok { require.NoError(t, err) } else { require.Error(t, err) } }) } } func TestNewPeerManager(t *testing.T) { // Zero options should be valid. _, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) // Invalid options should error. _, err = p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ PersistentPeers: []types.NodeID{"foo"}, }) require.Error(t, err) // Invalid database should error. _, err = p2p.NewPeerManager(selfID, nil, p2p.PeerManagerOptions{}) require.Error(t, err) // Empty self ID should error. _, err = p2p.NewPeerManager("", nil, p2p.PeerManagerOptions{}) require.Error(t, err) } func TestNewPeerManager_Persistence(t *testing.T) { aID := types.NodeID(strings.Repeat("a", 40)) aAddresses := []p2p.NodeAddress{ {Protocol: "tcp", NodeID: aID, Hostname: "127.0.0.1", Port: 26657, Path: "/path"}, {Protocol: "memory", NodeID: aID}, } bID := types.NodeID(strings.Repeat("b", 40)) bAddresses := []p2p.NodeAddress{ {Protocol: "tcp", NodeID: bID, Hostname: "b10c::1", Port: 26657, Path: "/path"}, {Protocol: "memory", NodeID: bID}, } cID := types.NodeID(strings.Repeat("c", 40)) cAddresses := []p2p.NodeAddress{ {Protocol: "tcp", NodeID: cID, Hostname: "host.domain", Port: 80}, {Protocol: "memory", NodeID: cID}, } // Create an initial peer manager and add the peers. db := dbm.NewMemDB() peerManager, err := p2p.NewPeerManager(selfID, db, p2p.PeerManagerOptions{ PersistentPeers: []types.NodeID{aID}, PeerScores: map[types.NodeID]p2p.PeerScore{bID: 1}, }) require.NoError(t, err) for _, addr := range append(append(aAddresses, bAddresses...), cAddresses...) { added, err := peerManager.Add(addr) require.NoError(t, err) require.True(t, added) } require.ElementsMatch(t, aAddresses, peerManager.Addresses(aID)) require.ElementsMatch(t, bAddresses, peerManager.Addresses(bID)) require.ElementsMatch(t, cAddresses, peerManager.Addresses(cID)) require.Equal(t, map[types.NodeID]p2p.PeerScore{ aID: p2p.PeerScorePersistent, bID: 1, cID: 0, }, peerManager.Scores()) // Creating a new peer manager with the same database should retain the // peers, but they should have updated scores from the new PersistentPeers // configuration. peerManager, err = p2p.NewPeerManager(selfID, db, p2p.PeerManagerOptions{ PersistentPeers: []types.NodeID{bID}, PeerScores: map[types.NodeID]p2p.PeerScore{cID: 1}, }) require.NoError(t, err) require.ElementsMatch(t, aAddresses, peerManager.Addresses(aID)) require.ElementsMatch(t, bAddresses, peerManager.Addresses(bID)) require.ElementsMatch(t, cAddresses, peerManager.Addresses(cID)) require.Equal(t, map[types.NodeID]p2p.PeerScore{ aID: 0, bID: p2p.PeerScorePersistent, cID: 1, }, peerManager.Scores()) } func TestNewPeerManager_SelfIDChange(t *testing.T) { a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} db := dbm.NewMemDB() peerManager, err := p2p.NewPeerManager(selfID, db, p2p.PeerManagerOptions{}) require.NoError(t, err) added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) require.ElementsMatch(t, []types.NodeID{a.NodeID, b.NodeID}, peerManager.Peers()) // If we change our selfID to one of the peers in the peer store, it // should be removed from the store. peerManager, err = p2p.NewPeerManager(a.NodeID, db, p2p.PeerManagerOptions{}) require.NoError(t, err) require.Equal(t, []types.NodeID{b.NodeID}, peerManager.Peers()) } func TestPeerManager_Add(t *testing.T) { aID := types.NodeID(strings.Repeat("a", 40)) bID := types.NodeID(strings.Repeat("b", 40)) cID := types.NodeID(strings.Repeat("c", 40)) peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ PersistentPeers: []types.NodeID{aID, cID}, MaxPeers: 2, MaxConnected: 2, }) require.NoError(t, err) // Adding a couple of addresses should work. aAddresses := []p2p.NodeAddress{ {Protocol: "tcp", NodeID: aID, Hostname: "localhost"}, {Protocol: "memory", NodeID: aID}, } for _, addr := range aAddresses { added, err := peerManager.Add(addr) require.NoError(t, err) require.True(t, added) } require.ElementsMatch(t, aAddresses, peerManager.Addresses(aID)) // Adding a different peer should be fine. bAddress := p2p.NodeAddress{Protocol: "tcp", NodeID: bID, Hostname: "localhost"} added, err := peerManager.Add(bAddress) require.NoError(t, err) require.True(t, added) require.Equal(t, []p2p.NodeAddress{bAddress}, peerManager.Addresses(bID)) require.ElementsMatch(t, aAddresses, peerManager.Addresses(aID)) // Adding an existing address again should be a noop. added, err = peerManager.Add(aAddresses[0]) require.NoError(t, err) require.False(t, added) require.ElementsMatch(t, aAddresses, peerManager.Addresses(aID)) // Adding a third peer with MaxPeers=2 should cause bID, which is // the lowest-scored peer (not in PersistentPeers), to be removed. added, err = peerManager.Add(p2p.NodeAddress{ Protocol: "tcp", NodeID: cID, Hostname: "localhost"}) require.NoError(t, err) require.True(t, added) require.ElementsMatch(t, []types.NodeID{aID, cID}, peerManager.Peers()) // Adding an invalid address should error. _, err = peerManager.Add(p2p.NodeAddress{Path: "foo"}) require.Error(t, err) // Adding self should error _, err = peerManager.Add(p2p.NodeAddress{Protocol: "memory", NodeID: selfID}) require.Error(t, err) } func TestPeerManager_DialNext(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) // Add an address. DialNext should return it. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) address, err := peerManager.DialNext(ctx) require.NoError(t, err) require.Equal(t, a, address) // Since there are no more undialed peers, the next call should block // until it times out. timeoutCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() _, err = peerManager.DialNext(timeoutCtx) require.Error(t, err) require.Equal(t, context.DeadlineExceeded, err) } func TestPeerManager_DialNext_Retry(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} options := p2p.PeerManagerOptions{ MinRetryTime: 100 * time.Millisecond, MaxRetryTime: 500 * time.Millisecond, } peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), options) require.NoError(t, err) added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) // Do five dial retries (six dials total). The retry time should double for // each failure. At the forth retry, MaxRetryTime should kick in. ctx, cancel = context.WithTimeout(ctx, 5*time.Second) defer cancel() for i := 0; i <= 5; i++ { start := time.Now() dial, err := peerManager.DialNext(ctx) require.NoError(t, err) require.Equal(t, a, dial) elapsed := time.Since(start).Round(time.Millisecond) switch i { case 0: require.LessOrEqual(t, elapsed, options.MinRetryTime) case 1: require.GreaterOrEqual(t, elapsed, options.MinRetryTime) case 2: require.GreaterOrEqual(t, elapsed, 2*options.MinRetryTime) case 3: require.GreaterOrEqual(t, elapsed, 4*options.MinRetryTime) case 4, 5: require.GreaterOrEqual(t, elapsed, options.MaxRetryTime) require.LessOrEqual(t, elapsed, 8*options.MinRetryTime) default: require.Fail(t, "unexpected retry") } require.NoError(t, peerManager.DialFailed(ctx, a)) } } func TestPeerManager_DialNext_WakeOnAdd(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) // Spawn a goroutine to add a peer after a delay. go func() { time.Sleep(200 * time.Millisecond) added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) }() // This will block until peer is added above. ctx, cancel = context.WithTimeout(ctx, 3*time.Second) defer cancel() dial, err := peerManager.DialNext(ctx) require.NoError(t, err) require.Equal(t, a, dial) } func TestPeerManager_DialNext_WakeOnDialFailed(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ MaxConnected: 1, }) require.NoError(t, err) a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} // Add and dial a. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) // Add b. We shouldn't be able to dial it, due to MaxConnected. added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Zero(t, dial) // Spawn a goroutine to fail a's dial attempt. sig := make(chan struct{}) go func() { defer close(sig) time.Sleep(200 * time.Millisecond) require.NoError(t, peerManager.DialFailed(ctx, a)) }() // This should make b available for dialing (not a, retries are disabled). opctx, opcancel := context.WithTimeout(ctx, 3*time.Second) defer opcancel() dial, err = peerManager.DialNext(opctx) require.NoError(t, err) require.Equal(t, b, dial) <-sig } func TestPeerManager_DialNext_WakeOnDialFailedRetry(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() options := p2p.PeerManagerOptions{MinRetryTime: 200 * time.Millisecond} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), options) require.NoError(t, err) a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} // Add a, dial it, and mark it a failure. This will start a retry timer. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) require.NoError(t, peerManager.DialFailed(ctx, dial)) failed := time.Now() // The retry timer should unblock DialNext and make a available again after // the retry time passes. ctx, cancel = context.WithTimeout(ctx, 3*time.Second) defer cancel() dial, err = peerManager.DialNext(ctx) require.NoError(t, err) require.Equal(t, a, dial) require.GreaterOrEqual(t, time.Since(failed), options.MinRetryTime) } func TestPeerManager_DialNext_WakeOnDisconnected(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) err = peerManager.Accepted(a.NodeID) require.NoError(t, err) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Zero(t, dial) dctx, dcancel := context.WithTimeout(ctx, 300*time.Millisecond) defer dcancel() go func() { time.Sleep(200 * time.Millisecond) peerManager.Disconnected(dctx, a.NodeID) }() ctx, cancel = context.WithTimeout(ctx, 3*time.Second) defer cancel() dial, err = peerManager.DialNext(ctx) require.NoError(t, err) require.Equal(t, a, dial) } func TestPeerManager_TryDialNext_MaxConnected(t *testing.T) { a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} c := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("c", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ MaxConnected: 2, }) require.NoError(t, err) // Add a and connect to it. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) require.NoError(t, peerManager.Dialed(a)) // Add b and start dialing it. added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, b, dial) // At this point, adding c will not allow dialing it. added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Zero(t, dial) } func TestPeerManager_TryDialNext_MaxConnectedUpgrade(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} c := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("c", 40))} d := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("d", 40))} e := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("e", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ PeerScores: map[types.NodeID]p2p.PeerScore{ a.NodeID: 0, b.NodeID: 1, c.NodeID: 2, d.NodeID: 3, e.NodeID: 0, }, PersistentPeers: []types.NodeID{c.NodeID, d.NodeID}, MaxConnected: 2, MaxConnectedUpgrade: 1, }) require.NoError(t, err) // Add a and connect to it. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) require.NoError(t, peerManager.Dialed(a)) // Add b and start dialing it. added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, b, dial) // Even though we are at capacity, we should be allowed to dial c for an // upgrade of a, since it's higher-scored. added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, c, dial) // However, since we're using all upgrade slots now, we can't add and dial // d, even though it's also higher-scored. added, err = peerManager.Add(d) require.NoError(t, err) require.True(t, added) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Zero(t, dial) // We go through with c's upgrade. require.NoError(t, peerManager.Dialed(c)) // Still can't dial d. dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Zero(t, dial) // Now, if we disconnect a, we should be allowed to dial d because we have a // free upgrade slot. peerManager.Disconnected(ctx, a.NodeID) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, d, dial) require.NoError(t, peerManager.Dialed(d)) // However, if we disconnect b (such that only c and d are connected), we // should not be allowed to dial e even though there are upgrade slots, // because there are no lower-scored nodes that can be upgraded. peerManager.Disconnected(ctx, b.NodeID) added, err = peerManager.Add(e) require.NoError(t, err) require.True(t, added) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Zero(t, dial) } func TestPeerManager_TryDialNext_UpgradeReservesPeer(t *testing.T) { a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} c := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("c", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ PeerScores: map[types.NodeID]p2p.PeerScore{b.NodeID: 1, c.NodeID: 1}, MaxConnected: 1, MaxConnectedUpgrade: 2, }) require.NoError(t, err) // Add a and connect to it. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) require.NoError(t, peerManager.Dialed(a)) // Add b and start dialing it. This will claim a for upgrading. added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, b, dial) // Adding c and dialing it will fail, because a is the only connected // peer that can be upgraded, and b is already trying to upgrade it. added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Empty(t, dial) } func TestPeerManager_TryDialNext_DialingConnected(t *testing.T) { aID := types.NodeID(strings.Repeat("a", 40)) a := p2p.NodeAddress{Protocol: "memory", NodeID: aID} aTCP := p2p.NodeAddress{Protocol: "tcp", NodeID: aID, Hostname: "localhost"} bID := types.NodeID(strings.Repeat("b", 40)) b := p2p.NodeAddress{Protocol: "memory", NodeID: bID} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ MaxConnected: 2, }) require.NoError(t, err) // Add a and dial it. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) // Adding a's TCP address will not dispense a, since it's already dialing. added, err = peerManager.Add(aTCP) require.NoError(t, err) require.True(t, added) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Zero(t, dial) // Marking a as dialed will still not dispense it. require.NoError(t, peerManager.Dialed(a)) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Zero(t, dial) // Adding b and accepting a connection from it will not dispense it either. added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(bID)) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Zero(t, dial) } func TestPeerManager_TryDialNext_Multiple(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() aID := types.NodeID(strings.Repeat("a", 40)) bID := types.NodeID(strings.Repeat("b", 40)) addresses := []p2p.NodeAddress{ {Protocol: "memory", NodeID: aID}, {Protocol: "memory", NodeID: bID}, {Protocol: "tcp", NodeID: aID, Hostname: "127.0.0.1"}, {Protocol: "tcp", NodeID: bID, Hostname: "::1"}, } peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) for _, address := range addresses { added, err := peerManager.Add(address) require.NoError(t, err) require.True(t, added) } // All addresses should be dispensed as long as dialing them has failed. dial := []p2p.NodeAddress{} for range addresses { address, err := peerManager.TryDialNext() require.NoError(t, err) require.NotZero(t, address) require.NoError(t, peerManager.DialFailed(ctx, address)) dial = append(dial, address) } require.ElementsMatch(t, dial, addresses) address, err := peerManager.TryDialNext() require.NoError(t, err) require.Zero(t, address) } func TestPeerManager_DialFailed(t *testing.T) { // DialFailed is tested through other tests, we'll just check a few basic // things here, e.g. reporting unknown addresses. aID := types.NodeID(strings.Repeat("a", 40)) a := p2p.NodeAddress{Protocol: "memory", NodeID: aID} bID := types.NodeID(strings.Repeat("b", 40)) b := p2p.NodeAddress{Protocol: "memory", NodeID: bID} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Dialing and then calling DialFailed with a different address (same // NodeID) should unmark as dialing and allow us to dial the other address // again, but not register the failed address. dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) require.NoError(t, peerManager.DialFailed(ctx, p2p.NodeAddress{ Protocol: "tcp", NodeID: aID, Hostname: "localhost"})) require.Equal(t, []p2p.NodeAddress{a}, peerManager.Addresses(aID)) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) // Calling DialFailed on same address twice should be fine. require.NoError(t, peerManager.DialFailed(ctx, a)) require.NoError(t, peerManager.DialFailed(ctx, a)) // DialFailed on an unknown peer shouldn't error or add it. require.NoError(t, peerManager.DialFailed(ctx, b)) require.Equal(t, []types.NodeID{aID}, peerManager.Peers()) } func TestPeerManager_DialFailed_UnreservePeer(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} c := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("c", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ PeerScores: map[types.NodeID]p2p.PeerScore{b.NodeID: 1, c.NodeID: 1}, MaxConnected: 1, MaxConnectedUpgrade: 2, }) require.NoError(t, err) // Add a and connect to it. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) require.NoError(t, peerManager.Dialed(a)) // Add b and start dialing it. This will claim a for upgrading. added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, b, dial) // Adding c and dialing it will fail, even though it could upgrade a and we // have free upgrade slots, because a is the only connected peer that can be // upgraded and b is already trying to upgrade it. added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Empty(t, dial) // Failing b's dial will now make c available for dialing. require.NoError(t, peerManager.DialFailed(ctx, b)) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, c, dial) } func TestPeerManager_Dialed_Connected(t *testing.T) { a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) // Marking a as dialed twice should error. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) require.NoError(t, peerManager.Dialed(a)) require.Error(t, peerManager.Dialed(a)) // Accepting a connection from b and then trying to mark it as dialed should fail. added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, b, dial) require.NoError(t, peerManager.Accepted(b.NodeID)) require.Error(t, peerManager.Dialed(b)) } func TestPeerManager_Dialed_Self(t *testing.T) { peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) // Dialing self should error. _, err = peerManager.Add(p2p.NodeAddress{Protocol: "memory", NodeID: selfID}) require.Error(t, err) } func TestPeerManager_Dialed_MaxConnected(t *testing.T) { a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ MaxConnected: 1, }) require.NoError(t, err) // Start to dial a. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) // Marking b as dialed in the meanwhile (even without TryDialNext) // should be fine. added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Dialed(b)) // Completing the dial for a should now error. require.Error(t, peerManager.Dialed(a)) } func TestPeerManager_Dialed_MaxConnectedUpgrade(t *testing.T) { a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} c := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("c", 40))} d := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("d", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ MaxConnected: 2, MaxConnectedUpgrade: 1, PeerScores: map[types.NodeID]p2p.PeerScore{c.NodeID: 1, d.NodeID: 1}, }) require.NoError(t, err) // Dialing a and b is fine. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Dialed(a)) added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Dialed(b)) // Starting an upgrade of c should be fine. added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, c, dial) require.NoError(t, peerManager.Dialed(c)) // Trying to mark d dialed should fail, since there are no more upgrade // slots and a/b haven't been evicted yet. added, err = peerManager.Add(d) require.NoError(t, err) require.True(t, added) require.Error(t, peerManager.Dialed(d)) } func TestPeerManager_Dialed_Unknown(t *testing.T) { a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) // Marking an unknown node as dialed should error. require.Error(t, peerManager.Dialed(a)) } func TestPeerManager_Dialed_Upgrade(t *testing.T) { a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} c := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("c", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ MaxConnected: 1, MaxConnectedUpgrade: 2, PeerScores: map[types.NodeID]p2p.PeerScore{b.NodeID: 1, c.NodeID: 1}, }) require.NoError(t, err) // Dialing a is fine. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Dialed(a)) // Upgrading it with b should work, since b has a higher score. added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, b, dial) require.NoError(t, peerManager.Dialed(b)) // a hasn't been evicted yet, but c shouldn't be allowed to upgrade anyway // since it's about to be evicted. added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Empty(t, dial) // a should now be evicted. evict, err := peerManager.TryEvictNext() require.NoError(t, err) require.Equal(t, a.NodeID, evict) } func TestPeerManager_Dialed_UpgradeEvenLower(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} c := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("c", 40))} d := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("d", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ MaxConnected: 2, MaxConnectedUpgrade: 1, PeerScores: map[types.NodeID]p2p.PeerScore{ a.NodeID: 3, b.NodeID: 2, c.NodeID: 10, d.NodeID: 1, }, }) require.NoError(t, err) // Connect to a and b. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Dialed(a)) added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Dialed(b)) // Start an upgrade with c, which should pick b to upgrade (since it // has score 2). added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, c, dial) // In the meanwhile, a disconnects and d connects. d is even lower-scored // than b (1 vs 2), which is currently being upgraded. peerManager.Disconnected(ctx, a.NodeID) added, err = peerManager.Add(d) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(d.NodeID)) // Once c completes the upgrade of b, it should instead evict d, // since it has en even lower score. require.NoError(t, peerManager.Dialed(c)) evict, err := peerManager.TryEvictNext() require.NoError(t, err) require.Equal(t, d.NodeID, evict) } func TestPeerManager_Dialed_UpgradeNoEvict(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} c := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("c", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ MaxConnected: 2, MaxConnectedUpgrade: 1, PeerScores: map[types.NodeID]p2p.PeerScore{ a.NodeID: 1, b.NodeID: 2, c.NodeID: 3, }, }) require.NoError(t, err) // Connect to a and b. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Dialed(a)) added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Dialed(b)) // Start an upgrade with c, which should pick a to upgrade. added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, c, dial) // In the meanwhile, b disconnects. peerManager.Disconnected(ctx, b.NodeID) // Once c completes the upgrade of b, there is no longer a need to // evict anything since we're at capacity. // since it has en even lower score. require.NoError(t, peerManager.Dialed(c)) evict, err := peerManager.TryEvictNext() require.NoError(t, err) require.Zero(t, evict) } func TestPeerManager_Accepted(t *testing.T) { a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} c := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("c", 40))} d := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("d", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) // Accepting a connection from self should error. require.Error(t, peerManager.Accepted(selfID)) // Accepting a connection from a known peer should work. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(a.NodeID)) // Accepting a connection from an already accepted peer should error. require.Error(t, peerManager.Accepted(a.NodeID)) // Accepting a connection from an unknown peer should work and register it. require.NoError(t, peerManager.Accepted(b.NodeID)) require.ElementsMatch(t, []types.NodeID{a.NodeID, b.NodeID}, peerManager.Peers()) // Accepting a connection from a peer that's being dialed should work, and // should cause the dial to fail. added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, c, dial) require.NoError(t, peerManager.Accepted(c.NodeID)) require.Error(t, peerManager.Dialed(c)) // Accepting a connection from a peer that's been dialed should fail. added, err = peerManager.Add(d) require.NoError(t, err) require.True(t, added) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, d, dial) require.NoError(t, peerManager.Dialed(d)) require.Error(t, peerManager.Accepted(d.NodeID)) } func TestPeerManager_Accepted_MaxConnected(t *testing.T) { a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} c := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("c", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ MaxConnected: 2, }) require.NoError(t, err) // Connect to a and b. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Dialed(a)) added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(b.NodeID)) // Accepting c should now fail. added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) require.Error(t, peerManager.Accepted(c.NodeID)) } func TestPeerManager_Accepted_MaxConnectedUpgrade(t *testing.T) { a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} c := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("c", 40))} d := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("d", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ PeerScores: map[types.NodeID]p2p.PeerScore{ c.NodeID: 1, d.NodeID: 2, }, MaxConnected: 1, MaxConnectedUpgrade: 1, }) require.NoError(t, err) // Dial a. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Dialed(a)) // Accepting b should fail, since it's not an upgrade over a. added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) require.Error(t, peerManager.Accepted(b.NodeID)) // Accepting c should work, since it upgrades a. added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(c.NodeID)) // a still hasn't been evicted, so accepting b should still fail. _, err = peerManager.Add(b) require.NoError(t, err) require.Error(t, peerManager.Accepted(b.NodeID)) // Also, accepting d should fail, since all upgrade slots are full. added, err = peerManager.Add(d) require.NoError(t, err) require.True(t, added) require.Error(t, peerManager.Accepted(d.NodeID)) } func TestPeerManager_Accepted_Upgrade(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} c := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("c", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ PeerScores: map[types.NodeID]p2p.PeerScore{ b.NodeID: 1, c.NodeID: 1, }, MaxConnected: 1, MaxConnectedUpgrade: 2, }) require.NoError(t, err) // Accept a. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(a.NodeID)) // Accepting b should work, since it upgrades a. added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(b.NodeID)) // c cannot get accepted, since a has been upgraded by b. added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) require.Error(t, peerManager.Accepted(c.NodeID)) // This should cause a to get evicted. evict, err := peerManager.TryEvictNext() require.NoError(t, err) require.Equal(t, a.NodeID, evict) peerManager.Disconnected(ctx, a.NodeID) // c still cannot get accepted, since it's not scored above b. require.Error(t, peerManager.Accepted(c.NodeID)) } func TestPeerManager_Accepted_UpgradeDialing(t *testing.T) { a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} c := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("c", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ PeerScores: map[types.NodeID]p2p.PeerScore{ b.NodeID: 1, c.NodeID: 1, }, MaxConnected: 1, MaxConnectedUpgrade: 2, }) require.NoError(t, err) // Accept a. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(a.NodeID)) // Start dial upgrade from a to b. added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, b, dial) // a has already been claimed as an upgrade of a, so accepting // c should fail since there's noone else to upgrade. added, err = peerManager.Add(c) require.NoError(t, err) require.True(t, added) require.Error(t, peerManager.Accepted(c.NodeID)) // However, if b connects to us while we're also trying to upgrade to it via // dialing, then we accept the incoming connection as an upgrade. require.NoError(t, peerManager.Accepted(b.NodeID)) // This should cause a to get evicted, and the dial upgrade to fail. evict, err := peerManager.TryEvictNext() require.NoError(t, err) require.Equal(t, a.NodeID, evict) require.Error(t, peerManager.Dialed(b)) } func TestPeerManager_Ready(t *testing.T) { a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} ctx, cancel := context.WithCancel(context.Background()) defer cancel() peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) sub := peerManager.Subscribe(ctx) // Connecting to a should still have it as status down. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(a.NodeID)) require.Equal(t, p2p.PeerStatusDown, peerManager.Status(a.NodeID)) // Marking a as ready should transition it to PeerStatusUp and send an update. peerManager.Ready(ctx, a.NodeID) require.Equal(t, p2p.PeerStatusUp, peerManager.Status(a.NodeID)) require.Equal(t, p2p.PeerUpdate{ NodeID: a.NodeID, Status: p2p.PeerStatusUp, }, <-sub.Updates()) // Marking an unconnected peer as ready should do nothing. added, err = peerManager.Add(b) require.NoError(t, err) require.True(t, added) require.Equal(t, p2p.PeerStatusDown, peerManager.Status(b.NodeID)) peerManager.Ready(ctx, b.NodeID) require.Equal(t, p2p.PeerStatusDown, peerManager.Status(b.NodeID)) require.Empty(t, sub.Updates()) } // See TryEvictNext for most tests, this just tests blocking behavior. func TestPeerManager_EvictNext(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(a.NodeID)) peerManager.Ready(ctx, a.NodeID) // Since there are no peers to evict, EvictNext should block until timeout. timeoutCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() _, err = peerManager.EvictNext(timeoutCtx) require.Error(t, err) require.Equal(t, context.DeadlineExceeded, err) // Erroring the peer will return it from EvictNext(). peerManager.Errored(a.NodeID, errors.New("foo")) evict, err := peerManager.EvictNext(timeoutCtx) require.NoError(t, err) require.Equal(t, a.NodeID, evict) // Since there are no more peers to evict, the next call should block. timeoutCtx, cancel = context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() _, err = peerManager.EvictNext(timeoutCtx) require.Error(t, err) require.Equal(t, context.DeadlineExceeded, err) } func TestPeerManager_EvictNext_WakeOnError(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(a.NodeID)) peerManager.Ready(ctx, a.NodeID) // Spawn a goroutine to error a peer after a delay. go func() { time.Sleep(200 * time.Millisecond) peerManager.Errored(a.NodeID, errors.New("foo")) }() // This will block until peer errors above. ctx, cancel = context.WithTimeout(ctx, 3*time.Second) defer cancel() evict, err := peerManager.EvictNext(ctx) require.NoError(t, err) require.Equal(t, a.NodeID, evict) } func TestPeerManager_EvictNext_WakeOnUpgradeDialed(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ MaxConnected: 1, MaxConnectedUpgrade: 1, PeerScores: map[types.NodeID]p2p.PeerScore{b.NodeID: 1}, }) require.NoError(t, err) // Connect a. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(a.NodeID)) peerManager.Ready(ctx, a.NodeID) // Spawn a goroutine to upgrade to b with a delay. go func() { time.Sleep(200 * time.Millisecond) added, err := peerManager.Add(b) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, b, dial) require.NoError(t, peerManager.Dialed(b)) }() // This will block until peer is upgraded above. ctx, cancel = context.WithTimeout(ctx, 3*time.Second) defer cancel() evict, err := peerManager.EvictNext(ctx) require.NoError(t, err) require.Equal(t, a.NodeID, evict) } func TestPeerManager_EvictNext_WakeOnUpgradeAccepted(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ MaxConnected: 1, MaxConnectedUpgrade: 1, PeerScores: map[types.NodeID]p2p.PeerScore{b.NodeID: 1}, }) require.NoError(t, err) // Connect a. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(a.NodeID)) peerManager.Ready(ctx, a.NodeID) // Spawn a goroutine to upgrade b with a delay. go func() { time.Sleep(200 * time.Millisecond) require.NoError(t, peerManager.Accepted(b.NodeID)) }() // This will block until peer is upgraded above. ctx, cancel = context.WithTimeout(ctx, 3*time.Second) defer cancel() evict, err := peerManager.EvictNext(ctx) require.NoError(t, err) require.Equal(t, a.NodeID, evict) } func TestPeerManager_TryEvictNext(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) // Nothing is evicted with no peers connected. evict, err := peerManager.TryEvictNext() require.NoError(t, err) require.Zero(t, evict) // Connecting to a won't evict anything either. require.NoError(t, peerManager.Accepted(a.NodeID)) peerManager.Ready(ctx, a.NodeID) // But if a errors it should be evicted. peerManager.Errored(a.NodeID, errors.New("foo")) evict, err = peerManager.TryEvictNext() require.NoError(t, err) require.Equal(t, a.NodeID, evict) // While a is being evicted (before disconnect), it shouldn't get evicted again. evict, err = peerManager.TryEvictNext() require.NoError(t, err) require.Zero(t, evict) peerManager.Errored(a.NodeID, errors.New("foo")) evict, err = peerManager.TryEvictNext() require.NoError(t, err) require.Zero(t, evict) } func TestPeerManager_Disconnected(t *testing.T) { a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) ctx, cancel := context.WithCancel(context.Background()) defer cancel() sub := peerManager.Subscribe(ctx) // Disconnecting an unknown peer does nothing. peerManager.Disconnected(ctx, a.NodeID) require.Empty(t, peerManager.Peers()) require.Empty(t, sub.Updates()) // Disconnecting an accepted non-ready peer does not send a status update. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(a.NodeID)) peerManager.Disconnected(ctx, a.NodeID) require.Empty(t, sub.Updates()) // Disconnecting a ready peer sends a status update. _, err = peerManager.Add(a) require.NoError(t, err) require.NoError(t, peerManager.Accepted(a.NodeID)) peerManager.Ready(ctx, a.NodeID) require.Equal(t, p2p.PeerStatusUp, peerManager.Status(a.NodeID)) require.NotEmpty(t, sub.Updates()) require.Equal(t, p2p.PeerUpdate{ NodeID: a.NodeID, Status: p2p.PeerStatusUp, }, <-sub.Updates()) peerManager.Disconnected(ctx, a.NodeID) require.Equal(t, p2p.PeerStatusDown, peerManager.Status(a.NodeID)) require.NotEmpty(t, sub.Updates()) require.Equal(t, p2p.PeerUpdate{ NodeID: a.NodeID, Status: p2p.PeerStatusDown, }, <-sub.Updates()) // Disconnecting a dialing peer does not unmark it as dialing, to avoid // dialing it multiple times in parallel. dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) peerManager.Disconnected(ctx, a.NodeID) dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Zero(t, dial) } func TestPeerManager_Errored(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) // Erroring an unknown peer does nothing. peerManager.Errored(a.NodeID, errors.New("foo")) require.Empty(t, peerManager.Peers()) evict, err := peerManager.TryEvictNext() require.NoError(t, err) require.Zero(t, evict) // Erroring a known peer does nothing, and won't evict it later, // even when it connects. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) peerManager.Errored(a.NodeID, errors.New("foo")) evict, err = peerManager.TryEvictNext() require.NoError(t, err) require.Zero(t, evict) require.NoError(t, peerManager.Accepted(a.NodeID)) peerManager.Ready(ctx, a.NodeID) evict, err = peerManager.TryEvictNext() require.NoError(t, err) require.Zero(t, evict) // However, erroring once connected will evict it. peerManager.Errored(a.NodeID, errors.New("foo")) evict, err = peerManager.TryEvictNext() require.NoError(t, err) require.Equal(t, a.NodeID, evict) } func TestPeerManager_Subscribe(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) // This tests all subscription events for full peer lifecycles. sub := peerManager.Subscribe(ctx) added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.Empty(t, sub.Updates()) // Inbound connection. require.NoError(t, peerManager.Accepted(a.NodeID)) require.Empty(t, sub.Updates()) peerManager.Ready(ctx, a.NodeID) require.NotEmpty(t, sub.Updates()) require.Equal(t, p2p.PeerUpdate{NodeID: a.NodeID, Status: p2p.PeerStatusUp}, <-sub.Updates()) peerManager.Disconnected(ctx, a.NodeID) require.NotEmpty(t, sub.Updates()) require.Equal(t, p2p.PeerUpdate{NodeID: a.NodeID, Status: p2p.PeerStatusDown}, <-sub.Updates()) // Outbound connection with peer error and eviction. dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) require.Empty(t, sub.Updates()) require.NoError(t, peerManager.Dialed(a)) require.Empty(t, sub.Updates()) peerManager.Ready(ctx, a.NodeID) require.NotEmpty(t, sub.Updates()) require.Equal(t, p2p.PeerUpdate{NodeID: a.NodeID, Status: p2p.PeerStatusUp}, <-sub.Updates()) peerManager.Errored(a.NodeID, errors.New("foo")) require.Empty(t, sub.Updates()) evict, err := peerManager.TryEvictNext() require.NoError(t, err) require.Equal(t, a.NodeID, evict) peerManager.Disconnected(ctx, a.NodeID) require.NotEmpty(t, sub.Updates()) require.Equal(t, p2p.PeerUpdate{NodeID: a.NodeID, Status: p2p.PeerStatusDown}, <-sub.Updates()) // Outbound connection with dial failure. dial, err = peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) require.Empty(t, sub.Updates()) require.NoError(t, peerManager.DialFailed(ctx, a)) require.Empty(t, sub.Updates()) } func TestPeerManager_Subscribe_Close(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) sub := peerManager.Subscribe(ctx) added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(a.NodeID)) require.Empty(t, sub.Updates()) peerManager.Ready(ctx, a.NodeID) require.NotEmpty(t, sub.Updates()) require.Equal(t, p2p.PeerUpdate{NodeID: a.NodeID, Status: p2p.PeerStatusUp}, <-sub.Updates()) // Closing the subscription should not send us the disconnected update. cancel() peerManager.Disconnected(ctx, a.NodeID) require.Empty(t, sub.Updates()) } func TestPeerManager_Subscribe_Broadcast(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() t.Cleanup(leaktest.Check(t)) a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) s2ctx, s2cancel := context.WithCancel(ctx) defer s2cancel() s1 := peerManager.Subscribe(ctx) s2 := peerManager.Subscribe(s2ctx) s3 := peerManager.Subscribe(ctx) // Connecting to a peer should send updates on all subscriptions. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.NoError(t, peerManager.Accepted(a.NodeID)) peerManager.Ready(ctx, a.NodeID) expectUp := p2p.PeerUpdate{NodeID: a.NodeID, Status: p2p.PeerStatusUp} require.NotEmpty(t, s1) require.Equal(t, expectUp, <-s1.Updates()) require.NotEmpty(t, s2) require.Equal(t, expectUp, <-s2.Updates()) require.NotEmpty(t, s3) require.Equal(t, expectUp, <-s3.Updates()) // We now close s2. Disconnecting the peer should only send updates // on s1 and s3. s2cancel() time.Sleep(250 * time.Millisecond) // give the thread a chance to exit peerManager.Disconnected(ctx, a.NodeID) expectDown := p2p.PeerUpdate{NodeID: a.NodeID, Status: p2p.PeerStatusDown} require.NotEmpty(t, s1) require.Equal(t, expectDown, <-s1.Updates()) require.Empty(t, s2.Updates()) require.NotEmpty(t, s3) require.Equal(t, expectDown, <-s3.Updates()) } func TestPeerManager_Close(t *testing.T) { // leaktest will check that spawned goroutines are closed. t.Cleanup(leaktest.CheckTimeout(t, 1*time.Second)) ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ MinRetryTime: 10 * time.Second, }) require.NoError(t, err) // This subscription isn't closed, but PeerManager.Close() // should reap the spawned goroutine. _ = peerManager.Subscribe(ctx) // This dial failure will start a retry timer for 10 seconds, which // should be reaped. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) dial, err := peerManager.TryDialNext() require.NoError(t, err) require.Equal(t, a, dial) require.NoError(t, peerManager.DialFailed(ctx, a)) } func TestPeerManager_Advertise(t *testing.T) { aID := types.NodeID(strings.Repeat("a", 40)) aTCP := p2p.NodeAddress{Protocol: "tcp", NodeID: aID, Hostname: "127.0.0.1", Port: 26657, Path: "/path"} aMem := p2p.NodeAddress{Protocol: "memory", NodeID: aID} bID := types.NodeID(strings.Repeat("b", 40)) bTCP := p2p.NodeAddress{Protocol: "tcp", NodeID: bID, Hostname: "b10c::1", Port: 26657, Path: "/path"} bMem := p2p.NodeAddress{Protocol: "memory", NodeID: bID} cID := types.NodeID(strings.Repeat("c", 40)) cTCP := p2p.NodeAddress{Protocol: "tcp", NodeID: cID, Hostname: "host.domain", Port: 80} cMem := p2p.NodeAddress{Protocol: "memory", NodeID: cID} dID := types.NodeID(strings.Repeat("d", 40)) // Create an initial peer manager and add the peers. peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{ PeerScores: map[types.NodeID]p2p.PeerScore{aID: 3, bID: 2, cID: 1}, }) require.NoError(t, err) added, err := peerManager.Add(aTCP) require.NoError(t, err) require.True(t, added) added, err = peerManager.Add(aMem) require.NoError(t, err) require.True(t, added) added, err = peerManager.Add(bTCP) require.NoError(t, err) require.True(t, added) added, err = peerManager.Add(bMem) require.NoError(t, err) require.True(t, added) added, err = peerManager.Add(cTCP) require.NoError(t, err) require.True(t, added) added, err = peerManager.Add(cMem) require.NoError(t, err) require.True(t, added) // d should get all addresses. require.ElementsMatch(t, []p2p.NodeAddress{ aTCP, aMem, bTCP, bMem, cTCP, cMem, }, peerManager.Advertise(dID, 100)) // a should not get its own addresses. require.ElementsMatch(t, []p2p.NodeAddress{ bTCP, bMem, cTCP, cMem, }, peerManager.Advertise(aID, 100)) // Asking for 0 addresses should return, well, 0. require.Empty(t, peerManager.Advertise(aID, 0)) // Asking for 2 addresses should get the highest-rated ones, i.e. a. require.ElementsMatch(t, []p2p.NodeAddress{ aTCP, aMem, }, peerManager.Advertise(dID, 2)) } func TestPeerManager_SetHeight_GetHeight(t *testing.T) { a := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("a", 40))} b := p2p.NodeAddress{Protocol: "memory", NodeID: types.NodeID(strings.Repeat("b", 40))} db := dbm.NewMemDB() peerManager, err := p2p.NewPeerManager(selfID, db, p2p.PeerManagerOptions{}) require.NoError(t, err) // Getting a height should default to 0, for unknown peers and // for known peers without height. added, err := peerManager.Add(a) require.NoError(t, err) require.True(t, added) require.EqualValues(t, 0, peerManager.GetHeight(a.NodeID)) require.EqualValues(t, 0, peerManager.GetHeight(b.NodeID)) // Setting a height should work for a known node. require.NoError(t, peerManager.SetHeight(a.NodeID, 3)) require.EqualValues(t, 3, peerManager.GetHeight(a.NodeID)) // Setting a height should add an unknown node. require.Equal(t, []types.NodeID{a.NodeID}, peerManager.Peers()) require.NoError(t, peerManager.SetHeight(b.NodeID, 7)) require.EqualValues(t, 7, peerManager.GetHeight(b.NodeID)) require.ElementsMatch(t, []types.NodeID{a.NodeID, b.NodeID}, peerManager.Peers()) // The heights should not be persisted. peerManager, err = p2p.NewPeerManager(selfID, db, p2p.PeerManagerOptions{}) require.NoError(t, err) require.ElementsMatch(t, []types.NodeID{a.NodeID, b.NodeID}, peerManager.Peers()) require.Zero(t, peerManager.GetHeight(a.NodeID)) require.Zero(t, peerManager.GetHeight(b.NodeID)) }