p2p/discv5: added new topic discovery package
This commit is contained in:
committed by
Felix Lange
parent
7db7109a5b
commit
49da42983a
953
p2p/discv5/ticket.go
Normal file
953
p2p/discv5/ticket.go
Normal file
@ -0,0 +1,953 @@
|
||||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package discv5
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
const (
|
||||
ticketTimeBucketLen = time.Minute
|
||||
timeWindow = 10 // * ticketTimeBucketLen
|
||||
wantTicketsInWindow = 10
|
||||
collectFrequency = time.Second * 30
|
||||
registerFrequency = time.Second * 60
|
||||
maxCollectDebt = 10
|
||||
maxRegisterDebt = 5
|
||||
keepTicketConst = time.Minute * 10
|
||||
keepTicketExp = time.Minute * 5
|
||||
targetWaitTime = time.Minute * 10
|
||||
topicQueryTimeout = time.Second * 5
|
||||
topicQueryResend = time.Minute
|
||||
// topic radius detection
|
||||
maxRadius = 0xffffffffffffffff
|
||||
radiusTC = time.Minute * 20
|
||||
radiusBucketsPerBit = 8
|
||||
minSlope = 1
|
||||
minPeakSize = 40
|
||||
maxNoAdjust = 20
|
||||
lookupWidth = 8
|
||||
minRightSum = 20
|
||||
searchForceQuery = 4
|
||||
)
|
||||
|
||||
// timeBucket represents absolute monotonic time in minutes.
|
||||
// It is used as the index into the per-topic ticket buckets.
|
||||
type timeBucket int
|
||||
|
||||
type ticket struct {
|
||||
topics []Topic
|
||||
regTime []mclock.AbsTime // Per-topic local absolute time when the ticket can be used.
|
||||
|
||||
// The serial number that was issued by the server.
|
||||
serial uint32
|
||||
// Used by registrar, tracks absolute time when the ticket was created.
|
||||
issueTime mclock.AbsTime
|
||||
|
||||
// Fields used only by registrants
|
||||
node *Node // the registrar node that signed this ticket
|
||||
refCnt int // tracks number of topics that will be registered using this ticket
|
||||
pong []byte // encoded pong packet signed by the registrar
|
||||
}
|
||||
|
||||
// ticketRef refers to a single topic in a ticket.
|
||||
type ticketRef struct {
|
||||
t *ticket
|
||||
idx int // index of the topic in t.topics and t.regTime
|
||||
}
|
||||
|
||||
func (ref ticketRef) topic() Topic {
|
||||
return ref.t.topics[ref.idx]
|
||||
}
|
||||
|
||||
func (ref ticketRef) topicRegTime() mclock.AbsTime {
|
||||
return ref.t.regTime[ref.idx]
|
||||
}
|
||||
|
||||
func pongToTicket(localTime mclock.AbsTime, topics []Topic, node *Node, p *ingressPacket) (*ticket, error) {
|
||||
wps := p.data.(*pong).WaitPeriods
|
||||
if len(topics) != len(wps) {
|
||||
return nil, fmt.Errorf("bad wait period list: got %d values, want %d", len(topics), len(wps))
|
||||
}
|
||||
if rlpHash(topics) != p.data.(*pong).TopicHash {
|
||||
return nil, fmt.Errorf("bad topic hash")
|
||||
}
|
||||
t := &ticket{
|
||||
issueTime: localTime,
|
||||
node: node,
|
||||
topics: topics,
|
||||
pong: p.rawData,
|
||||
regTime: make([]mclock.AbsTime, len(wps)),
|
||||
}
|
||||
// Convert wait periods to local absolute time.
|
||||
for i, wp := range wps {
|
||||
t.regTime[i] = localTime + mclock.AbsTime(time.Second*time.Duration(wp))
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func ticketToPong(t *ticket, pong *pong) {
|
||||
pong.Expiration = uint64(t.issueTime / mclock.AbsTime(time.Second))
|
||||
pong.TopicHash = rlpHash(t.topics)
|
||||
pong.TicketSerial = t.serial
|
||||
pong.WaitPeriods = make([]uint32, len(t.regTime))
|
||||
for i, regTime := range t.regTime {
|
||||
pong.WaitPeriods[i] = uint32(time.Duration(regTime-t.issueTime) / time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
type ticketStore struct {
|
||||
// radius detector and target address generator
|
||||
// exists for both searched and registered topics
|
||||
radius map[Topic]*topicRadius
|
||||
|
||||
// Contains buckets (for each absolute minute) of tickets
|
||||
// that can be used in that minute.
|
||||
// This is only set if the topic is being registered.
|
||||
tickets map[Topic]topicTickets
|
||||
regtopics []Topic
|
||||
nodes map[*Node]*ticket
|
||||
nodeLastReq map[*Node]reqInfo
|
||||
|
||||
lastBucketFetched timeBucket
|
||||
nextTicketCached *ticketRef
|
||||
nextTicketReg mclock.AbsTime
|
||||
|
||||
searchTopicMap map[Topic]searchTopic
|
||||
searchTopicList []Topic
|
||||
searchTopicPtr int
|
||||
nextTopicQueryCleanup mclock.AbsTime
|
||||
queriesSent map[*Node]map[common.Hash]sentQuery
|
||||
radiusLookupCnt int
|
||||
}
|
||||
|
||||
type searchTopic struct {
|
||||
foundChn chan<- string
|
||||
listIdx int
|
||||
}
|
||||
|
||||
type sentQuery struct {
|
||||
sent mclock.AbsTime
|
||||
lookup lookupInfo
|
||||
}
|
||||
|
||||
type topicTickets struct {
|
||||
buckets map[timeBucket][]ticketRef
|
||||
nextLookup, nextReg mclock.AbsTime
|
||||
}
|
||||
|
||||
func newTicketStore() *ticketStore {
|
||||
return &ticketStore{
|
||||
radius: make(map[Topic]*topicRadius),
|
||||
tickets: make(map[Topic]topicTickets),
|
||||
nodes: make(map[*Node]*ticket),
|
||||
nodeLastReq: make(map[*Node]reqInfo),
|
||||
searchTopicMap: make(map[Topic]searchTopic),
|
||||
queriesSent: make(map[*Node]map[common.Hash]sentQuery),
|
||||
}
|
||||
}
|
||||
|
||||
// addTopic starts tracking a topic. If register is true,
|
||||
// the local node will register the topic and tickets will be collected.
|
||||
func (s *ticketStore) addTopic(t Topic, register bool) {
|
||||
debugLog(fmt.Sprintf(" addTopic(%v, %v)", t, register))
|
||||
if s.radius[t] == nil {
|
||||
s.radius[t] = newTopicRadius(t)
|
||||
}
|
||||
if register && s.tickets[t].buckets == nil {
|
||||
s.tickets[t] = topicTickets{buckets: make(map[timeBucket][]ticketRef)}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ticketStore) addSearchTopic(t Topic, foundChn chan<- string) {
|
||||
s.addTopic(t, false)
|
||||
if s.searchTopicMap[t].foundChn == nil {
|
||||
s.searchTopicList = append(s.searchTopicList, t)
|
||||
s.searchTopicMap[t] = searchTopic{foundChn: foundChn, listIdx: len(s.searchTopicList) - 1}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ticketStore) removeSearchTopic(t Topic) {
|
||||
if st := s.searchTopicMap[t]; st.foundChn != nil {
|
||||
lastIdx := len(s.searchTopicList) - 1
|
||||
lastTopic := s.searchTopicList[lastIdx]
|
||||
s.searchTopicList[st.listIdx] = lastTopic
|
||||
sl := s.searchTopicMap[lastTopic]
|
||||
sl.listIdx = st.listIdx
|
||||
s.searchTopicMap[lastTopic] = sl
|
||||
s.searchTopicList = s.searchTopicList[:lastIdx]
|
||||
delete(s.searchTopicMap, t)
|
||||
}
|
||||
}
|
||||
|
||||
// removeRegisterTopic deletes all tickets for the given topic.
|
||||
func (s *ticketStore) removeRegisterTopic(topic Topic) {
|
||||
debugLog(fmt.Sprintf(" removeRegisterTopic(%v)", topic))
|
||||
for _, list := range s.tickets[topic].buckets {
|
||||
for _, ref := range list {
|
||||
ref.t.refCnt--
|
||||
if ref.t.refCnt == 0 {
|
||||
delete(s.nodes, ref.t.node)
|
||||
delete(s.nodeLastReq, ref.t.node)
|
||||
}
|
||||
}
|
||||
}
|
||||
delete(s.tickets, topic)
|
||||
}
|
||||
|
||||
func (s *ticketStore) regTopicSet() []Topic {
|
||||
topics := make([]Topic, 0, len(s.tickets))
|
||||
for topic := range s.tickets {
|
||||
topics = append(topics, topic)
|
||||
}
|
||||
return topics
|
||||
}
|
||||
|
||||
// nextRegisterLookup returns the target of the next lookup for ticket collection.
|
||||
func (s *ticketStore) nextRegisterLookup() (lookup lookupInfo, delay time.Duration) {
|
||||
debugLog("nextRegisterLookup()")
|
||||
firstTopic, ok := s.iterRegTopics()
|
||||
for topic := firstTopic; ok; {
|
||||
debugLog(fmt.Sprintf(" checking topic %v, len(s.tickets[topic]) = %d", topic, len(s.tickets[topic].buckets)))
|
||||
if s.tickets[topic].buckets != nil && s.needMoreTickets(topic) {
|
||||
next := s.radius[topic].nextTarget(false)
|
||||
debugLog(fmt.Sprintf(" %x 1s", next.target[:8]))
|
||||
return next, 100 * time.Millisecond
|
||||
}
|
||||
topic, ok = s.iterRegTopics()
|
||||
if topic == firstTopic {
|
||||
break // We have checked all topics.
|
||||
}
|
||||
}
|
||||
debugLog(" null, 40s")
|
||||
return lookupInfo{}, 40 * time.Second
|
||||
}
|
||||
|
||||
func (s *ticketStore) nextSearchLookup() lookupInfo {
|
||||
if len(s.searchTopicList) == 0 {
|
||||
return lookupInfo{}
|
||||
}
|
||||
if s.searchTopicPtr >= len(s.searchTopicList) {
|
||||
s.searchTopicPtr = 0
|
||||
}
|
||||
topic := s.searchTopicList[s.searchTopicPtr]
|
||||
s.searchTopicPtr++
|
||||
target := s.radius[topic].nextTarget(s.radiusLookupCnt >= searchForceQuery)
|
||||
if target.radiusLookup {
|
||||
s.radiusLookupCnt++
|
||||
} else {
|
||||
s.radiusLookupCnt = 0
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
// iterRegTopics returns topics to register in arbitrary order.
|
||||
// The second return value is false if there are no topics.
|
||||
func (s *ticketStore) iterRegTopics() (Topic, bool) {
|
||||
debugLog("iterRegTopics()")
|
||||
if len(s.regtopics) == 0 {
|
||||
if len(s.tickets) == 0 {
|
||||
debugLog(" false")
|
||||
return "", false
|
||||
}
|
||||
// Refill register list.
|
||||
for t := range s.tickets {
|
||||
s.regtopics = append(s.regtopics, t)
|
||||
}
|
||||
}
|
||||
topic := s.regtopics[len(s.regtopics)-1]
|
||||
s.regtopics = s.regtopics[:len(s.regtopics)-1]
|
||||
debugLog(" " + string(topic) + " true")
|
||||
return topic, true
|
||||
}
|
||||
|
||||
func (s *ticketStore) needMoreTickets(t Topic) bool {
|
||||
return s.tickets[t].nextLookup < mclock.Now()
|
||||
}
|
||||
|
||||
// ticketsInWindow returns the tickets of a given topic in the registration window.
|
||||
func (s *ticketStore) ticketsInWindow(t Topic) []ticketRef {
|
||||
ltBucket := s.lastBucketFetched
|
||||
var res []ticketRef
|
||||
tickets := s.tickets[t].buckets
|
||||
for g := ltBucket; g < ltBucket+timeWindow; g++ {
|
||||
res = append(res, tickets[g]...)
|
||||
}
|
||||
debugLog(fmt.Sprintf("ticketsInWindow(%v) = %v", t, len(res)))
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *ticketStore) removeExcessTickets(t Topic) {
|
||||
tickets := s.ticketsInWindow(t)
|
||||
if len(tickets) <= wantTicketsInWindow {
|
||||
return
|
||||
}
|
||||
sort.Sort(ticketRefByWaitTime(tickets))
|
||||
for _, r := range tickets[wantTicketsInWindow:] {
|
||||
s.removeTicketRef(r)
|
||||
}
|
||||
}
|
||||
|
||||
type ticketRefByWaitTime []ticketRef
|
||||
|
||||
// Len is the number of elements in the collection.
|
||||
func (s ticketRefByWaitTime) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (r ticketRef) waitTime() mclock.AbsTime {
|
||||
return r.t.regTime[r.idx] - r.t.issueTime
|
||||
}
|
||||
|
||||
// Less reports whether the element with
|
||||
// index i should sort before the element with index j.
|
||||
func (s ticketRefByWaitTime) Less(i, j int) bool {
|
||||
return s[i].waitTime() < s[j].waitTime()
|
||||
}
|
||||
|
||||
// Swap swaps the elements with indexes i and j.
|
||||
func (s ticketRefByWaitTime) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
func (s *ticketStore) addTicketRef(r ticketRef) {
|
||||
topic := r.t.topics[r.idx]
|
||||
t := s.tickets[topic]
|
||||
if t.buckets == nil {
|
||||
return
|
||||
}
|
||||
bucket := timeBucket(r.t.regTime[r.idx] / mclock.AbsTime(ticketTimeBucketLen))
|
||||
t.buckets[bucket] = append(t.buckets[bucket], r)
|
||||
r.t.refCnt++
|
||||
|
||||
min := mclock.Now() - mclock.AbsTime(collectFrequency)*maxCollectDebt
|
||||
if t.nextLookup < min {
|
||||
t.nextLookup = min
|
||||
}
|
||||
t.nextLookup += mclock.AbsTime(collectFrequency)
|
||||
s.tickets[topic] = t
|
||||
|
||||
//s.removeExcessTickets(topic)
|
||||
}
|
||||
|
||||
func (s *ticketStore) nextFilteredTicket() (t *ticketRef, wait time.Duration) {
|
||||
now := mclock.Now()
|
||||
for {
|
||||
t, wait = s.nextRegisterableTicket()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
regTime := now + mclock.AbsTime(wait)
|
||||
topic := t.t.topics[t.idx]
|
||||
if regTime >= s.tickets[topic].nextReg {
|
||||
return
|
||||
}
|
||||
s.removeTicketRef(*t)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ticketStore) ticketRegistered(t ticketRef) {
|
||||
now := mclock.Now()
|
||||
|
||||
topic := t.t.topics[t.idx]
|
||||
tt := s.tickets[topic]
|
||||
min := now - mclock.AbsTime(registerFrequency)*maxRegisterDebt
|
||||
if min > tt.nextReg {
|
||||
tt.nextReg = min
|
||||
}
|
||||
tt.nextReg += mclock.AbsTime(registerFrequency)
|
||||
s.tickets[topic] = tt
|
||||
|
||||
s.removeTicketRef(t)
|
||||
}
|
||||
|
||||
// nextRegisterableTicket returns the next ticket that can be used
|
||||
// to register.
|
||||
//
|
||||
// If the returned wait time <= zero the ticket can be used. For a positive
|
||||
// wait time, the caller should requery the next ticket later.
|
||||
//
|
||||
// A ticket can be returned more than once with <= zero wait time in case
|
||||
// the ticket contains multiple topics.
|
||||
func (s *ticketStore) nextRegisterableTicket() (t *ticketRef, wait time.Duration) {
|
||||
defer func() {
|
||||
if t == nil {
|
||||
debugLog(" nil")
|
||||
} else {
|
||||
debugLog(fmt.Sprintf(" node = %x sn = %v wait = %v", t.t.node.ID[:8], t.t.serial, wait))
|
||||
}
|
||||
}()
|
||||
|
||||
debugLog("nextRegisterableTicket()")
|
||||
now := mclock.Now()
|
||||
if s.nextTicketCached != nil {
|
||||
return s.nextTicketCached, time.Duration(s.nextTicketCached.topicRegTime() - now)
|
||||
}
|
||||
|
||||
for bucket := s.lastBucketFetched; ; bucket++ {
|
||||
var (
|
||||
empty = true // true if there are no tickets
|
||||
nextTicket ticketRef // uninitialized if this bucket is empty
|
||||
)
|
||||
for _, tickets := range s.tickets {
|
||||
//s.removeExcessTickets(topic)
|
||||
if len(tickets.buckets) != 0 {
|
||||
empty = false
|
||||
if list := tickets.buckets[bucket]; list != nil {
|
||||
for _, ref := range list {
|
||||
//debugLog(fmt.Sprintf(" nrt bucket = %d node = %x sn = %v wait = %v", bucket, ref.t.node.ID[:8], ref.t.serial, time.Duration(ref.topicRegTime()-now)))
|
||||
if nextTicket.t == nil || ref.topicRegTime() < nextTicket.topicRegTime() {
|
||||
nextTicket = ref
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if empty {
|
||||
return nil, 0
|
||||
}
|
||||
if nextTicket.t != nil {
|
||||
wait = time.Duration(nextTicket.topicRegTime() - now)
|
||||
s.nextTicketCached = &nextTicket
|
||||
return &nextTicket, wait
|
||||
}
|
||||
s.lastBucketFetched = bucket
|
||||
}
|
||||
}
|
||||
|
||||
// removeTicket removes a ticket from the ticket store
|
||||
func (s *ticketStore) removeTicketRef(ref ticketRef) {
|
||||
debugLog(fmt.Sprintf("removeTicketRef(node = %x sn = %v)", ref.t.node.ID[:8], ref.t.serial))
|
||||
topic := ref.topic()
|
||||
tickets := s.tickets[topic].buckets
|
||||
if tickets == nil {
|
||||
return
|
||||
}
|
||||
bucket := timeBucket(ref.t.regTime[ref.idx] / mclock.AbsTime(ticketTimeBucketLen))
|
||||
list := tickets[bucket]
|
||||
idx := -1
|
||||
for i, bt := range list {
|
||||
if bt.t == ref.t {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx == -1 {
|
||||
panic(nil)
|
||||
}
|
||||
list = append(list[:idx], list[idx+1:]...)
|
||||
if len(list) != 0 {
|
||||
tickets[bucket] = list
|
||||
} else {
|
||||
delete(tickets, bucket)
|
||||
}
|
||||
ref.t.refCnt--
|
||||
if ref.t.refCnt == 0 {
|
||||
delete(s.nodes, ref.t.node)
|
||||
delete(s.nodeLastReq, ref.t.node)
|
||||
}
|
||||
|
||||
// Make nextRegisterableTicket return the next available ticket.
|
||||
s.nextTicketCached = nil
|
||||
}
|
||||
|
||||
type lookupInfo struct {
|
||||
target common.Hash
|
||||
topic Topic
|
||||
radiusLookup bool
|
||||
}
|
||||
|
||||
type reqInfo struct {
|
||||
pingHash []byte
|
||||
lookup lookupInfo
|
||||
time mclock.AbsTime
|
||||
}
|
||||
|
||||
// returns -1 if not found
|
||||
func (t *ticket) findIdx(topic Topic) int {
|
||||
for i, tt := range t.topics {
|
||||
if tt == topic {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (s *ticketStore) registerLookupDone(lookup lookupInfo, nodes []*Node, ping func(n *Node) []byte) {
|
||||
now := mclock.Now()
|
||||
for i, n := range nodes {
|
||||
if i == 0 || (binary.BigEndian.Uint64(n.sha[:8])^binary.BigEndian.Uint64(lookup.target[:8])) < s.radius[lookup.topic].minRadius {
|
||||
if lookup.radiusLookup {
|
||||
if lastReq, ok := s.nodeLastReq[n]; !ok || time.Duration(now-lastReq.time) > radiusTC {
|
||||
s.nodeLastReq[n] = reqInfo{pingHash: ping(n), lookup: lookup, time: now}
|
||||
}
|
||||
} else {
|
||||
if s.nodes[n] == nil {
|
||||
s.nodeLastReq[n] = reqInfo{pingHash: ping(n), lookup: lookup, time: now}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ticketStore) searchLookupDone(lookup lookupInfo, nodes []*Node, ping func(n *Node) []byte, query func(n *Node, topic Topic) []byte) {
|
||||
now := mclock.Now()
|
||||
for i, n := range nodes {
|
||||
if i == 0 || (binary.BigEndian.Uint64(n.sha[:8])^binary.BigEndian.Uint64(lookup.target[:8])) < s.radius[lookup.topic].minRadius {
|
||||
if lookup.radiusLookup {
|
||||
if lastReq, ok := s.nodeLastReq[n]; !ok || time.Duration(now-lastReq.time) > radiusTC {
|
||||
s.nodeLastReq[n] = reqInfo{pingHash: ping(n), lookup: lookup, time: now}
|
||||
}
|
||||
} // else {
|
||||
if s.canQueryTopic(n, lookup.topic) {
|
||||
hash := query(n, lookup.topic)
|
||||
s.addTopicQuery(common.BytesToHash(hash), n, lookup)
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ticketStore) adjustWithTicket(now mclock.AbsTime, targetHash common.Hash, t *ticket) {
|
||||
for i, topic := range t.topics {
|
||||
if tt, ok := s.radius[topic]; ok {
|
||||
tt.adjustWithTicket(now, targetHash, ticketRef{t, i})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ticketStore) addTicket(localTime mclock.AbsTime, pingHash []byte, t *ticket) {
|
||||
debugLog(fmt.Sprintf("add(node = %x sn = %v)", t.node.ID[:8], t.serial))
|
||||
|
||||
lastReq, ok := s.nodeLastReq[t.node]
|
||||
if !(ok && bytes.Equal(pingHash, lastReq.pingHash)) {
|
||||
return
|
||||
}
|
||||
s.adjustWithTicket(localTime, lastReq.lookup.target, t)
|
||||
|
||||
if lastReq.lookup.radiusLookup || s.nodes[t.node] != nil {
|
||||
return
|
||||
}
|
||||
|
||||
topic := lastReq.lookup.topic
|
||||
topicIdx := t.findIdx(topic)
|
||||
if topicIdx == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
bucket := timeBucket(localTime / mclock.AbsTime(ticketTimeBucketLen))
|
||||
if s.lastBucketFetched == 0 || bucket < s.lastBucketFetched {
|
||||
s.lastBucketFetched = bucket
|
||||
}
|
||||
|
||||
if _, ok := s.tickets[topic]; ok {
|
||||
wait := t.regTime[topicIdx] - localTime
|
||||
rnd := rand.ExpFloat64()
|
||||
if rnd > 10 {
|
||||
rnd = 10
|
||||
}
|
||||
if float64(wait) < float64(keepTicketConst)+float64(keepTicketExp)*rnd {
|
||||
// use the ticket to register this topic
|
||||
//fmt.Println("addTicket", t.node.ID[:8], t.node.addr().String(), t.serial, t.pong)
|
||||
s.addTicketRef(ticketRef{t, topicIdx})
|
||||
}
|
||||
}
|
||||
|
||||
if t.refCnt > 0 {
|
||||
s.nextTicketCached = nil
|
||||
s.nodes[t.node] = t
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ticketStore) getNodeTicket(node *Node) *ticket {
|
||||
if s.nodes[node] == nil {
|
||||
debugLog(fmt.Sprintf("getNodeTicket(%x) sn = nil", node.ID[:8]))
|
||||
} else {
|
||||
debugLog(fmt.Sprintf("getNodeTicket(%x) sn = %v", node.ID[:8], s.nodes[node].serial))
|
||||
}
|
||||
return s.nodes[node]
|
||||
}
|
||||
|
||||
func (s *ticketStore) canQueryTopic(node *Node, topic Topic) bool {
|
||||
qq := s.queriesSent[node]
|
||||
if qq != nil {
|
||||
now := mclock.Now()
|
||||
for _, sq := range qq {
|
||||
if sq.lookup.topic == topic && sq.sent > now-mclock.AbsTime(topicQueryResend) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *ticketStore) addTopicQuery(hash common.Hash, node *Node, lookup lookupInfo) {
|
||||
now := mclock.Now()
|
||||
qq := s.queriesSent[node]
|
||||
if qq == nil {
|
||||
qq = make(map[common.Hash]sentQuery)
|
||||
s.queriesSent[node] = qq
|
||||
}
|
||||
qq[hash] = sentQuery{sent: now, lookup: lookup}
|
||||
s.cleanupTopicQueries(now)
|
||||
}
|
||||
|
||||
func (s *ticketStore) cleanupTopicQueries(now mclock.AbsTime) {
|
||||
if s.nextTopicQueryCleanup > now {
|
||||
return
|
||||
}
|
||||
exp := now - mclock.AbsTime(topicQueryResend)
|
||||
for n, qq := range s.queriesSent {
|
||||
for h, q := range qq {
|
||||
if q.sent < exp {
|
||||
delete(qq, h)
|
||||
}
|
||||
}
|
||||
if len(qq) == 0 {
|
||||
delete(s.queriesSent, n)
|
||||
}
|
||||
}
|
||||
s.nextTopicQueryCleanup = now + mclock.AbsTime(topicQueryTimeout)
|
||||
}
|
||||
|
||||
func (s *ticketStore) gotTopicNodes(from *Node, hash common.Hash, nodes []rpcNode) (timeout bool) {
|
||||
now := mclock.Now()
|
||||
//fmt.Println("got", from.addr().String(), hash, len(nodes))
|
||||
qq := s.queriesSent[from]
|
||||
if qq == nil {
|
||||
return true
|
||||
}
|
||||
q, ok := qq[hash]
|
||||
if !ok || now > q.sent+mclock.AbsTime(topicQueryTimeout) {
|
||||
return true
|
||||
}
|
||||
inside := float64(0)
|
||||
if len(nodes) > 0 {
|
||||
inside = 1
|
||||
}
|
||||
s.radius[q.lookup.topic].adjust(now, q.lookup.target, from.sha, inside)
|
||||
chn := s.searchTopicMap[q.lookup.topic].foundChn
|
||||
if chn == nil {
|
||||
//fmt.Println("no channel")
|
||||
return false
|
||||
}
|
||||
for _, node := range nodes {
|
||||
ip := node.IP
|
||||
if ip.IsUnspecified() || ip.IsLoopback() {
|
||||
ip = from.IP
|
||||
}
|
||||
enode := NewNode(node.ID, ip, node.UDP-1, node.TCP-1).String() // subtract one from port while discv5 is running in test mode on UDPport+1
|
||||
select {
|
||||
case chn <- enode:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type topicRadius struct {
|
||||
topic Topic
|
||||
topicHashPrefix uint64
|
||||
radius, minRadius uint64
|
||||
buckets []topicRadiusBucket
|
||||
}
|
||||
|
||||
type topicRadiusEvent int
|
||||
|
||||
const (
|
||||
trOutside topicRadiusEvent = iota
|
||||
trInside
|
||||
trNoAdjust
|
||||
trCount
|
||||
)
|
||||
|
||||
type topicRadiusBucket struct {
|
||||
weights [trCount]float64
|
||||
lastTime mclock.AbsTime
|
||||
value float64
|
||||
lookupSent map[common.Hash]mclock.AbsTime
|
||||
}
|
||||
|
||||
func (b *topicRadiusBucket) update(now mclock.AbsTime) {
|
||||
if now == b.lastTime {
|
||||
return
|
||||
}
|
||||
exp := math.Exp(-float64(now-b.lastTime) / float64(radiusTC))
|
||||
for i, w := range b.weights {
|
||||
b.weights[i] = w * exp
|
||||
}
|
||||
b.lastTime = now
|
||||
|
||||
for target, tm := range b.lookupSent {
|
||||
if now-tm > mclock.AbsTime(pingTimeout) {
|
||||
b.weights[trNoAdjust] += 1
|
||||
delete(b.lookupSent, target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *topicRadiusBucket) adjust(now mclock.AbsTime, inside float64) {
|
||||
b.update(now)
|
||||
if inside <= 0 {
|
||||
b.weights[trOutside] += 1
|
||||
} else {
|
||||
if inside >= 1 {
|
||||
b.weights[trInside] += 1
|
||||
} else {
|
||||
b.weights[trInside] += inside
|
||||
b.weights[trOutside] += 1 - inside
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTopicRadius(t Topic) *topicRadius {
|
||||
topicHash := crypto.Keccak256Hash([]byte(t))
|
||||
topicHashPrefix := binary.BigEndian.Uint64(topicHash[0:8])
|
||||
|
||||
return &topicRadius{
|
||||
topic: t,
|
||||
topicHashPrefix: topicHashPrefix,
|
||||
radius: maxRadius,
|
||||
minRadius: maxRadius,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *topicRadius) getBucketIdx(addrHash common.Hash) int {
|
||||
prefix := binary.BigEndian.Uint64(addrHash[0:8])
|
||||
var log2 float64
|
||||
if prefix != r.topicHashPrefix {
|
||||
log2 = math.Log2(float64(prefix ^ r.topicHashPrefix))
|
||||
}
|
||||
bucket := int((64 - log2) * radiusBucketsPerBit)
|
||||
max := 64*radiusBucketsPerBit - 1
|
||||
if bucket > max {
|
||||
return max
|
||||
}
|
||||
if bucket < 0 {
|
||||
return 0
|
||||
}
|
||||
return bucket
|
||||
}
|
||||
|
||||
func (r *topicRadius) targetForBucket(bucket int) common.Hash {
|
||||
min := math.Pow(2, 64-float64(bucket+1)/radiusBucketsPerBit)
|
||||
max := math.Pow(2, 64-float64(bucket)/radiusBucketsPerBit)
|
||||
a := uint64(min)
|
||||
b := randUint64n(uint64(max - min))
|
||||
xor := a + b
|
||||
if xor < a {
|
||||
xor = ^uint64(0)
|
||||
}
|
||||
prefix := r.topicHashPrefix ^ xor
|
||||
var target common.Hash
|
||||
binary.BigEndian.PutUint64(target[0:8], prefix)
|
||||
rand.Read(target[8:])
|
||||
return target
|
||||
}
|
||||
|
||||
func (r *topicRadius) isInRadius(addrHash common.Hash) bool {
|
||||
nodePrefix := binary.BigEndian.Uint64(addrHash[0:8])
|
||||
dist := nodePrefix ^ r.topicHashPrefix
|
||||
return dist < r.radius
|
||||
}
|
||||
|
||||
func (r *topicRadius) chooseLookupBucket(a, b int) int {
|
||||
if a < 0 {
|
||||
a = 0
|
||||
}
|
||||
if a > b {
|
||||
return -1
|
||||
}
|
||||
c := 0
|
||||
for i := a; i <= b; i++ {
|
||||
if i >= len(r.buckets) || r.buckets[i].weights[trNoAdjust] < maxNoAdjust {
|
||||
c++
|
||||
}
|
||||
}
|
||||
if c == 0 {
|
||||
return -1
|
||||
}
|
||||
rnd := randUint(uint32(c))
|
||||
for i := a; i <= b; i++ {
|
||||
if i >= len(r.buckets) || r.buckets[i].weights[trNoAdjust] < maxNoAdjust {
|
||||
if rnd == 0 {
|
||||
return i
|
||||
}
|
||||
rnd--
|
||||
}
|
||||
}
|
||||
panic(nil) // should never happen
|
||||
}
|
||||
|
||||
func (r *topicRadius) needMoreLookups(a, b int, maxValue float64) bool {
|
||||
var max float64
|
||||
if a < 0 {
|
||||
a = 0
|
||||
}
|
||||
if b >= len(r.buckets) {
|
||||
b = len(r.buckets) - 1
|
||||
if r.buckets[b].value > max {
|
||||
max = r.buckets[b].value
|
||||
}
|
||||
}
|
||||
if b >= a {
|
||||
for i := a; i <= b; i++ {
|
||||
if r.buckets[i].value > max {
|
||||
max = r.buckets[i].value
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxValue-max < minPeakSize
|
||||
}
|
||||
|
||||
func (r *topicRadius) recalcRadius() (radius uint64, radiusLookup int) {
|
||||
maxBucket := 0
|
||||
maxValue := float64(0)
|
||||
now := mclock.Now()
|
||||
v := float64(0)
|
||||
for i, _ := range r.buckets {
|
||||
r.buckets[i].update(now)
|
||||
v += r.buckets[i].weights[trOutside] - r.buckets[i].weights[trInside]
|
||||
r.buckets[i].value = v
|
||||
//fmt.Printf("%v %v | ", v, r.buckets[i].weights[trNoAdjust])
|
||||
}
|
||||
//fmt.Println()
|
||||
slopeCross := -1
|
||||
for i, b := range r.buckets {
|
||||
v := b.value
|
||||
if v < float64(i)*minSlope {
|
||||
slopeCross = i
|
||||
break
|
||||
}
|
||||
if v > maxValue {
|
||||
maxValue = v
|
||||
maxBucket = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
minRadBucket := len(r.buckets)
|
||||
sum := float64(0)
|
||||
for minRadBucket > 0 && sum < minRightSum {
|
||||
minRadBucket--
|
||||
b := r.buckets[minRadBucket]
|
||||
sum += b.weights[trInside] + b.weights[trOutside]
|
||||
}
|
||||
r.minRadius = uint64(math.Pow(2, 64-float64(minRadBucket)/radiusBucketsPerBit))
|
||||
|
||||
lookupLeft := -1
|
||||
if r.needMoreLookups(0, maxBucket-lookupWidth-1, maxValue) {
|
||||
lookupLeft = r.chooseLookupBucket(maxBucket-lookupWidth, maxBucket-1)
|
||||
}
|
||||
lookupRight := -1
|
||||
if slopeCross != maxBucket && (minRadBucket <= maxBucket || r.needMoreLookups(maxBucket+lookupWidth, len(r.buckets)-1, maxValue)) {
|
||||
for len(r.buckets) <= maxBucket+lookupWidth {
|
||||
r.buckets = append(r.buckets, topicRadiusBucket{lookupSent: make(map[common.Hash]mclock.AbsTime)})
|
||||
}
|
||||
lookupRight = r.chooseLookupBucket(maxBucket, maxBucket+lookupWidth-1)
|
||||
}
|
||||
if lookupLeft == -1 {
|
||||
radiusLookup = lookupRight
|
||||
} else {
|
||||
if lookupRight == -1 {
|
||||
radiusLookup = lookupLeft
|
||||
} else {
|
||||
if randUint(2) == 0 {
|
||||
radiusLookup = lookupLeft
|
||||
} else {
|
||||
radiusLookup = lookupRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//fmt.Println("mb", maxBucket, "sc", slopeCross, "mrb", minRadBucket, "ll", lookupLeft, "lr", lookupRight, "mv", maxValue)
|
||||
|
||||
if radiusLookup == -1 {
|
||||
// no more radius lookups needed at the moment, return a radius
|
||||
rad := maxBucket
|
||||
if minRadBucket < rad {
|
||||
rad = minRadBucket
|
||||
}
|
||||
radius = ^uint64(0)
|
||||
if rad > 0 {
|
||||
radius = uint64(math.Pow(2, 64-float64(rad)/radiusBucketsPerBit))
|
||||
}
|
||||
r.radius = radius
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *topicRadius) nextTarget(forceRegular bool) lookupInfo {
|
||||
if !forceRegular {
|
||||
_, radiusLookup := r.recalcRadius()
|
||||
if radiusLookup != -1 {
|
||||
target := r.targetForBucket(radiusLookup)
|
||||
r.buckets[radiusLookup].lookupSent[target] = mclock.Now()
|
||||
return lookupInfo{target: target, topic: r.topic, radiusLookup: true}
|
||||
}
|
||||
}
|
||||
|
||||
radExt := r.radius / 2
|
||||
if radExt > maxRadius-r.radius {
|
||||
radExt = maxRadius - r.radius
|
||||
}
|
||||
rnd := randUint64n(r.radius) + randUint64n(2*radExt)
|
||||
if rnd > radExt {
|
||||
rnd -= radExt
|
||||
} else {
|
||||
rnd = radExt - rnd
|
||||
}
|
||||
|
||||
prefix := r.topicHashPrefix ^ rnd
|
||||
var target common.Hash
|
||||
binary.BigEndian.PutUint64(target[0:8], prefix)
|
||||
rand.Read(target[8:])
|
||||
return lookupInfo{target: target, topic: r.topic, radiusLookup: false}
|
||||
}
|
||||
|
||||
func (r *topicRadius) adjustWithTicket(now mclock.AbsTime, targetHash common.Hash, t ticketRef) {
|
||||
wait := t.t.regTime[t.idx] - t.t.issueTime
|
||||
inside := float64(wait)/float64(targetWaitTime) - 0.5
|
||||
if inside > 1 {
|
||||
inside = 1
|
||||
}
|
||||
if inside < 0 {
|
||||
inside = 0
|
||||
}
|
||||
r.adjust(now, targetHash, t.t.node.sha, inside)
|
||||
}
|
||||
|
||||
func (r *topicRadius) adjust(now mclock.AbsTime, targetHash, addrHash common.Hash, inside float64) {
|
||||
bucket := r.getBucketIdx(addrHash)
|
||||
//fmt.Println("adjust", bucket, len(r.buckets), inside)
|
||||
if bucket >= len(r.buckets) {
|
||||
return
|
||||
}
|
||||
r.buckets[bucket].adjust(now, inside)
|
||||
delete(r.buckets[bucket].lookupSent, targetHash)
|
||||
}
|
Reference in New Issue
Block a user