mirror of
https://github.com/PeernetOfficial/Abstraction.git
synced 2026-07-23 13:37:50 +01:00
added example package
This commit is contained in:
211
vendor/github.com/PeernetOfficial/core/dht/DHT Lite.go
generated
vendored
Normal file
211
vendor/github.com/PeernetOfficial/core/dht/DHT Lite.go
generated
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
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{})
|
||||
|
||||
// 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,
|
||||
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()
|
||||
|
||||
result, ok := <-search.Results
|
||||
if !ok { // Check if closed channel. Redundant with checking <-search.TerminateSignal.
|
||||
return nil, nil
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
314
vendor/github.com/PeernetOfficial/core/dht/Hash Table.go
generated
vendored
Normal file
314
vendor/github.com/PeernetOfficial/core/dht/Hash Table.go
generated
vendored
Normal file
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
File Name: Hash Table.go
|
||||
Copyright: 2021 Peernet s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package dht
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// hashTable represents the hashtable state
|
||||
type hashTable struct {
|
||||
// The ID of the local node
|
||||
Self *Node
|
||||
|
||||
// the size in bits of the keys used to identify nodes and store and
|
||||
// retrieve data; in basic Kademlia this is 160, the length of a SHA1
|
||||
bBits int
|
||||
|
||||
// the maximum number of contacts stored in a bucket
|
||||
bSize int
|
||||
|
||||
// Routing table a list of all known nodes in the network
|
||||
// Nodes within buckets are sorted by least recently seen e.g.
|
||||
// [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
|
||||
// ^ ^
|
||||
// └ Least recently seen Most recently seen ┘
|
||||
RoutingTable [][]*Node // bBits x bSize
|
||||
|
||||
mutex *sync.RWMutex
|
||||
}
|
||||
|
||||
func newHashTable(self *Node, bits, bucketSize int) *hashTable {
|
||||
ht := &hashTable{
|
||||
bBits: bits,
|
||||
bSize: bucketSize,
|
||||
mutex: &sync.RWMutex{},
|
||||
Self: self,
|
||||
}
|
||||
|
||||
ht.RoutingTable = make([][]*Node, ht.bBits)
|
||||
return ht
|
||||
}
|
||||
|
||||
func (ht *hashTable) markNodeAsSeen(index int, ID []byte) {
|
||||
ht.mutex.Lock()
|
||||
defer ht.mutex.Unlock()
|
||||
bucket := ht.RoutingTable[index]
|
||||
nodeIndex := -1
|
||||
for i, v := range bucket {
|
||||
if bytes.Compare(v.ID, ID) == 0 {
|
||||
nodeIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if nodeIndex == -1 {
|
||||
//errors.New("Tried to mark nonexistent node as seen")
|
||||
return
|
||||
}
|
||||
|
||||
n := bucket[nodeIndex]
|
||||
n.LastSeen = time.Now().UTC()
|
||||
|
||||
bucket = append(bucket[:nodeIndex], bucket[nodeIndex+1:]...)
|
||||
bucket = append(bucket, n)
|
||||
ht.RoutingTable[index] = bucket
|
||||
}
|
||||
|
||||
func (ht *hashTable) doesNodeExistInBucket(bucket int, ID []byte) (node *Node) {
|
||||
ht.mutex.RLock()
|
||||
defer ht.mutex.RUnlock()
|
||||
for _, node = range ht.RoutingTable[bucket] {
|
||||
if bytes.Compare(node.ID, ID) == 0 {
|
||||
return node
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ht *hashTable) doesNodeExist(ID []byte) (node *Node) {
|
||||
return ht.doesNodeExistInBucket(ht.getBucketIndexFromDifferingBit(ID), ID)
|
||||
}
|
||||
|
||||
// getClosestContacts returns the closest nodes to the target. filterFunc is optional and allows the caller to filter the nodes.
|
||||
func (ht *hashTable) getClosestContacts(num int, target []byte, filterFunc NodeFilterFunc, ignoredNodes ...[]byte) *shortList {
|
||||
ht.mutex.RLock()
|
||||
defer ht.mutex.RUnlock()
|
||||
|
||||
// First we need to build the list of adjacent indices to our target in order
|
||||
index := ht.getBucketIndexFromDifferingBit(target)
|
||||
indexList := []int{index}
|
||||
for i, j := index-1, index+1; len(indexList) < ht.bBits; i, j = i-1, j+1 {
|
||||
if j < ht.bBits {
|
||||
indexList = append(indexList, j)
|
||||
}
|
||||
if i >= 0 {
|
||||
indexList = append(indexList, i)
|
||||
}
|
||||
}
|
||||
|
||||
sl := newShortList()
|
||||
|
||||
leftToAdd := num
|
||||
|
||||
// Next we select alpha contacts and add them to the short list
|
||||
for leftToAdd > 0 && len(indexList) > 0 {
|
||||
index, indexList = indexList[0], indexList[1:]
|
||||
bucketContacts := len(ht.RoutingTable[index])
|
||||
bucketLoop:
|
||||
for i := 0; i < bucketContacts; i++ {
|
||||
for j := 0; j < len(ignoredNodes); j++ {
|
||||
if bytes.Compare(ht.RoutingTable[index][i].ID, ignoredNodes[j]) == 0 {
|
||||
continue bucketLoop
|
||||
}
|
||||
}
|
||||
|
||||
// Use the filter function if set. It allows the caller to only accept certain nodes.
|
||||
if filterFunc != nil && !filterFunc(ht.RoutingTable[index][i]) {
|
||||
continue
|
||||
}
|
||||
|
||||
sl.AppendUniqueNodes(ht.RoutingTable[index][i])
|
||||
leftToAdd--
|
||||
if leftToAdd == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(sl)
|
||||
|
||||
return sl
|
||||
}
|
||||
|
||||
func (ht *hashTable) insertNode(node *Node, shouldEvict func(nodeOld *Node, nodeNew *Node) bool) {
|
||||
index := ht.getBucketIndexFromDifferingBit(node.ID)
|
||||
|
||||
// If the node already exist, mark it as seen
|
||||
if ht.doesNodeExistInBucket(index, node.ID) != nil {
|
||||
ht.markNodeAsSeen(index, node.ID)
|
||||
return
|
||||
}
|
||||
|
||||
node.LastSeen = time.Now().UTC()
|
||||
|
||||
ht.mutex.Lock()
|
||||
defer ht.mutex.Unlock()
|
||||
|
||||
bucket := ht.RoutingTable[index]
|
||||
|
||||
if len(bucket) == ht.bSize {
|
||||
if shouldEvict(bucket[0], node) {
|
||||
bucket = append(bucket, node)
|
||||
bucket = bucket[1:]
|
||||
}
|
||||
} else {
|
||||
bucket = append(bucket, node)
|
||||
}
|
||||
|
||||
ht.RoutingTable[index] = bucket
|
||||
}
|
||||
|
||||
func (ht *hashTable) removeNode(ID []byte) {
|
||||
ht.mutex.Lock()
|
||||
defer ht.mutex.Unlock()
|
||||
|
||||
index := ht.getBucketIndexFromDifferingBit(ID)
|
||||
bucket := ht.RoutingTable[index]
|
||||
|
||||
for i, v := range bucket {
|
||||
if bytes.Compare(v.ID, ID) == 0 {
|
||||
bucket = append(bucket[:i], bucket[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
ht.RoutingTable[index] = bucket
|
||||
}
|
||||
|
||||
func (ht *hashTable) getTotalNodesInBucket(bucket int) int {
|
||||
ht.mutex.RLock()
|
||||
defer ht.mutex.RUnlock()
|
||||
return len(ht.RoutingTable[bucket])
|
||||
}
|
||||
|
||||
func (ht *hashTable) getRandomIDFromBucket(bucket int) []byte {
|
||||
ht.mutex.RLock()
|
||||
defer ht.mutex.RUnlock()
|
||||
// Set the new ID to to be equal in every byte up to
|
||||
// the byte of the first differing bit in the bucket
|
||||
|
||||
byteIndex := bucket / 8
|
||||
var id []byte
|
||||
for i := 0; i < byteIndex; i++ {
|
||||
id = append(id, ht.Self.ID[i])
|
||||
}
|
||||
differingBitStart := bucket % 8
|
||||
|
||||
var firstByte byte
|
||||
// check each bit from left to right in order
|
||||
for i := 0; i < 8; i++ {
|
||||
// Set the value of the bit to be the same as the ID
|
||||
// up to the differing bit. Then begin randomizing
|
||||
var bit bool
|
||||
if i < differingBitStart {
|
||||
bit = hasBit(ht.Self.ID[byteIndex], uint(i))
|
||||
} else {
|
||||
bit = rand.Intn(2) == 1
|
||||
}
|
||||
|
||||
if bit {
|
||||
firstByte += byte(math.Pow(2, float64(7-i)))
|
||||
}
|
||||
}
|
||||
|
||||
id = append(id, firstByte)
|
||||
|
||||
// Randomize each remaining byte
|
||||
for i := byteIndex + 1; i < 20; i++ {
|
||||
randomByte := byte(rand.Intn(256))
|
||||
id = append(id, randomByte)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (ht *hashTable) lastSeenBefore(cutoff time.Time) (nodes []*Node) {
|
||||
ht.mutex.RLock()
|
||||
defer ht.mutex.RUnlock()
|
||||
nodes = make([]*Node, 0, ht.bSize)
|
||||
for _, v := range ht.RoutingTable {
|
||||
for _, n := range v {
|
||||
if n.LastSeen.Before(cutoff) {
|
||||
nodes = append(nodes, n)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (ht *hashTable) getBucketIndexFromDifferingBit(id1 []byte) int {
|
||||
// Look at each byte from left to right
|
||||
for j := 0; j < len(id1); j++ {
|
||||
// xor the byte
|
||||
xor := id1[j] ^ ht.Self.ID[j]
|
||||
|
||||
// check each bit on the xored result from left to right in order
|
||||
for i := 0; i < 8; i++ {
|
||||
if hasBit(xor, uint(i)) {
|
||||
byteIndex := j * 8
|
||||
bitIndex := i
|
||||
return ht.bBits - (byteIndex + bitIndex) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the ids must be the same
|
||||
// this should only happen during bootstrapping
|
||||
return 0
|
||||
}
|
||||
|
||||
func (ht *hashTable) totalNodes() int {
|
||||
ht.mutex.RLock()
|
||||
defer ht.mutex.RUnlock()
|
||||
var total int
|
||||
for _, v := range ht.RoutingTable {
|
||||
total += len(v)
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func (ht *hashTable) Nodes() (nodes []*Node) {
|
||||
ht.mutex.RLock()
|
||||
defer ht.mutex.RUnlock()
|
||||
nodes = make([]*Node, 0, ht.bSize)
|
||||
for _, v := range ht.RoutingTable {
|
||||
nodes = append(nodes, v...)
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// Simple helper function to determine the value of a particular bit in a byte by index
|
||||
|
||||
// Example:
|
||||
// number: 1
|
||||
// bits: 00000001
|
||||
// pos: 01234567
|
||||
func hasBit(n byte, pos uint) bool {
|
||||
pos = 7 - pos
|
||||
val := n & (1 << pos)
|
||||
return (val > 0)
|
||||
}
|
||||
|
||||
// getTotalNodesPerBucket returns the count of nodes in all buckets
|
||||
func (ht *hashTable) getTotalNodesPerBucket() (total []int) {
|
||||
ht.mutex.RLock()
|
||||
defer ht.mutex.RUnlock()
|
||||
|
||||
for n, _ := range ht.RoutingTable {
|
||||
total = append(total, len(ht.RoutingTable[n]))
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
113
vendor/github.com/PeernetOfficial/core/dht/Information Request.go
generated
vendored
Normal file
113
vendor/github.com/PeernetOfficial/core/dht/Information Request.go
generated
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
File Name: Information Request.go
|
||||
Copyright: 2021 Peernet s.r.o.
|
||||
Author: Peter Kleissner
|
||||
|
||||
Information requests are asynchronous queries sent to nodes.
|
||||
*/
|
||||
|
||||
package dht
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// InformationRequest is an asynchronous request sent to nodes. It tracks any asynchronous replies and handles timeouts.
|
||||
type InformationRequest struct {
|
||||
Action int // ActionX
|
||||
Key []byte // Key that is being queried
|
||||
ResultChan chan *NodeMessage // Result channel
|
||||
ResultChanExt chan *NodeMessage // External result channel to use instead
|
||||
ActiveNodes uint64 // Number of nodes actively handling the request.
|
||||
Nodes []*Node // Nodes that are receiving the request.
|
||||
IsTerminated bool // If true, it was signaled for termination
|
||||
TerminateSignal chan struct{} // gets closed on termination signal, can be used in select via "case _ = <- network.terminateSignal:"
|
||||
sync.Mutex // for sychronized closing
|
||||
}
|
||||
|
||||
// Actions for performing the information request
|
||||
const (
|
||||
ActionFindNode = iota // Find a node
|
||||
ActionFindValue // Find a value
|
||||
)
|
||||
|
||||
const messageChannelSize = 100
|
||||
|
||||
// NewInformationRequest creates a new information request and adds it to the list.
|
||||
// It marks the count of nodes as active, meaning the caller should later decrease it via ActiveNodesSub.
|
||||
func (dht *DHT) NewInformationRequest(Action int, Key []byte, Nodes []*Node) (ir *InformationRequest) {
|
||||
ir = &InformationRequest{
|
||||
ResultChan: make(chan *NodeMessage, messageChannelSize),
|
||||
TerminateSignal: make(chan struct{}),
|
||||
Action: Action,
|
||||
Key: Key,
|
||||
Nodes: Nodes,
|
||||
ActiveNodes: uint64(len(Nodes)),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CollectResults collects all information request responses within the given timeout.
|
||||
func (ir *InformationRequest) CollectResults(timeout time.Duration) (results []*NodeMessage) {
|
||||
for {
|
||||
select {
|
||||
case result, ok := <-ir.ResultChan:
|
||||
if !ok { // channel closed?
|
||||
return
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
|
||||
case <-time.After(timeout):
|
||||
ir.Terminate()
|
||||
return
|
||||
|
||||
case <-ir.TerminateSignal:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Terminate sends the termination signal to all workers. It is safe to call Terminate multiple times.
|
||||
func (ir *InformationRequest) Terminate() {
|
||||
ir.Lock()
|
||||
defer ir.Unlock()
|
||||
|
||||
if ir.IsTerminated {
|
||||
return
|
||||
}
|
||||
|
||||
// set the termination signal
|
||||
ir.IsTerminated = true
|
||||
close(ir.TerminateSignal) // safety guaranteed via lock
|
||||
|
||||
close(ir.ResultChan)
|
||||
}
|
||||
|
||||
// Done is called when a remote node is done.
|
||||
func (ir *InformationRequest) Done() {
|
||||
if atomic.AddUint64(&ir.ActiveNodes, ^uint64(0)) <= 0 {
|
||||
// If the counter reaches 0, it means no nodes are handling this request anymore -> terminate it.
|
||||
ir.Terminate()
|
||||
}
|
||||
}
|
||||
|
||||
// QueueResult accepts incoming results and queues them to the result channel. Non-blocking.
|
||||
func (ir *InformationRequest) QueueResult(message *NodeMessage) {
|
||||
ir.Lock()
|
||||
if !ir.IsTerminated {
|
||||
targetChan := ir.ResultChan
|
||||
if ir.ResultChanExt != nil {
|
||||
targetChan = ir.ResultChanExt
|
||||
}
|
||||
|
||||
select {
|
||||
case targetChan <- message:
|
||||
default:
|
||||
}
|
||||
}
|
||||
ir.Unlock()
|
||||
}
|
||||
133
vendor/github.com/PeernetOfficial/core/dht/Node.go
generated
vendored
Normal file
133
vendor/github.com/PeernetOfficial/core/dht/Node.go
generated
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
File Name: Node.go
|
||||
Copyright: 2021 Peernet s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package dht
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Node is the over-the-wire representation of a node
|
||||
type Node struct {
|
||||
// ID is the unique identifier
|
||||
ID []byte
|
||||
|
||||
// LastSeen when was this node last considered seen by the DHT
|
||||
LastSeen time.Time
|
||||
|
||||
// Info is an arbitrary pointer specified by the caller
|
||||
Info interface{}
|
||||
}
|
||||
|
||||
// shortList is used in order to sort a list of arbitrary nodes against a comparator. These nodes are sorted by xor distance
|
||||
type shortList struct {
|
||||
// Nodes are a list of nodes to be compared
|
||||
Nodes []*Node
|
||||
|
||||
// Comparator is the ID to compare to
|
||||
Comparator []byte
|
||||
|
||||
// Contacted is a list of nodes that are considered contacted
|
||||
Contacted map[string]bool
|
||||
}
|
||||
|
||||
func newShortList() *shortList {
|
||||
return &shortList{
|
||||
Contacted: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func areNodesEqual(n1 *Node, n2 *Node, allowNilID bool) bool {
|
||||
if n1 == nil || n2 == nil {
|
||||
return false
|
||||
}
|
||||
if !allowNilID {
|
||||
if n1.ID == nil || n2.ID == nil {
|
||||
return false
|
||||
}
|
||||
if bytes.Compare(n1.ID, n2.ID) != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *shortList) RemoveNode(ID []byte) {
|
||||
for i := 0; i < n.Len(); i++ {
|
||||
if bytes.Compare(n.Nodes[i].ID, ID) == 0 {
|
||||
n.Nodes = append(n.Nodes[:i], n.Nodes[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *shortList) AppendUniqueNodes(nodes ...*Node) {
|
||||
nodesLoop:
|
||||
for _, vv := range nodes {
|
||||
for _, v := range n.Nodes {
|
||||
if bytes.Compare(v.ID, vv.ID) == 0 {
|
||||
continue nodesLoop
|
||||
}
|
||||
}
|
||||
n.Nodes = append(n.Nodes, vv)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *shortList) Len() int {
|
||||
return len(n.Nodes)
|
||||
}
|
||||
|
||||
func (n *shortList) Swap(i, j int) {
|
||||
n.Nodes[i], n.Nodes[j] = n.Nodes[j], n.Nodes[i]
|
||||
}
|
||||
|
||||
func (n *shortList) Less(i, j int) bool {
|
||||
iDist := getDistance(n.Nodes[i].ID, n.Comparator)
|
||||
jDist := getDistance(n.Nodes[j].ID, n.Comparator)
|
||||
|
||||
return iDist.Cmp(jDist) == -1
|
||||
}
|
||||
|
||||
func getDistance(id1 []byte, id2 []byte) *big.Int {
|
||||
buf1 := new(big.Int).SetBytes(id1)
|
||||
buf2 := new(big.Int).SetBytes(id2)
|
||||
result := new(big.Int).Xor(buf1, buf2)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetUncontacted returns a list of uncontacted nodes. Each returned node will be marked as contacted.
|
||||
func (n *shortList) GetUncontacted(count int, useCount bool) (Nodes []*Node) {
|
||||
for _, node := range n.Nodes {
|
||||
if useCount && count <= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// Don't contact nodes already contacted
|
||||
if n.Contacted[string(node.ID)] == true {
|
||||
continue
|
||||
}
|
||||
|
||||
n.Contacted[string(node.ID)] = true
|
||||
Nodes = append(Nodes, node)
|
||||
|
||||
count--
|
||||
}
|
||||
|
||||
return Nodes
|
||||
}
|
||||
|
||||
// NodeMessage is a message sent by a node
|
||||
type NodeMessage struct {
|
||||
SenderID []byte // Sender of the message
|
||||
Data []byte // FIND_VALUE: Actual data
|
||||
Closest []*Node // FIND_VALUE, FIND_NODE: Closest nodes to the requested key
|
||||
Storing []*Node // FIND_VALUE: Nodes known to store the value
|
||||
}
|
||||
|
||||
// NodeFilterFunc is called to filter nodes based on the callers choice
|
||||
type NodeFilterFunc func(node *Node) (accept bool)
|
||||
9
vendor/github.com/PeernetOfficial/core/dht/README.md
generated
vendored
Normal file
9
vendor/github.com/PeernetOfficial/core/dht/README.md
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# DHT Lite
|
||||
|
||||
This code is a fork from https://github.com/james-lawrence/kademlia and https://github.com/prettymuchbryce/kademlia with modifications for proper abstraction. All networking code was removed from the original one. This package shall only provide DHT fuctionality.
|
||||
|
||||
The following functions are not handled here and must be done by the caller, if desired:
|
||||
* Remove nodes that are deemed inactive via `dht.RemoveNode`.
|
||||
* Provide a function `ShouldEvict` to determine if a node shall be evicted in favor of another one.
|
||||
* Refresh buckets via `dht.RefreshBuckets`.
|
||||
* The actual store data functions (and associated replication/expiration) are not provided, only the functionality to traverse through the network.
|
||||
325
vendor/github.com/PeernetOfficial/core/dht/Search Client.go
generated
vendored
Normal file
325
vendor/github.com/PeernetOfficial/core/dht/Search Client.go
generated
vendored
Normal file
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
File Name: Search Client.go
|
||||
Copyright: 2021 Peernet s.r.o.
|
||||
Author: Peter Kleissner
|
||||
|
||||
A search client runs concurrent information requests for a single query. It solves the query efficiently by using levels.
|
||||
Any result that is closer to the target gets pushed down into a new lower level, which contacts nodes closer to the result.
|
||||
Level are running concurrently.
|
||||
*/
|
||||
|
||||
package dht
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MaxAcceptKnownStore is maximum count accepted of known peers that store the value
|
||||
const MaxAcceptKnownStore = 10
|
||||
|
||||
// MaxClosest is maximum number of closest peers accepted
|
||||
const MaxClosest = 3
|
||||
|
||||
// MaxLevel defines the max level.
|
||||
const MaxLevel = 10
|
||||
|
||||
// SearchClient defines a search in the distributed hash table involving multiple information requests.
|
||||
// The search can be created for a node or for a value, both identified by the hash (known as the key).
|
||||
type SearchClient struct {
|
||||
Action int // ActionX
|
||||
Key []byte // Key that is being queried
|
||||
IsTerminated bool // If true, it was signaled for termination
|
||||
TerminateSignal chan struct{} // gets closed on termination signal, can be used in select via "case _ = <- TerminateSignal:"
|
||||
sync.Mutex // for sychronized closing
|
||||
timeStart time.Time // When the search started.
|
||||
timeEnd time.Time // When the search ended.
|
||||
dht *DHT // DHT used
|
||||
timeoutTotal time.Duration // Timeout after the entire search will be terminated client-side.
|
||||
timeoutIR time.Duration // Timeout for information requests (entire roundtrip).
|
||||
alpha int // Count of concurrent information requests per level.
|
||||
Results chan *SearchResult // Result channel
|
||||
list *shortList // List of nodes to contact
|
||||
contactedNodesMap map[string]struct{} // List of nodes already contacted
|
||||
contactedNodesMutex sync.RWMutex // Sync map access
|
||||
storing chan []*Node // Internal channel to signal nodes that indicate storing the searched value.
|
||||
activeLevels uint64 // demo
|
||||
LogStatus func(function, format string, v ...interface{}) // Filter function for status output
|
||||
}
|
||||
|
||||
// SearchResult is a single result to the search. Depending on the search type and parameters, multiple results may be sent.
|
||||
type SearchResult struct {
|
||||
Key []byte // Original key that was searched for
|
||||
Action int // Original action
|
||||
SenderID []byte // Sender node ID of the result
|
||||
|
||||
// data for ActionFindNode
|
||||
TargetNode *Node // The node that was requested.
|
||||
|
||||
// data for ActionFindValue
|
||||
Data []byte // Actual data
|
||||
}
|
||||
|
||||
// NewSearch creates a new search client.
|
||||
// Action indicates the action to take (from ActionX constants), to either find a node, or a value.
|
||||
// Timeout is the total time the search may take, covering all information requests. TimeoutIR is the time an information request may take.
|
||||
// Alpha is the number of concurrent requests that will be performed.
|
||||
func (dht *DHT) NewSearch(Action int, Key []byte, Timeout, TimeoutIR time.Duration, Alpha int) (client *SearchClient) {
|
||||
client = &SearchClient{
|
||||
Action: Action,
|
||||
Key: Key,
|
||||
dht: dht,
|
||||
timeoutTotal: Timeout,
|
||||
timeoutIR: TimeoutIR,
|
||||
alpha: Alpha,
|
||||
contactedNodesMap: make(map[string]struct{}),
|
||||
storing: make(chan []*Node, Alpha*2),
|
||||
TerminateSignal: make(chan struct{}),
|
||||
Results: make(chan *SearchResult),
|
||||
LogStatus: func(function, format string, v ...interface{}) {},
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Terminate sends the termination signal to all workers. It is safe to call Terminate multiple times.
|
||||
func (client *SearchClient) Terminate() {
|
||||
client.Lock()
|
||||
defer client.Unlock()
|
||||
|
||||
if client.IsTerminated {
|
||||
return
|
||||
}
|
||||
|
||||
// set the termination signal
|
||||
client.IsTerminated = true
|
||||
close(client.TerminateSignal) // safety guaranteed via lock
|
||||
|
||||
client.timeEnd = time.Now()
|
||||
|
||||
close(client.Results)
|
||||
close(client.storing)
|
||||
}
|
||||
|
||||
// isContactedNode checks if a node was contacted
|
||||
func (client *SearchClient) isContactedNode(ID []byte, Set bool) (contacted bool) {
|
||||
client.contactedNodesMutex.Lock()
|
||||
_, contacted = client.contactedNodesMap[string(ID)]
|
||||
if Set {
|
||||
client.contactedNodesMap[string(ID)] = struct{}{}
|
||||
}
|
||||
client.contactedNodesMutex.Unlock()
|
||||
return contacted
|
||||
}
|
||||
|
||||
// filterUncontactedNodes returns only nodes that were not contacted so far. All nodes will be set to contacted. Limit is optional (0 for no limit).
|
||||
func (client *SearchClient) filterUncontactedNodes(input []*Node, limit int) (output []*Node) {
|
||||
client.contactedNodesMutex.Lock()
|
||||
|
||||
for _, node := range input {
|
||||
if _, ok := client.contactedNodesMap[string(node.ID)]; !ok {
|
||||
output = append(output, node)
|
||||
client.contactedNodesMap[string(node.ID)] = struct{}{}
|
||||
|
||||
if limit > 0 {
|
||||
limit--
|
||||
if limit == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.contactedNodesMutex.Unlock()
|
||||
return output
|
||||
}
|
||||
|
||||
// SearchAway starts the search. Non-blocking!
|
||||
func (client *SearchClient) SearchAway() {
|
||||
client.timeStart = time.Now()
|
||||
|
||||
// create the first search level and start it
|
||||
client.list = client.dht.ht.getClosestContacts(client.alpha, client.Key, nil)
|
||||
if len(client.list.Nodes) == 0 {
|
||||
client.Terminate()
|
||||
return
|
||||
}
|
||||
|
||||
go client.queryNodesKnownStore()
|
||||
|
||||
// start the first information request
|
||||
go client.startSearch(0)
|
||||
|
||||
// start an automated termination function for the timeout
|
||||
go func(client *SearchClient) {
|
||||
// sleep + watch for closing
|
||||
select {
|
||||
case <-client.TerminateSignal: // exit the function on other signal
|
||||
return
|
||||
case <-time.After(client.timeoutTotal):
|
||||
client.Terminate()
|
||||
}
|
||||
}(client)
|
||||
}
|
||||
|
||||
// sendInfoRequest sends out a new info request to the nodes
|
||||
func (client *SearchClient) sendInfoRequest(nodes []*Node, resultChan chan *NodeMessage) (info *InformationRequest) {
|
||||
if client.IsTerminated {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, node := range nodes {
|
||||
client.LogStatus("search.sendInfoRequest", "contact node %s\n", hex.EncodeToString(node.ID))
|
||||
}
|
||||
|
||||
info = client.dht.NewInformationRequest(client.Action, client.Key, nodes)
|
||||
info.ResultChanExt = resultChan
|
||||
|
||||
switch client.Action {
|
||||
case ActionFindNode:
|
||||
client.dht.SendRequestFindNode(info)
|
||||
case ActionFindValue:
|
||||
client.dht.SendRequestFindValue(info)
|
||||
}
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-client.TerminateSignal:
|
||||
case <-time.After(client.timeoutIR):
|
||||
}
|
||||
info.Terminate()
|
||||
}()
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// queryNodesKnownStore queries nodes that are known to store the value. Only for ActionFindValue.
|
||||
// Returned 'closest nodes' are ignored, as the queried nodes are expected to store the value. This might be adjusted in the future.
|
||||
func (client *SearchClient) queryNodesKnownStore() {
|
||||
// all results are redirected to a single channel
|
||||
resultChan := make(chan *NodeMessage, client.alpha)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-client.TerminateSignal:
|
||||
return
|
||||
|
||||
case nodes := <-client.storing:
|
||||
client.sendInfoRequest(nodes, resultChan)
|
||||
|
||||
case result := <-resultChan:
|
||||
if len(result.Data) > 0 {
|
||||
client.Results <- &SearchResult{Key: client.Key, Action: client.Action, SenderID: result.SenderID, Data: result.Data}
|
||||
client.Terminate()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (client *SearchClient) startSearch(level int) {
|
||||
atomic.AddUint64(&client.activeLevels, 1)
|
||||
defer atomic.AddUint64(&client.activeLevels, ^uint64(0))
|
||||
nestedStarted := false
|
||||
|
||||
results := make(chan *NodeMessage, client.alpha*2)
|
||||
|
||||
closestNode := client.list.Nodes[0]
|
||||
|
||||
// start an info request
|
||||
startInfoRequest := func() (info *InformationRequest) {
|
||||
nodes := client.list.GetUncontacted(client.alpha, true)
|
||||
if len(nodes) == 0 {
|
||||
client.LogStatus("search.startSearch", "search in level %d aborted, no new nodes to contact\n", level)
|
||||
return nil
|
||||
}
|
||||
client.LogStatus("search.startSearch", "start search in level %d contacting %d nodes\n", level, len(nodes))
|
||||
return client.sendInfoRequest(nodes, results)
|
||||
}
|
||||
|
||||
info := startInfoRequest()
|
||||
if info == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-client.TerminateSignal:
|
||||
client.LogStatus("search.startSearch", "search in level %d aborted, search client termination signal\n", level)
|
||||
return
|
||||
|
||||
case result := <-results:
|
||||
|
||||
switch client.Action {
|
||||
case ActionFindValue:
|
||||
// search for value and it was found?
|
||||
if len(result.Data) > 0 {
|
||||
client.LogStatus("search.startSearch", "result: sender %s: data found (%d bytes)\n", hex.EncodeToString(result.SenderID), len(result.Data))
|
||||
client.Results <- &SearchResult{Key: client.Key, Action: client.Action, SenderID: result.SenderID, Data: result.Data}
|
||||
client.Terminate()
|
||||
return
|
||||
}
|
||||
|
||||
result.Storing = client.filterUncontactedNodes(result.Storing, MaxAcceptKnownStore)
|
||||
result.Closest = client.filterUncontactedNodes(result.Closest, MaxClosest)
|
||||
|
||||
client.LogStatus("search.startSearch", "result: sender %s: %d uncontacted nodes store and %d nodes are close to value\n", hex.EncodeToString(result.SenderID), len(result.Storing), len(result.Closest))
|
||||
|
||||
// Find value: Nodes known to store the value are queried in a separate function.
|
||||
if len(result.Storing) > 0 {
|
||||
client.storing <- result.Storing
|
||||
}
|
||||
|
||||
case ActionFindNode:
|
||||
// search for node and it was found?
|
||||
for _, closePeer := range result.Closest {
|
||||
if bytes.Equal(closePeer.ID, client.Key) {
|
||||
client.LogStatus("search.startSearch", "result: sender %s: node found!\n", hex.EncodeToString(result.SenderID))
|
||||
client.Results <- &SearchResult{Key: client.Key, Action: client.Action, SenderID: result.SenderID, TargetNode: closePeer}
|
||||
client.Terminate()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result.Closest = client.filterUncontactedNodes(result.Closest, MaxClosest)
|
||||
|
||||
client.LogStatus("search.startSearch", "find node: sender %s: %d nodes are close to value\n", hex.EncodeToString(result.SenderID), len(result.Closest))
|
||||
|
||||
}
|
||||
|
||||
// Add closest to list
|
||||
client.list.AppendUniqueNodes(result.Closest...)
|
||||
|
||||
// If no subsequent level, and there's closer nodes, start one!
|
||||
if !nestedStarted && !bytes.Equal(client.list.Nodes[0].ID, closestNode.ID) && level < MaxLevel {
|
||||
nestedStarted = true
|
||||
go client.startSearch(level + 1)
|
||||
}
|
||||
|
||||
case <-info.TerminateSignal:
|
||||
// If highest level (= not nested), and there was no conclusive result, try one more round.
|
||||
// This helps against result poisoning.
|
||||
|
||||
if !nestedStarted {
|
||||
client.LogStatus("search.startSearch", "search in level %d aborted, info request termination signal. Final try.\n", level)
|
||||
if info = startInfoRequest(); info != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if client.activeLevels == 1 { // if this was the last level, no more results will appear
|
||||
client.LogStatus("search.startSearch", "level %d last active level, not found, terminate search\n", level)
|
||||
client.Terminate()
|
||||
} else {
|
||||
client.LogStatus("search.startSearch", "level %d end, info request termination signal\n", level)
|
||||
}
|
||||
return
|
||||
|
||||
//case <-time.After(time.Second):
|
||||
// Future todo: Launch another routine with the with uncontacted nodes if any, to speed up the query
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user