package kademlia import ( "fmt" "sort" // "math" "encoding/json" "io/ioutil" "os" "strings" "sync" "sync/atomic" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/logger" ) var kadlogger = logger.NewLogger("KΛÐ") const ( bucketSize = 20 maxProx = 255 ) type Kademlia struct { // immutable baseparam addr Address // adjustable parameters MaxProx int MaxProxBinSize int BucketSize int currentMaxBucketSize int nodeDB [][]*NodeRecord nodeIndex map[Address]*NodeRecord GetNode func(int) // state proxLimit int proxSize int // count int buckets []*bucket dblock sync.RWMutex lock sync.RWMutex quitC chan bool } type Address common.Hash type Node interface { Addr() Address Url() string LastActive() time.Time Drop() } type NodeRecord struct { Address Address `json:address` Url string `json:url` Active int64 `json:active` node Node } func (self *NodeRecord) setActive() { if self.node != nil { self.Active = self.node.LastActive().UnixNano() } } type kadDB struct { Address Address `json:address` Nodes [][]*NodeRecord `json:nodes` } // public constructor // hash is a byte slice of length equal to self.HashBytes func New() *Kademlia { return &Kademlia{} } // accessor for KAD self address func (self *Kademlia) Addr() Address { return self.addr } // accessor for KAD self count func (self *Kademlia) Count() int { return self.count } // Start brings up a pool of entries potentially from an offline persisted source // and sets default values for optional parameters func (self *Kademlia) Start(addr Address) error { self.lock.Lock() defer self.lock.Unlock() if self.quitC != nil { return nil } self.addr = addr if self.MaxProx == 0 { self.MaxProx = maxProx } if self.BucketSize == 0 { self.BucketSize = bucketSize } // runtime parameters if self.MaxProxBinSize == 0 { self.MaxProxBinSize = self.BucketSize } self.buckets = make([]*bucket, self.MaxProx+1) for i, _ := range self.buckets { self.buckets[i] = &bucket{size: self.BucketSize} // will initialise bucket{int(0),[]Node(nil),sync.Mutex} } self.nodeDB = make([][]*NodeRecord, 8*len(self.addr)) self.nodeIndex = make(map[Address]*NodeRecord) self.quitC = make(chan bool) return nil } // Stop saves the routing table into a persistant form func (self *Kademlia) Stop(path string) (err error) { self.lock.Lock() defer self.lock.Unlock() if self.quitC == nil { return } close(self.quitC) self.quitC = nil if len(path) > 0 { err = self.Save(path) if err != nil { kadlogger.Warnf("unable to save node records: %v", err) } } return } // RemoveNode is the entrypoint where nodes are taken offline func (self *Kademlia) RemoveNode(node Node) (err error) { self.lock.Lock() defer self.lock.Unlock() index := self.proximityBin(node.Addr()) bucket := self.buckets[index] for i := 0; i < len(bucket.nodes); i++ { if node.Addr() == bucket.nodes[i].Addr() { bucket.nodes = append(bucket.nodes[:i], bucket.nodes[(i+1):]...) } } self.count-- if len(bucket.nodes) < bucket.size { err = fmt.Errorf("insufficient nodes (%v) in bucket %v", len(bucket.nodes), index) } if len(bucket.nodes) == 0 { self.adjustProx(index, -1) } // async callback to notify user that bucket needs filling // action is left to the user if self.GetNode != nil { go self.GetNode(index) } return } // AddNode is the entry point where new nodes are registered func (self *Kademlia) AddNode(node Node) (err error) { self.lock.Lock() defer self.lock.Unlock() index := self.proximityBin(node.Addr()) kadlogger.Debugf("bin %d, len: %d\n", index, len(self.buckets)) bucket := self.buckets[index] err = bucket.insert(node) if err != nil { return } self.count++ if index >= self.proxLimit { self.adjustProx(index, 1) } go func() { self.dblock.Lock() defer self.dblock.Unlock() record, found := self.nodeIndex[node.Addr()] if found { record.node = node } else { record = &NodeRecord{ Address: node.Addr(), Url: node.Url(), Active: node.LastActive().UnixNano(), node: node, } self.nodeIndex[node.Addr()] = record self.nodeDB[index] = append(self.nodeDB[index], record) } }() kadlogger.Infof("add peer %v...", node) return } // adjust Prox (proxLimit and proxSize after an insertion of add nodes into bucket r) func (self *Kademlia) adjustProx(r int, add int) { switch { case add > 0 && r == self.proxLimit: self.proxLimit += add for ; self.proxLimit < self.MaxProx && len(self.buckets[self.proxLimit].nodes) > 0; self.proxLimit++ { self.proxSize -= len(self.buckets[self.proxLimit].nodes) } case add > 0 && r > self.proxLimit && self.proxSize+add > self.MaxProxBinSize: self.proxLimit++ self.proxSize -= len(self.buckets[r].nodes) - add case add > 0 && r > self.proxLimit: self.proxSize += add case add < 0 && r < self.proxLimit && len(self.buckets[r].nodes) == 0: for i := self.proxLimit - 1; i > r; i-- { self.proxSize += len(self.buckets[i].nodes) } self.proxLimit = r } } /* GetNodes(target) returns the list of nodes belonging to the same proximity bin as the target. The most proximate bin will be the union of the bins between proxLimit and MaxProx. proxLimit is dynamically adjusted so that 1) there is no empty buckets in bin < proxLimit and 2) the sum of all items are the maximum possible but lower than MaxProxBinSize */ func (self *Kademlia) GetNodes(target Address, max int) []Node { return self.getNodes(target, max).nodes } func (self *Kademlia) getNodes(target Address, max int) (r nodesByDistance) { self.lock.RLock() defer self.lock.RUnlock() r.target = target index := self.proximityBin(target) start := index var down bool if index >= self.proxLimit { index = self.proxLimit start = self.MaxProx down = true } var n int limit := max if max == 0 { limit = 1000 } for { bucket := self.buckets[start].nodes for i := 0; i < len(bucket); i++ { r.push(bucket[i], limit) n++ } if max == 0 && start <= index && n > 0 || max > 0 && down && start <= index && (n >= limit || n == self.Count() || start == 0) { break } if down { start-- } else { if start == self.MaxProx { if index == 0 { break } start = index - 1 down = true } else { start++ } } } return } // this is used to add node records to the persisted db // TODO: maybe db needs to be purged occasionally (reputation will take care of // that) func (self *Kademlia) AddNodeRecords(nrs []*NodeRecord) { self.dblock.Lock() defer self.dblock.Unlock() for _, node := range nrs { _, found := self.nodeIndex[node.Address] if !found { self.nodeIndex[node.Address] = node index := self.proximityBin(node.Address) self.nodeDB[index] = append(self.nodeDB[index], node) } } } /* GetNodeRecords gives back an at most max length slice of node records in order of decreasing priority for desired connection Used to pick candidates for live nodes to satisfy Kademlia network for Swarm Does a round robin on buckets starting from 0 to proxLimit then back on each round i we inspect if live-nodes fill the bucket if len(nodes) + i < currentMaxBucketSize, then take ith element in corresponding db row ordered by reputation (active time?) This has double role. Starting as naive node with empty db, this implements Kademlia bootstrapping As a mature node, it manages quickly fill in blanks or short lines All on demand */ func (self *Kademlia) GetNodeRecords(max int) (nrs []*NodeRecord, err error) { var round int for max > 0 { for i, b := range self.buckets { if len(b.nodes)+round < self.currentMaxBucketSize { if nr := self.getNodeRecord(i, round); nr != nil { nrs = append(nrs) } } } round++ max-- } return } func (self *Kademlia) getNodeRecord(row, col int) (nr *NodeRecord) { if row >= 0 && row < len(self.nodeDB) && col >= 0 && col < len(self.nodeDB[row]) { nr = self.nodeDB[row][col] } return } // in situ mutable bucket type bucket struct { size int nodes []Node lock sync.RWMutex } func (a Address) Bin() string { var bs []string for _, b := range a[:] { bs = append(bs, fmt.Sprintf("%08b", b)) } return strings.Join(bs, "") } // nodesByDistance is a list of nodes, ordered by distance to target. type nodesByDistance struct { nodes []Node target Address } func sortedByDistanceTo(target Address, slice []Node) bool { var last Address for i, node := range slice { if i > 0 { if proxCmp(target, node.Addr(), last) < 0 { return false } } last = node.Addr() } return true } // push(node, max) adds the given node to the list, keeping the total size // below max elements. func (h *nodesByDistance) push(node Node, max int) { // returns the firt index ix such that func(i) returns true ix := sort.Search(len(h.nodes), func(i int) bool { return proxCmp(h.target, h.nodes[i].Addr(), node.Addr()) >= 0 }) if len(h.nodes) < max { h.nodes = append(h.nodes, node) } if ix < len(h.nodes) { copy(h.nodes[ix+1:], h.nodes[ix:]) h.nodes[ix] = node } } // insert adds a peer to a bucket either by appending to existing items if // bucket length does not exceed bucketLength, or by replacing the worst // Node in the bucket func (self *bucket) insert(node Node) (err error) { self.lock.Lock() defer self.lock.Unlock() if len(self.nodes) >= self.size { // >= allows us to add peers beyond the bucketsize limitation worst := self.worstNode() self.nodes[worst] = node } else { self.nodes = append(self.nodes, node) } return } // worst expunges the single worst entry in a row, where worst entry is with a peer that has not been active the longests func (self *bucket) worstNode() (index int) { var oldest time.Time for i, node := range self.nodes { if (oldest == time.Time{}) || node.LastActive().Before(oldest) { oldest = node.LastActive() index = i } } return } /* Taking the proximity value relative to a fix point x classifies the points in the space (n byte long byte sequences) into bins the items in which are each at most half as distant from x as items in the previous bin. Given a sample of uniformly distrbuted items (a hash function over arbitrary sequence) the proximity scale maps onto series of subsets with cardinalities on a negative exponential scale. It also has the property that any two item belonging to the same bin are at most half as distant from each other as they are from x. If we think of random sample of items in the bins as connections in a network of interconnected nodes than relative proximity can serve as the basis for local decisions for graph traversal where the task is to find a route between two points. Since in every step of forwarding, the finite distance halves, there is a guaranteed constant maximum limit on the number of hops needed to reach one node from the other. */ func (self *Kademlia) proximityBin(other Address) (ret int) { ret = proximity(self.addr, other) if ret > self.MaxProx { ret = self.MaxProx } return } /* The distance metric MSB(x, y) of two equal length byte sequences x an y is the value of the binary integer cast of the xor-ed byte sequence (most significant bit first). proximity(x, y) counts the common zeros in the front of this distance measure. */ func proximity(one, other Address) (ret int) { for i := 0; i < len(one); i++ { oxo := one[i] ^ other[i] for j := 0; j < 8; j++ { if (uint8(oxo)>>uint8(7-j))&0x1 != 0 { return i*8 + j } } } return len(one) * 8 } // proxCmp compares the distances a->target and b->target. // Returns -1 if a is closer to target, 1 if b is closer to target // and 0 if they are equal. func proxCmp(target, a, b Address) int { for i := range target { da := a[i] ^ target[i] db := b[i] ^ target[i] if da > db { return 1 } else if da < db { return -1 } } return 0 } func (self *Kademlia) DB() [][]*NodeRecord { return self.nodeDB } func (n *NodeRecord) bumpActive() { stamp := time.Now().Unix() atomic.StoreInt64(&n.Active, stamp) } func (n *NodeRecord) LastActive() time.Time { stamp := atomic.LoadInt64(&n.Active) return time.Unix(stamp, 0) } // save persists all peers encountered func (self *Kademlia) Save(path string) error { kad := kadDB{ Address: self.addr, Nodes: self.nodeDB, } for _, b := range kad.Nodes { for _, node := range b { node.setActive() } } data, err := json.MarshalIndent(&kad, "", " ") if err != nil { return err } return ioutil.WriteFile(path, data, os.ModePerm) } // loading the idle node record from disk func (self *Kademlia) Load(path string) (err error) { var data []byte data, err = ioutil.ReadFile(path) if err != nil { return } var kad kadDB err = json.Unmarshal(data, &kad) if err != nil { return } self.nodeDB = kad.Nodes if self.addr != kad.Address { return fmt.Errorf("invalid kad db: address mismatch, expected %v, got %v", self.addr, kad.Address) } return }