package dht import ( "crypto/rand" "encoding/hex" "sync" "time" "github.com/lbryio/lbry.go/v3/dht/bits" "github.com/lbryio/lbry.go/v3/extras/stop" "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" "go.uber.org/atomic" ) // TODO: iterativeFindValue may be stopping early. if it gets a response with one peer, it should keep going because other nodes may know about more peers that have that blob // TODO: or, it should try a tcp handshake with peers as it finds them, to make sure they are still online and have the blob var cfLog *logrus.Logger func init() { cfLog = logrus.StandardLogger() } func NodeFinderUseLogger(l *logrus.Logger) { cfLog = l } type contactFinder struct { findValue bool // true if we're using findValue target bits.Bitmap node *Node grp *stop.Group findValueMutex *sync.Mutex findValueResult []Contact activeContactsMutex *sync.Mutex activeContacts []Contact shortlistMutex *sync.Mutex shortlist []Contact shortlistAdded map[bits.Bitmap]bool closestContactMutex *sync.RWMutex closestContact *Contact notGettingCloser *atomic.Bool } func FindContacts(node *Node, target bits.Bitmap, findValue bool, parentGrp *stop.Group) ([]Contact, bool, error) { cf := &contactFinder{ node: node, target: target, findValue: findValue, findValueMutex: &sync.Mutex{}, activeContactsMutex: &sync.Mutex{}, shortlistMutex: &sync.Mutex{}, shortlistAdded: make(map[bits.Bitmap]bool), grp: stop.New(parentGrp), closestContactMutex: &sync.RWMutex{}, notGettingCloser: atomic.NewBool(false), } return cf.Find() } func (cf *contactFinder) Stop() { cf.grp.StopAndWait() } func (cf *contactFinder) Find() ([]Contact, bool, error) { if cf.findValue { cf.debug("starting iterativeFindValue") } else { cf.debug("starting iterativeFindNode") } cf.appendNewToShortlist(cf.node.rt.GetClosest(cf.target, alpha)) if len(cf.shortlist) == 0 { return nil, false, errors.WithStack(errors.Newf( "[%s] find %s: no contacts in routing table", cf.node.id.HexShort(), cf.target.HexShort(), )) } go cf.cycle(false) timeout := 5 * time.Second CycleLoop: for { select { case <-time.After(timeout): go cf.cycle(false) case <-cf.grp.Ch(): break CycleLoop } } // TODO: what to do if we have less than K active contacts, shortlist is empty, but we have other contacts in our routing table whom we have not contacted. prolly contact them var contacts []Contact var found bool if cf.findValue && len(cf.findValueResult) > 0 { contacts = cf.findValueResult found = true } else { contacts = cf.activeContacts if len(contacts) > bucketSize { contacts = contacts[:bucketSize] } } cf.Stop() return contacts, found, nil } // cycle does a single cycle of sending alpha probes and checking results against closestNode func (cf *contactFinder) cycle(bigCycle bool) { cycleID := randString(6) if bigCycle { cf.debug("LAUNCHING CYCLE %s, AND ITS A BIG CYCLE", cycleID) } else { cf.debug("LAUNCHING CYCLE %s", cycleID) } defer cf.debug("CYCLE %s DONE", cycleID) cf.closestContactMutex.RLock() closestContact := cf.closestContact cf.closestContactMutex.RUnlock() var wg sync.WaitGroup ch := make(chan *Contact) limit := alpha if bigCycle { limit = bucketSize } for i := 0; i < limit; i++ { wg.Add(1) go func() { defer wg.Done() ch <- cf.probe(cycleID) }() } go func() { wg.Wait() close(ch) }() foundCloser := false for { c, more := <-ch if !more { break } if c != nil && (closestContact == nil || cf.target.Closer(c.ID, closestContact.ID)) { if closestContact != nil { cf.debug("|%s| best contact improved: %s -> %s", cycleID, closestContact.ID.HexShort(), c.ID.HexShort()) } else { cf.debug("|%s| best contact starting at %s", cycleID, c.ID.HexShort()) } foundCloser = true closestContact = c } } if cf.isSearchFinished() { cf.grp.Stop() return } if foundCloser { cf.closestContactMutex.Lock() // have to check again after locking in case other probes found a closer one in the meantime if cf.closestContact == nil || cf.target.Closer(closestContact.ID, cf.closestContact.ID) { cf.closestContact = closestContact } cf.closestContactMutex.Unlock() go cf.cycle(false) } else if !bigCycle { cf.debug("|%s| no improvement, running big cycle", cycleID) go cf.cycle(true) } else { // big cycle ran and there was no improvement, so we're done cf.debug("|%s| big cycle ran, still no improvement", cycleID) cf.notGettingCloser.Store(true) } } // probe sends a single probe, updates the lists, and returns the closest contact it found func (cf *contactFinder) probe(cycleID string) *Contact { maybeContact := cf.popFromShortlist() if maybeContact == nil { cf.debug("|%s| no contacts in shortlist, returning", cycleID) return nil } c := *maybeContact if c.ID.Equals(cf.node.id) { return nil } cf.debug("|%s| probe %s: launching", cycleID, c.ID.HexShort()) req := Request{Arg: &cf.target} if cf.findValue { req.Method = findValueMethod } else { req.Method = findNodeMethod } var res *Response resCh := cf.node.SendAsync(c, req) select { case res = <-resCh: case <-cf.grp.Ch(): cf.debug("|%s| probe %s: canceled", cycleID, c.ID.HexShort()) return nil } if res == nil { cf.debug("|%s| probe %s: req canceled or timed out", cycleID, c.ID.HexShort()) return nil } if cf.findValue && res.FindValueKey != "" { cf.debug("|%s| probe %s: got value", cycleID, c.ID.HexShort()) cf.findValueMutex.Lock() cf.findValueResult = res.Contacts cf.findValueMutex.Unlock() cf.grp.Stop() return nil } cf.debug("|%s| probe %s: got %s", cycleID, c.ID.HexShort(), res.argsDebug()) cf.insertIntoActiveList(c) cf.appendNewToShortlist(res.Contacts) cf.activeContactsMutex.Lock() contacts := cf.activeContacts if len(contacts) > bucketSize { contacts = contacts[:bucketSize] } contactsStr := "" for _, c := range contacts { contactsStr += c.ID.HexShort() + ", " } cf.activeContactsMutex.Unlock() return cf.closest(res.Contacts...) } // appendNewToShortlist appends any new contacts to the shortlist and sorts it by distance // contacts that have already been added to the shortlist in the past are ignored func (cf *contactFinder) appendNewToShortlist(contacts []Contact) { cf.shortlistMutex.Lock() defer cf.shortlistMutex.Unlock() for _, c := range contacts { if _, ok := cf.shortlistAdded[c.ID]; !ok { cf.shortlist = append(cf.shortlist, c) cf.shortlistAdded[c.ID] = true } } sortByDistance(cf.shortlist, cf.target) } // popFromShortlist pops the first contact off the shortlist and returns it func (cf *contactFinder) popFromShortlist() *Contact { cf.shortlistMutex.Lock() defer cf.shortlistMutex.Unlock() if len(cf.shortlist) == 0 { return nil } first := cf.shortlist[0] cf.shortlist = cf.shortlist[1:] return &first } // insertIntoActiveList inserts the contact into appropriate place in the list of active contacts (sorted by distance) func (cf *contactFinder) insertIntoActiveList(contact Contact) { cf.activeContactsMutex.Lock() defer cf.activeContactsMutex.Unlock() inserted := false for i, n := range cf.activeContacts { if cf.target.Closer(contact.ID, n.ID) { cf.activeContacts = append(cf.activeContacts[:i], append([]Contact{contact}, cf.activeContacts[i:]...)...) inserted = true break } } if !inserted { cf.activeContacts = append(cf.activeContacts, contact) } } // isSearchFinished returns true if the search is done and should be stopped func (cf *contactFinder) isSearchFinished() bool { if cf.findValue && len(cf.findValueResult) > 0 { return true } select { case <-cf.grp.Ch(): return true default: } if cf.notGettingCloser.Load() { return true } cf.activeContactsMutex.Lock() defer cf.activeContactsMutex.Unlock() return len(cf.activeContacts) >= bucketSize } func (cf *contactFinder) debug(format string, args ...interface{}) { args = append([]interface{}{cf.node.id.HexShort()}, append([]interface{}{cf.target.HexShort()}, args...)...) cfLog.Debugf("[%s] find %s: "+format, args...) } func (cf *contactFinder) closest(contacts ...Contact) *Contact { if len(contacts) == 0 { return nil } closest := contacts[0] for _, c := range contacts { if cf.target.Closer(c.ID, closest.ID) { closest = c } } return &closest } // randString returns a random alphanumeric string of a given length func randString(length int) string { buf := make([]byte, length/2) _, err := rand.Reader.Read(buf) if err != nil { panic(err) } randStr := hex.EncodeToString(buf)[:length] if len(randStr) < length { panic("Could not create random string that is long enough") } return randStr }