added example package

This commit is contained in:
2022-12-11 06:18:00 +00:00
parent fc9ed17ecd
commit 73b8a7c2a3
811 changed files with 276135 additions and 1 deletions

211
vendor/github.com/PeernetOfficial/core/dht/DHT Lite.go generated vendored Normal file
View 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
}
}
}

View 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
}

View 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
View 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
View 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.

View 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
}
}
}