diff --git a/network/p2p/peerstore/peerstore.go b/network/p2p/peerstore/peerstore.go index ee6bf90a65..63a88966ff 100644 --- a/network/p2p/peerstore/peerstore.go +++ b/network/p2p/peerstore/peerstore.go @@ -18,15 +18,52 @@ package peerstore import ( "fmt" + "math" + "math/rand" + "time" "github.com/libp2p/go-libp2p/core/peer" libp2p "github.com/libp2p/go-libp2p/core/peerstore" mempstore "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem" + "golang.org/x/exp/slices" ) +// when using GetAddresses with getAllAddresses, all the addresses will be retrieved, regardless +// of how many addresses the phonebook actually has. ( with the retry-after logic applied ) +const getAllAddresses = math.MaxInt32 + +// PhoneBookEntryRoles defines the roles that a single entry on the phonebook can take. +// currently, we have two roles : relay role and archiver role, which are mutually exclusive. +// +//msgp:ignore PhoneBookEntryRoles +type PhoneBookEntryRoles int + +const addressDataKey string = "addressData" + // PeerStore implements Peerstore and CertifiedAddrBook. type PeerStore struct { peerStoreCAB + connectionsRateLimitingCount uint + connectionsRateLimitingWindow time.Duration +} + +// addressData: holds the information associated with each phonebook address. +type addressData struct { + // retryAfter is the time to wait before retrying to connect to the address. + retryAfter time.Time + + // recentConnectionTimes is the log of connection times used to observe the maximum + // connections to the address in a given time window. + recentConnectionTimes []time.Time + + // networkNames: lists the networks to which the given address belongs. + networkNames map[string]bool + + // role is the role that this address serves. + role PhoneBookEntryRoles + + // persistent is set true for peers whose record should not be removed for the peer list + persistent bool } // peerStoreCAB combines the libp2p Peerstore and CertifiedAddrBook interfaces. @@ -47,6 +84,294 @@ func NewPeerStore(addrInfo []*peer.AddrInfo) (*PeerStore, error) { info := addrInfo[i] ps.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) } - pstore := &PeerStore{ps} + pstore := &PeerStore{peerStoreCAB: ps} + return pstore, nil +} + +// MakePhonebook creates a phonebook with the passed configuration values +func MakePhonebook(connectionsRateLimitingCount uint, + connectionsRateLimitingWindow time.Duration) (*PeerStore, error) { + ps, err := mempstore.NewPeerstore() + if err != nil { + return &PeerStore{}, fmt.Errorf("cannot initialize a peerstore: %w", err) + } + pstore := &PeerStore{peerStoreCAB: ps, + connectionsRateLimitingCount: connectionsRateLimitingCount, + connectionsRateLimitingWindow: connectionsRateLimitingWindow, + } return pstore, nil } + +// GetAddresses returns up to N addresses, but may return fewer +func (ps *PeerStore) GetAddresses(n int, role PhoneBookEntryRoles) []string { + return shuffleSelect(ps.filterRetryTime(time.Now(), role), n) +} + +// UpdateRetryAfter updates the retryAfter time for the given address. +func (ps *PeerStore) UpdateRetryAfter(addr string, retryAfter time.Time) { + info, err := PeerInfoFromDomainPort(addr) + if err != nil { + return + } + metadata, _ := ps.Get(info.ID, addressDataKey) + if metadata != nil { + ad, ok := metadata.(addressData) + if !ok { + return + } + ad.retryAfter = retryAfter + _ = ps.Put(info.ID, addressDataKey, ad) + } + +} + +// GetConnectionWaitTime will calculate and return the wait +// time to prevent exceeding connectionsRateLimitingCount. +// The connection should be established when the waitTime is 0. +// It will register a provisional next connection time when the waitTime is 0. +// The provisional time should be updated after the connection with UpdateConnectionTime +func (ps *PeerStore) GetConnectionWaitTime(addr string) (bool, time.Duration, time.Time) { + curTime := time.Now() + info, err := PeerInfoFromDomainPort(addr) + if err != nil { + return false, 0 /* not used */, curTime /* not used */ + } + var timeSince time.Duration + var numElmtsToRemove int + metadata, err := ps.Get(info.ID, addressDataKey) + if err != nil { + return false, 0 /* not used */, curTime /* not used */ + } + ad, ok := metadata.(addressData) + if !ok { + return false, 0 /* not used */, curTime /* not used */ + } + // Remove from recentConnectionTimes the times later than ConnectionsRateLimitingWindowSeconds + for numElmtsToRemove < len(ad.recentConnectionTimes) { + timeSince = curTime.Sub(ad.recentConnectionTimes[numElmtsToRemove]) + if timeSince >= ps.connectionsRateLimitingWindow { + numElmtsToRemove++ + } else { + break // break the loop. The rest are earlier than 1 second + } + } + + // Remove the expired elements from e.data[addr].recentConnectionTimes + ps.popNElements(numElmtsToRemove, peer.ID(addr)) + // If there are max number of connections within the time window, wait + metadata, _ = ps.Get(info.ID, addressDataKey) + ad, ok = metadata.(addressData) + if !ok { + return false, 0 /* not used */, curTime /* not used */ + } + numElts := len(ad.recentConnectionTimes) + if uint(numElts) >= ps.connectionsRateLimitingCount { + return true, /* true */ + ps.connectionsRateLimitingWindow - timeSince, curTime /* not used */ + } + + // Else, there is space in connectionsRateLimitingCount. The + // connection request of the caller will proceed + // Update curTime, since it may have significantly changed if waited + provisionalTime := time.Now() + // Append the provisional time for the next connection request + ps.appendTime(info.ID, provisionalTime) + return true, 0 /* no wait. proceed */, provisionalTime +} + +// UpdateConnectionTime updates the connection time for the given address. +func (ps *PeerStore) UpdateConnectionTime(addr string, provisionalTime time.Time) bool { + info, err := PeerInfoFromDomainPort(addr) + if err != nil { + return false + } + metadata, err := ps.Get(info.ID, addressDataKey) + if err != nil { + return false + } + ad, ok := metadata.(addressData) + if !ok { + return false + } + defer func() { + _ = ps.Put(info.ID, addressDataKey, ad) + + }() + + // Find the provisionalTime and update it + entry := ad.recentConnectionTimes + for indx, val := range entry { + if provisionalTime == val { + entry[indx] = time.Now() + return true + } + } + + // Case where the time is not found: it was removed from the list. + // This may happen when the time expires before the connection was established with the server. + // The time should be added again. + entry = append(entry, time.Now()) + ad.recentConnectionTimes = entry + + return true +} + +// ReplacePeerList replaces the peer list for the given networkName and role. +func (ps *PeerStore) ReplacePeerList(addressesThey []string, networkName string, role PhoneBookEntryRoles) { + // prepare a map of items we'd like to remove. + removeItems := make(map[peer.ID]bool, 0) + peerIDs := ps.Peers() + for _, pid := range peerIDs { + data, _ := ps.Get(pid, addressDataKey) + if data != nil { + ad := data.(addressData) + if ad.networkNames[networkName] && ad.role == role && !ad.persistent { + removeItems[pid] = true + } + } + + } + for _, addr := range addressesThey { + info, err := PeerInfoFromDomainPort(addr) + if err != nil { + return + } + data, _ := ps.Get(info.ID, addressDataKey) + if data != nil { + // we already have this. + // Update the networkName + ad := data.(addressData) + ad.networkNames[networkName] = true + + // do not remove this entry + delete(removeItems, info.ID) + } else { + // we don't have this item. add it. + ps.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) + entry := makePhonebookEntryData(networkName, role, false) + _ = ps.Put(info.ID, addressDataKey, entry) + } + } + + // remove items that were missing in addressesThey + for k := range removeItems { + ps.deletePhonebookEntry(k, networkName) + } +} + +// AddPersistentPeers stores addresses of peers which are persistent. +// i.e. they won't be replaced by ReplacePeerList calls +func (ps *PeerStore) AddPersistentPeers(dnsAddresses []string, networkName string, role PhoneBookEntryRoles) { + + for _, addr := range dnsAddresses { + info, err := PeerInfoFromDomainPort(addr) + if err != nil { + return + } + data, _ := ps.Get(info.ID, addressDataKey) + if data != nil { + // we already have this. + // Make sure the persistence field is set to true + ad := data.(addressData) + ad.persistent = true + _ = ps.Put(info.ID, addressDataKey, data) + + } else { + // we don't have this item. add it. + ps.AddAddrs(info.ID, info.Addrs, libp2p.PermanentAddrTTL) + entry := makePhonebookEntryData(networkName, role, true) + _ = ps.Put(info.ID, addressDataKey, entry) + } + } +} + +// Length returns the number of addrs in peerstore +func (ps *PeerStore) Length() int { + return len(ps.Peers()) +} + +// makePhonebookEntryData creates a new address entry for provided network name and role. +func makePhonebookEntryData(networkName string, role PhoneBookEntryRoles, persistent bool) addressData { + pbData := addressData{ + networkNames: make(map[string]bool), + recentConnectionTimes: make([]time.Time, 0), + role: role, + persistent: persistent, + } + pbData.networkNames[networkName] = true + return pbData +} + +func (ps *PeerStore) deletePhonebookEntry(peerID peer.ID, networkName string) { + data, err := ps.Get(peerID, addressDataKey) + if err != nil { + return + } + ad := data.(addressData) + delete(ad.networkNames, networkName) + if 0 == len(ad.networkNames) { + ps.ClearAddrs(peerID) + _ = ps.Put(peerID, addressDataKey, nil) + } +} + +// AppendTime adds the current time to recentConnectionTimes in +// addressData of addr +func (ps *PeerStore) appendTime(peerID peer.ID, t time.Time) { + data, _ := ps.Get(peerID, addressDataKey) + ad := data.(addressData) + ad.recentConnectionTimes = append(ad.recentConnectionTimes, t) + _ = ps.Put(peerID, addressDataKey, ad) +} + +// PopEarliestTime removes the earliest time from recentConnectionTimes in +// addressData for addr +// It is expected to be later than ConnectionsRateLimitingWindow +func (ps *PeerStore) popNElements(n int, peerID peer.ID) { + data, _ := ps.Get(peerID, addressDataKey) + ad := data.(addressData) + ad.recentConnectionTimes = ad.recentConnectionTimes[n:] + _ = ps.Put(peerID, addressDataKey, ad) +} + +func (ps *PeerStore) filterRetryTime(t time.Time, role PhoneBookEntryRoles) []string { + o := make([]string, 0, len(ps.Peers())) + for _, peerID := range ps.Peers() { + data, _ := ps.Get(peerID, addressDataKey) + if data != nil { + ad := data.(addressData) + if t.After(ad.retryAfter) && role == ad.role { + o = append(o, string(peerID)) + } + } + } + return o +} + +func shuffleSelect(set []string, n int) []string { + if n >= len(set) || n == getAllAddresses { + // return shuffled copy of everything + out := slices.Clone(set) + shuffleStrings(out) + return out + } + // Pick random indexes from the set + indexSample := make([]int, n) + for i := range indexSample { + indexSample[i] = rand.Intn(len(set)-i) + i + for oi, ois := range indexSample[:i] { + if ois == indexSample[i] { + indexSample[i] = oi + } + } + } + out := make([]string, n) + for i, index := range indexSample { + out[i] = set[index] + } + return out +} + +func shuffleStrings(set []string) { + rand.Shuffle(len(set), func(i, j int) { set[i], set[j] = set[j], set[i] }) +} diff --git a/network/p2p/peerstore/peerstore_test.go b/network/p2p/peerstore/peerstore_test.go index 2debcae255..4263564c34 100644 --- a/network/p2p/peerstore/peerstore_test.go +++ b/network/p2p/peerstore/peerstore_test.go @@ -19,7 +19,9 @@ package peerstore import ( "crypto/rand" "fmt" + "math" "testing" + "time" "github.com/algorand/go-algorand/test/partitiontest" libp2p_crypto "github.com/libp2p/go-libp2p/core/crypto" @@ -28,6 +30,13 @@ import ( "github.com/stretchr/testify/require" ) +// PhoneBookEntryRelayRole used for all the relays that are provided either via the algobootstrap SRV record +// or via a configuration file. +const PhoneBookEntryRelayRole = 1 + +// PhoneBookEntryArchiverRole used for all the archivers that are provided via the archive SRV record. +const PhoneBookEntryArchiverRole = 2 + func TestPeerstore(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() @@ -77,3 +86,342 @@ func TestPeerstore(t *testing.T) { require.Equal(t, 7, len(peers)) } + +func testPhonebookAll(t *testing.T, set []string, ph *PeerStore) { + actual := ph.GetAddresses(len(set), PhoneBookEntryRelayRole) + for _, got := range actual { + ok := false + for _, known := range set { + if got == known { + ok = true + break + } + } + if !ok { + t.Errorf("get returned junk %#v", got) + } + } + for _, known := range set { + ok := false + for _, got := range actual { + if got == known { + ok = true + break + } + } + if !ok { + t.Errorf("get missed %#v; actual=%#v; set=%#v", known, actual, set) + } + } +} + +func testPhonebookUniform(t *testing.T, set []string, ph *PeerStore, getsize int) { + uniformityTestLength := 250000 / len(set) + expected := (uniformityTestLength * getsize) / len(set) + counts := make(map[string]int) + for i := 0; i < len(set); i++ { + counts[set[i]] = 0 + } + for i := 0; i < uniformityTestLength; i++ { + actual := ph.GetAddresses(getsize, PhoneBookEntryRelayRole) + for _, xa := range actual { + if _, ok := counts[xa]; ok { + counts[xa]++ + } + } + } + min, max := math.MaxInt, 0 + for _, count := range counts { + if count > max { + max = count + } + if count < min { + min = count + } + } + // TODO: what's a good probability-theoretic threshold for good enough? + if max-min > (expected / 5) { + t.Errorf("counts %#v", counts) + } +} + +func TestArrayPhonebookAll(t *testing.T) { + partitiontest.PartitionTest(t) + + set := []string{"a:4041", "b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} + ph, err := MakePhonebook(1, 1*time.Millisecond) + require.NoError(t, err) + for _, addr := range set { + entry := makePhonebookEntryData("", PhoneBookEntryRelayRole, false) + info, _ := PeerInfoFromDomainPort(addr) + ph.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) + ph.Put(info.ID, addressDataKey, entry) + } + testPhonebookAll(t, set, ph) +} + +func TestArrayPhonebookUniform1(t *testing.T) { + partitiontest.PartitionTest(t) + + set := []string{"a:4041", "b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} + ph, err := MakePhonebook(1, 1*time.Millisecond) + require.NoError(t, err) + for _, addr := range set { + entry := makePhonebookEntryData("", PhoneBookEntryRelayRole, false) + info, _ := PeerInfoFromDomainPort(addr) + ph.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) + ph.Put(info.ID, addressDataKey, entry) + } + testPhonebookUniform(t, set, ph, 1) +} + +func TestArrayPhonebookUniform3(t *testing.T) { + partitiontest.PartitionTest(t) + + set := []string{"a:4041", "b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} + ph, err := MakePhonebook(1, 1*time.Millisecond) + require.NoError(t, err) + for _, addr := range set { + entry := makePhonebookEntryData("", PhoneBookEntryRelayRole, false) + info, _ := PeerInfoFromDomainPort(addr) + ph.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) + ph.Put(info.ID, addressDataKey, entry) + } + testPhonebookUniform(t, set, ph, 3) +} + +func TestMultiPhonebook(t *testing.T) { + partitiontest.PartitionTest(t) + + set := []string{"a:4041", "b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} + pha := make([]string, 0) + for _, e := range set[:5] { + pha = append(pha, e) + } + phb := make([]string, 0) + for _, e := range set[5:] { + phb = append(phb, e) + } + + ph, err := MakePhonebook(1, 1*time.Millisecond) + require.NoError(t, err) + ph.ReplacePeerList(pha, "pha", PhoneBookEntryRelayRole) + ph.ReplacePeerList(phb, "phb", PhoneBookEntryRelayRole) + + testPhonebookAll(t, set, ph) + testPhonebookUniform(t, set, ph, 1) + testPhonebookUniform(t, set, ph, 3) +} + +// TestMultiPhonebookPersistentPeers validates that the peers added via Phonebook.AddPersistentPeers +// are not replaced when Phonebook.ReplacePeerList is called +func TestMultiPhonebookPersistentPeers(t *testing.T) { + partitiontest.PartitionTest(t) + + persistentPeers := []string{"a:4041"} + set := []string{"b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} + pha := make([]string, 0) + for _, e := range set[:5] { + pha = append(pha, e) + } + phb := make([]string, 0) + for _, e := range set[5:] { + phb = append(phb, e) + } + ph, err := MakePhonebook(1, 1*time.Millisecond) + require.NoError(t, err) + ph.AddPersistentPeers(persistentPeers, "pha", PhoneBookEntryRelayRole) + ph.AddPersistentPeers(persistentPeers, "phb", PhoneBookEntryRelayRole) + ph.ReplacePeerList(pha, "pha", PhoneBookEntryRelayRole) + ph.ReplacePeerList(phb, "phb", PhoneBookEntryRelayRole) + + testPhonebookAll(t, append(set, persistentPeers...), ph) + allAddresses := ph.GetAddresses(len(set)+len(persistentPeers), PhoneBookEntryRelayRole) + for _, pp := range persistentPeers { + require.Contains(t, allAddresses, pp) + } +} + +func TestMultiPhonebookDuplicateFiltering(t *testing.T) { + partitiontest.PartitionTest(t) + + set := []string{"b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} + pha := make([]string, 0) + for _, e := range set[:7] { + pha = append(pha, e) + } + phb := make([]string, 0) + for _, e := range set[3:] { + phb = append(phb, e) + } + ph, err := MakePhonebook(1, 1*time.Millisecond) + require.NoError(t, err) + ph.ReplacePeerList(pha, "pha", PhoneBookEntryRelayRole) + ph.ReplacePeerList(phb, "phb", PhoneBookEntryRelayRole) + + testPhonebookAll(t, set, ph) + testPhonebookUniform(t, set, ph, 1) + testPhonebookUniform(t, set, ph, 3) +} + +func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { + partitiontest.PartitionTest(t) + + // make the connectionsRateLimitingWindow long enough to avoid triggering it when the + // test is running in a slow environment + // The test will artificially simulate time passing + timeUnit := 2000 * time.Second + connectionsRateLimitingWindow := 2 * timeUnit + entries, err := MakePhonebook(3, connectionsRateLimitingWindow) + require.NoError(t, err) + addr1 := "addrABC:4040" + addr2 := "addrXYZ:4041" + info1, _ := PeerInfoFromDomainPort(addr1) + info2, _ := PeerInfoFromDomainPort(addr2) + + // Address not in. Should return false + addrInPhonebook, _, provisionalTime := entries.GetConnectionWaitTime(addr1) + require.Equal(t, false, addrInPhonebook) + require.Equal(t, false, entries.UpdateConnectionTime(addr1, provisionalTime)) + + // Test the addresses are populated in the phonebook and a + // time can be added to one of them + entries.ReplacePeerList([]string{addr1, addr2}, "default", PhoneBookEntryRelayRole) + addrInPhonebook, waitTime, provisionalTime := entries.GetConnectionWaitTime(addr1) + require.Equal(t, true, addrInPhonebook) + require.Equal(t, time.Duration(0), waitTime) + require.Equal(t, true, entries.UpdateConnectionTime(addr1, provisionalTime)) + data, _ := entries.Get(info1.ID, addressDataKey) + require.NotNil(t, data) + ad := data.(addressData) + phBookData := ad.recentConnectionTimes + require.Equal(t, 1, len(phBookData)) + + // simulate passing a unit of time + for rct := range phBookData { + phBookData[rct] = phBookData[rct].Add(-1 * timeUnit) + } + + // add another value to addr + addrInPhonebook, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr1) + require.Equal(t, time.Duration(0), waitTime) + require.Equal(t, true, entries.UpdateConnectionTime(addr1, provisionalTime)) + data, _ = entries.Get(info1.ID, addressDataKey) + ad = data.(addressData) + phBookData = ad.recentConnectionTimes + require.Equal(t, 2, len(phBookData)) + + // simulate passing a unit of time + for rct := range phBookData { + phBookData[rct] = phBookData[rct].Add(-1 * timeUnit) + } + + // the first time should be removed and a new one added + // there should not be any wait + addrInPhonebook, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr1) + require.Equal(t, time.Duration(0), waitTime) + require.Equal(t, true, entries.UpdateConnectionTime(addr1, provisionalTime)) + data, _ = entries.Get(info1.ID, addressDataKey) + ad = data.(addressData) + phBookData2 := ad.recentConnectionTimes + require.Equal(t, 2, len(phBookData2)) + + // make sure the right time was removed + require.Equal(t, phBookData[1], phBookData2[0]) + require.Equal(t, true, phBookData2[0].Before(phBookData2[1])) + + // try requesting from another address, make sure + // a separate array is used for these new requests + + // add 3 values to another address. should not wait + // value 1 + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr2) + require.Equal(t, time.Duration(0), waitTime) + require.Equal(t, true, entries.UpdateConnectionTime(addr2, provisionalTime)) + + // introduce a gap between the two requests so that only the first will be removed later when waited + // simulate passing a unit of time + data2, _ := entries.Get(info2.ID, addressDataKey) + require.NotNil(t, data2) + ad2 := data2.(addressData) + for rct := range ad2.recentConnectionTimes { + ad2.recentConnectionTimes[rct] = ad2.recentConnectionTimes[rct].Add(-1 * timeUnit) + } + + // value 2 + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr2) + require.Equal(t, time.Duration(0), waitTime) + require.Equal(t, true, entries.UpdateConnectionTime(addr2, provisionalTime)) + // value 3 + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr2) + require.Equal(t, time.Duration(0), waitTime) + require.Equal(t, true, entries.UpdateConnectionTime(addr2, provisionalTime)) + + data2, _ = entries.Get(info2.ID, addressDataKey) + ad2 = data2.(addressData) + phBookData = ad2.recentConnectionTimes + // all three times should be queued + require.Equal(t, 3, len(phBookData)) + + // add another element to trigger wait + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr2) + require.Greater(t, int64(waitTime), int64(0)) + // no element should be removed + data2, _ = entries.Get(info2.ID, addressDataKey) + ad2 = data2.(addressData) + phBookData2 = ad2.recentConnectionTimes + require.Equal(t, phBookData[0], phBookData2[0]) + require.Equal(t, phBookData[1], phBookData2[1]) + require.Equal(t, phBookData[2], phBookData2[2]) + // simulate passing of the waitTime duration + for rct := range ad2.recentConnectionTimes { + ad2.recentConnectionTimes[rct] = ad2.recentConnectionTimes[rct].Add(-1 * waitTime) + } + + // The wait should be sufficient + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr2) + require.Equal(t, time.Duration(0), waitTime) + require.Equal(t, true, entries.UpdateConnectionTime(addr2, provisionalTime)) + // only one element should be removed, and one added + data2, _ = entries.Get(info2.ID, addressDataKey) + ad2 = data2.(addressData) + phBookData2 = ad2.recentConnectionTimes + require.Equal(t, 3, len(phBookData2)) + + // make sure the right time was removed + require.Equal(t, phBookData[1], phBookData2[0]) + require.Equal(t, phBookData[2], phBookData2[1]) +} + +// TestPhonebookRoles tests that the filtering by roles for different +// phonebooks entries works as expected. +func TestPhonebookRoles(t *testing.T) { + partitiontest.PartitionTest(t) + + relaysSet := []string{"relay1:4040", "relay2:4041", "relay3:4042"} + archiverSet := []string{"archiver1:1111", "archiver2:1112", "archiver3:1113"} + + ph, err := MakePhonebook(1, 1) + require.NoError(t, err) + ph.ReplacePeerList(relaysSet, "default", PhoneBookEntryRelayRole) + ph.ReplacePeerList(archiverSet, "default", PhoneBookEntryArchiverRole) + require.Equal(t, len(relaysSet)+len(archiverSet), len(ph.Peers())) + require.Equal(t, len(relaysSet)+len(archiverSet), ph.Length()) + + for _, role := range []PhoneBookEntryRoles{PhoneBookEntryRelayRole, PhoneBookEntryArchiverRole} { + for k := 0; k < 100; k++ { + for l := 0; l < 3; l++ { + entries := ph.GetAddresses(l, role) + if role == PhoneBookEntryRelayRole { + for _, entry := range entries { + require.Contains(t, entry, "relay") + } + } else if role == PhoneBookEntryArchiverRole { + for _, entry := range entries { + require.Contains(t, entry, "archiver") + } + } + } + } + } +} diff --git a/network/p2p/peerstore/utils.go b/network/p2p/peerstore/utils.go index eabcccbdae..b96fc1c8e0 100644 --- a/network/p2p/peerstore/utils.go +++ b/network/p2p/peerstore/utils.go @@ -17,6 +17,9 @@ package peerstore import ( + "fmt" + "strings" + "github.com/libp2p/go-libp2p/core/peer" "github.com/multiformats/go-multiaddr" ) @@ -49,3 +52,18 @@ func PeerInfoFromAddr(addr string) (*peer.AddrInfo, error) { } return info, nil } + +// PeerInfoFromDomainPort converts a string of the form domain:port to AddrInfo +func PeerInfoFromDomainPort(domainPort string) (*peer.AddrInfo, error) { + parts := strings.Split(domainPort, ":") + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return nil, fmt.Errorf("invalid domain port string %s, found %d colon-separated parts", domainPort, len(parts)) + } + maddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/dns4/%s/tcp/%s", parts[0], parts[1])) + if err != nil { + return nil, err + } + // These will never have peer IDs + transport, _ := peer.SplitAddr(maddr) + return &peer.AddrInfo{ID: peer.ID(domainPort), Addrs: []multiaddr.Multiaddr{transport}}, nil +} diff --git a/network/phonebook.go b/network/phonebook.go index 0ad7be1a0c..cf189eb6a4 100644 --- a/network/phonebook.go +++ b/network/phonebook.go @@ -25,7 +25,7 @@ import ( "golang.org/x/exp/slices" ) -// when using GetAddresses with getAllAddresses, all the addresses will be retrieved, regardless +// getAllAddresses when using GetAddresses with getAllAddresses, all the addresses will be retrieved, regardless // of how many addresses the phonebook actually has. ( with the retry-after logic applied ) const getAllAddresses = math.MaxInt32