mirror of
https://github.com/PeernetOfficial/core.git
synced 2026-07-17 02:47:51 +01:00
DHT: Store and Get functions use search client. Multiple improvements in search client. Alpha parameter is now provided by init caller. StoreDataDHT add closestCount parameter.
217 lines
8.1 KiB
Go
217 lines
8.1 KiB
Go
/*
|
|
File Name: DHT Lite.go
|
|
Copyright: 2021 Peernet s.r.o.
|
|
Author: Peter Kleissner
|
|
|
|
A "lite" DHT implementation without any direct network and store code. There is really no reason for any of the heavy network implementation to be part of this.
|
|
*/
|
|
|
|
package dht
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// DHT represents the state of the local node in the distributed hash table
|
|
type DHT struct {
|
|
ht *hashTable
|
|
|
|
// A small number representing the degree of parallelism in network calls.
|
|
// The alpha amount of nodes will be contacted in parallel for finding the target.
|
|
alpha int
|
|
|
|
// Functions below must be set and provided by the caller.
|
|
|
|
// ShouldEvict determines whether node 1 shall be evicted in favor of node 2
|
|
ShouldEvict func(node1, node2 *Node) bool
|
|
|
|
// SendRequestStore sends an announcement-store message to the remote node. It informs the remote node that the local one stores the given key-value.
|
|
SendRequestStore func(node *Node, key []byte, dataSize uint64)
|
|
|
|
// SendRequestFindNode sends an information request to find a particular node. nodes are the nodes to send the request to.
|
|
SendRequestFindNode func(request *InformationRequest)
|
|
|
|
// SendRequestFindValue sends an information request to find data. nodes are the nodes to send the request to.
|
|
SendRequestFindValue func(request *InformationRequest)
|
|
|
|
// FilterSearchStatus is called with updates of searches in the DHT
|
|
FilterSearchStatus func(client *SearchClient, function, format string, v ...interface{})
|
|
|
|
// The maximum time to wait for a response to any message in Store, Get, FindNode
|
|
TMsgTimeout time.Duration
|
|
|
|
// TimeoutSearch is the maximum time a search may take.
|
|
TimeoutSearch time.Duration
|
|
|
|
// TimeoutIR is the maximum an information request to a node may take.
|
|
TimeoutIR time.Duration
|
|
}
|
|
|
|
// NewDHT initializes a new DHT node with default values.
|
|
func NewDHT(self *Node, bits, bucketSize, alpha int) *DHT {
|
|
return &DHT{
|
|
ht: newHashTable(self, bits, bucketSize),
|
|
alpha: alpha,
|
|
TMsgTimeout: 2 * time.Second,
|
|
FilterSearchStatus: func(client *SearchClient, function, format string, v ...interface{}) {},
|
|
TimeoutSearch: 10 * time.Second,
|
|
TimeoutIR: 6 * time.Second,
|
|
}
|
|
}
|
|
|
|
// NumNodes returns the total number of nodes stored in the local routing table
|
|
func (dht *DHT) NumNodes() int {
|
|
return dht.ht.totalNodes()
|
|
}
|
|
|
|
// Nodes returns the nodes themselves sotred in the routing table.
|
|
func (dht *DHT) Nodes() []*Node {
|
|
return dht.ht.Nodes()
|
|
}
|
|
|
|
// GetSelfID returns the identifier of the local node
|
|
func (dht *DHT) GetSelfID() []byte {
|
|
return dht.ht.Self.ID
|
|
}
|
|
|
|
// AddNode adds a node into the appropriate k bucket. These buckets are stored in big-endian order so we look at the bits from right to left in order to find the appropriate bucket.
|
|
func (dht *DHT) AddNode(node *Node) {
|
|
// The previous code made an immediate ping to the oldest node to "ping the oldest node to find out if it responds back in a reasonable amount of time. If not - remove it."
|
|
// In DHT Lite, however, it will be up to the caller to determine nodes to remove.
|
|
dht.ht.insertNode(node, dht.ShouldEvict)
|
|
}
|
|
|
|
// RemoveNode removes a node
|
|
func (dht *DHT) RemoveNode(ID []byte) {
|
|
dht.ht.removeNode(ID)
|
|
}
|
|
|
|
// GetClosestContacts returns the closes contacts in the hash table
|
|
func (dht *DHT) GetClosestContacts(count int, target []byte, filterFunc NodeFilterFunc, ignoredNodes ...[]byte) []*Node {
|
|
closest := dht.ht.getClosestContacts(count, target, filterFunc, ignoredNodes...)
|
|
return closest.Nodes
|
|
}
|
|
|
|
// MarkNodeAsSeen marks a node as seen, which pushes it to the top in the bucket list.
|
|
func (dht *DHT) MarkNodeAsSeen(ID []byte) {
|
|
dht.ht.markNodeAsSeen(dht.ht.getBucketIndexFromDifferingBit(ID), ID)
|
|
}
|
|
|
|
// IsNodeCloser compares 2 nodes to self. If true, the first node is closer (= smaller distance) to self than the second.
|
|
func (dht *DHT) IsNodeCloser(node1, node2 []byte) bool {
|
|
iDist := getDistance(node1, dht.ht.Self.ID)
|
|
jDist := getDistance(node2, dht.ht.Self.ID)
|
|
|
|
return iDist.Cmp(jDist) == -1
|
|
}
|
|
|
|
// IsNodeContact checks if the given node is in the local routing table
|
|
func (dht *DHT) IsNodeContact(ID []byte) (node *Node) {
|
|
return dht.ht.doesNodeExist(ID)
|
|
}
|
|
|
|
// ---- Synchronous network query functions below ----
|
|
|
|
// Store informs the network about data stored locally.
|
|
// Data size informs how big the data is without sending the actual data. closestCount is the number of closest nodes to contact.
|
|
func (dht *DHT) Store(key []byte, dataSize uint64, closestCount int) (err error) {
|
|
if len(key)*8 != dht.ht.bBits {
|
|
return errors.New("invalid key size")
|
|
}
|
|
|
|
// TODO: Introduce ActionFindClosestNodes?
|
|
|
|
search := dht.NewSearch(ActionFindNode, key, dht.TimeoutSearch, dht.TimeoutIR, dht.alpha)
|
|
search.LogStatus = func(function, format string, v ...interface{}) {
|
|
dht.FilterSearchStatus(search, function, format, v...)
|
|
}
|
|
search.LogStatus("dht.Store", "Search for closest nodes to key %s. Full timeout %s, per node %s. Alpha = %d.\n", hex.EncodeToString(key), dht.TimeoutSearch.String(), dht.TimeoutIR.String(), dht.alpha)
|
|
search.SearchAway()
|
|
|
|
// search.Results channel is ignored here. Only the closest nodes to the key are of interest. It is not expected to find a match of key and node ID.
|
|
<-search.TerminateSignal
|
|
|
|
// Contact the closes nodes found.
|
|
for n := 0; n < closestCount && n < len(search.list.Nodes); n++ {
|
|
node := search.list.Nodes[n]
|
|
search.LogStatus("dht.Store", "Send info-store message to node %s\n", hex.EncodeToString(node.ID))
|
|
dht.SendRequestStore(node, key, dataSize)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Get retrieves data from the network using key
|
|
func (dht *DHT) Get(key []byte) (value []byte, senderID []byte, found bool, err error) {
|
|
if len(key)*8 != dht.ht.bBits {
|
|
return nil, nil, false, errors.New("invalid key size")
|
|
}
|
|
|
|
search := dht.NewSearch(ActionFindValue, key, dht.TimeoutSearch, dht.TimeoutIR, dht.alpha)
|
|
search.LogStatus = func(function, format string, v ...interface{}) {
|
|
dht.FilterSearchStatus(search, function, format, v...)
|
|
}
|
|
search.LogStatus("dht.Get", "Search for node %s. Full timeout %s, per node %s. Alpha = %d.\n", hex.EncodeToString(key), dht.TimeoutSearch.String(), dht.TimeoutIR.String(), dht.alpha)
|
|
search.SearchAway()
|
|
|
|
select {
|
|
case <-search.TerminateSignal:
|
|
return nil, nil, false, nil
|
|
case result := <-search.Results:
|
|
return result.Data, result.SenderID, true, nil
|
|
}
|
|
}
|
|
|
|
// FindNode finds the target node in the network. Blocking!
|
|
// The caller may use dht.NewSearch directly and take advantage of the asynchronous response and custom timeouts.
|
|
func (dht *DHT) FindNode(key []byte) (node *Node, err error) {
|
|
if len(key)*8 != dht.ht.bBits {
|
|
return nil, errors.New("invalid key size")
|
|
}
|
|
|
|
search := dht.NewSearch(ActionFindNode, key, dht.TimeoutSearch, dht.TimeoutIR, dht.alpha)
|
|
search.LogStatus = func(function, format string, v ...interface{}) {
|
|
dht.FilterSearchStatus(search, function, format, v...)
|
|
}
|
|
search.LogStatus("dht.FindNode", "Search for node %s. Full timeout %s, per node %s. Alpha = %d.\n", hex.EncodeToString(key), dht.TimeoutSearch.String(), dht.TimeoutIR.String(), dht.alpha)
|
|
search.SearchAway()
|
|
|
|
select {
|
|
case <-search.TerminateSignal:
|
|
return nil, nil
|
|
case result := <-search.Results:
|
|
return result.TargetNode, nil
|
|
}
|
|
}
|
|
|
|
// ---- DHT Health ----
|
|
|
|
// DisableBucketRefresh is an option for debug purposes to reduce noise. It can be useful to disable bucket refresh when debugging outgoing DHT searches.
|
|
var DisableBucketRefresh = false
|
|
|
|
// RefreshBuckets refreshes all buckets not meeting the target node number. 0 to refresh all.
|
|
func (dht *DHT) RefreshBuckets(target int) {
|
|
if DisableBucketRefresh {
|
|
return
|
|
}
|
|
|
|
for bucket, total := range dht.ht.getTotalNodesPerBucket() {
|
|
if target == 0 || total < target {
|
|
nodeR := dht.ht.getRandomIDFromBucket(bucket)
|
|
|
|
// Refreshing closest bucket? Use self ID instead of random one.
|
|
if bucket == 0 {
|
|
nodeR = dht.ht.Self.ID
|
|
}
|
|
|
|
dht.FindNode(nodeR)
|
|
}
|
|
|
|
if DisableBucketRefresh { // may be disabled while in full refresh which may take some time
|
|
return
|
|
}
|
|
}
|
|
}
|