/* File Name: Kademlia.go Copyright: 2021 Peernet s.r.o. Author: Peter Kleissner */ package core import ( "bytes" "time" "github.com/PeernetOfficial/core/dht" "github.com/PeernetOfficial/core/protocol" ) const alpha = 5 // Count of nodes to be contacted in parallel for finding a key const bucketSize = 20 // Count of nodes per bucket var nodesDHT *dht.DHT func initKademlia() { nodesDHT = dht.NewDHT(&dht.Node{ID: nodeID}, 256, bucketSize, alpha) // ShouldEvict determines whether node 1 shall be evicted in favor of node 2 nodesDHT.ShouldEvict = func(node1, node2 *dht.Node) bool { rttOld := node1.Info.(*PeerInfo).GetRTT() rttNew := node2.Info.(*PeerInfo).GetRTT() // evict the old node if the new one has a faster ping time if rttOld == 0 { // old one has no recent RTT (happens if all connections are inactive)? return true } else if rttNew > 0 { // If new RTT is smaller, evict old one. return rttNew < rttOld } // If here, none has a RTT. Keep the closer (by distance) one. return nodesDHT.IsNodeCloser(node1.ID, node2.ID) } // SendRequestStore sends a store message to the remote node. I.e. asking it to store the given key-value nodesDHT.SendRequestStore = func(node *dht.Node, key []byte, dataSize uint64) { node.Info.(*PeerInfo).sendAnnouncementStore(key, dataSize) } // SendRequestFindNode sends an information request to find a particular node. nodes are the nodes to send the request to. nodesDHT.SendRequestFindNode = func(request *dht.InformationRequest) { for _, node := range request.Nodes { node.Info.(*PeerInfo).sendAnnouncementFindNode(request) } } // SendRequestFindValue sends an information request to find data. nodes are the nodes to send the request to. nodesDHT.SendRequestFindValue = func(request *dht.InformationRequest) { for _, node := range request.Nodes { node.Info.(*PeerInfo).sendAnnouncementFindValue(request) } } nodesDHT.FilterSearchStatus = Filters.DHTSearchStatus } // autoBucketRefresh refreshes buckets every 5 minutes to meet the alpha nodes per bucket target. Force full refresh every hour. func autoBucketRefresh() { for minute := 5; ; minute += 5 { time.Sleep(time.Minute * 5) target := alpha if minute%60 == 0 { target = 0 } nodesDHT.RefreshBuckets(target) } } // bootstrapKademlia bootstraps the Kademlia bucket list func bootstrapKademlia() { monitor := make(chan *PeerInfo) registerPeerMonitor(monitor) // Wait until there are at least 2 peers connected. for { <-monitor if nodesDHT.NumNodes() >= 2 { break } } unregisterPeerMonitor(monitor) // Refresh every 10 seconds 3 times for n := 0; n < 3; n++ { nodesDHT.RefreshBuckets(alpha) time.Sleep(time.Second) } } // Future sendAnnouncementX: If it detects that announcements are sent out to the same peer within 50ms it should activate a wait-and-group scheme. func (peer *PeerInfo) sendAnnouncementFindNode(request *dht.InformationRequest) { // If the key is self, send it as FIND_SELF if bytes.Equal(request.Key, nodeID) { peer.sendAnnouncement(false, true, nil, nil, nil, request) } else { peer.sendAnnouncement(false, false, []protocol.KeyHash{{Hash: request.Key}}, nil, nil, request) } } func (peer *PeerInfo) sendAnnouncementFindValue(request *dht.InformationRequest) { findSelf := false var findPeer []protocol.KeyHash var findValue []protocol.KeyHash findValue = append(findValue, protocol.KeyHash{Hash: request.Key}) peer.sendAnnouncement(false, findSelf, findPeer, findValue, nil, request) } func (peer *PeerInfo) sendAnnouncementStore(fileHash []byte, fileSize uint64) { peer.sendAnnouncement(false, false, nil, nil, []protocol.InfoStore{{ID: protocol.KeyHash{Hash: fileHash}, Size: fileSize, Type: 0}}, nil) } // ---- CORE DATA FUNCTIONS ---- // Data2Hash returns the hash for the data func Data2Hash(data []byte) (hash []byte) { return protocol.HashData(data) } // GetData returns the requested data. It checks first the local store and then tries via DHT. func GetData(hash []byte) (data []byte, senderNodeID []byte, found bool) { if data, found = GetDataLocal(hash); found { return data, nodeID, found } return GetDataDHT(hash) } // GetDataLocal returns data from the local warehouse. func GetDataLocal(hash []byte) (data []byte, found bool) { return Warehouse.Get(hash) } // GetDataDHT requests data via DHT func GetDataDHT(hash []byte) (data []byte, senderNodeID []byte, found bool) { data, senderNodeID, found, _ = nodesDHT.Get(hash) return data, senderNodeID, found } // StoreDataLocal stores data into the local warehouse. func StoreDataLocal(data []byte) error { key := protocol.HashData(data) return Warehouse.Set(key, data) } // StoreDataDHT stores data locally and informs closestCount peers in the DHT about it. // Remote peers may choose to keep a record (in case another peers asks) or mirror the full data. func StoreDataDHT(data []byte, closestCount int) error { key := protocol.HashData(data) if err := Warehouse.Set(key, data); err != nil { return err } return nodesDHT.Store(key, uint64(len(data)), closestCount) } // ---- NODE FUNCTIONS ---- // IsNodeContact checks if the node is a contact in the local DHT routing table func IsNodeContact(nodeID []byte) (node *dht.Node, peer *PeerInfo) { node = nodesDHT.IsNodeContact(nodeID) if node == nil { return nil, nil } return node, node.Info.(*PeerInfo) } // FindNode finds a node via the DHT func FindNode(nodeID []byte, Timeout time.Duration) (node *dht.Node, peer *PeerInfo, err error) { node, err = nodesDHT.FindNode(nodeID) if node == nil { return nil, nil, err } return node, node.Info.(*PeerInfo), err } // ---- Asynchronous Search ---- // AsyncSearch creates an async search for the given key in the DHT. // 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 AsyncSearch(Action int, Key []byte, Timeout, TimeoutIR time.Duration, Alpha int) (client *dht.SearchClient) { return nodesDHT.NewSearch(Action, Key, Timeout, TimeoutIR, Alpha) }