2015-01-27 15:33:26 +02:00
|
|
|
// Package discover implements the Node Discovery Protocol.
|
|
|
|
//
|
|
|
|
// The Node Discovery protocol provides a way to find RLPx nodes that
|
|
|
|
// can be connected to. It uses a Kademlia-like protocol to maintain a
|
|
|
|
// distributed database of the IDs and endpoints of all listening
|
|
|
|
// nodes.
|
|
|
|
package discover
|
|
|
|
|
|
|
|
import (
|
2015-04-27 01:50:18 +03:00
|
|
|
"crypto/rand"
|
2015-01-27 15:33:26 +02:00
|
|
|
"net"
|
|
|
|
"sort"
|
|
|
|
"sync"
|
|
|
|
"time"
|
2015-04-23 18:47:24 +03:00
|
|
|
|
2015-04-27 01:50:18 +03:00
|
|
|
"github.com/ethereum/go-ethereum/common"
|
2015-04-23 13:11:21 +03:00
|
|
|
"github.com/ethereum/go-ethereum/crypto"
|
2015-04-23 18:47:24 +03:00
|
|
|
"github.com/ethereum/go-ethereum/logger"
|
|
|
|
"github.com/ethereum/go-ethereum/logger/glog"
|
2015-01-27 15:33:26 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2015-04-27 01:50:18 +03:00
|
|
|
alpha = 3 // Kademlia concurrency factor
|
|
|
|
bucketSize = 16 // Kademlia bucket size
|
|
|
|
hashBits = len(common.Hash{}) * 8
|
|
|
|
nBuckets = hashBits + 1 // Number of buckets
|
|
|
|
|
2015-03-25 17:45:53 +02:00
|
|
|
maxBondingPingPongs = 10
|
2015-01-27 15:33:26 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
type Table struct {
|
|
|
|
mutex sync.Mutex // protects buckets, their content, and nursery
|
|
|
|
buckets [nBuckets]*bucket // index of known nodes by distance
|
|
|
|
nursery []*Node // bootstrap nodes
|
2015-04-24 18:04:41 +03:00
|
|
|
db *nodeDB // database of known nodes
|
2015-01-27 15:33:26 +02:00
|
|
|
|
2015-03-25 17:45:53 +02:00
|
|
|
bondmu sync.Mutex
|
|
|
|
bonding map[NodeID]*bondproc
|
|
|
|
bondslots chan struct{} // limits total number of active bonding processes
|
|
|
|
|
2015-01-27 15:33:26 +02:00
|
|
|
net transport
|
|
|
|
self *Node // metadata of the local node
|
2015-03-25 17:45:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type bondproc struct {
|
|
|
|
err error
|
|
|
|
n *Node
|
|
|
|
done chan struct{}
|
2015-01-27 15:33:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// transport is implemented by the UDP transport.
|
|
|
|
// it is an interface so we can test without opening lots of UDP
|
|
|
|
// sockets and without generating a private key.
|
|
|
|
type transport interface {
|
2015-03-25 17:45:53 +02:00
|
|
|
ping(NodeID, *net.UDPAddr) error
|
|
|
|
waitping(NodeID) error
|
|
|
|
findnode(toid NodeID, addr *net.UDPAddr, target NodeID) ([]*Node, error)
|
2015-01-27 15:33:26 +02:00
|
|
|
close()
|
|
|
|
}
|
|
|
|
|
|
|
|
// bucket contains nodes, ordered by their last activity.
|
2015-03-25 17:45:53 +02:00
|
|
|
// the entry that was most recently active is the last element
|
|
|
|
// in entries.
|
2015-01-27 15:33:26 +02:00
|
|
|
type bucket struct {
|
|
|
|
lastLookup time.Time
|
|
|
|
entries []*Node
|
|
|
|
}
|
|
|
|
|
2015-04-24 18:04:41 +03:00
|
|
|
func newTable(t transport, ourID NodeID, ourAddr *net.UDPAddr, nodeDBPath string) *Table {
|
2015-04-27 10:19:16 +03:00
|
|
|
// If no node database was given, use an in-memory one
|
2015-04-27 15:28:17 +03:00
|
|
|
db, err := newNodeDB(nodeDBPath, Version)
|
2015-04-24 18:04:41 +03:00
|
|
|
if err != nil {
|
|
|
|
glog.V(logger.Warn).Infoln("Failed to open node database:", err)
|
2015-04-27 15:28:17 +03:00
|
|
|
db, _ = newNodeDB("", Version)
|
2015-04-23 18:47:24 +03:00
|
|
|
}
|
2015-03-25 17:45:53 +02:00
|
|
|
tab := &Table{
|
|
|
|
net: t,
|
2015-04-24 18:04:41 +03:00
|
|
|
db: db,
|
2015-04-23 13:11:21 +03:00
|
|
|
self: newNode(ourID, ourAddr.IP, uint16(ourAddr.Port), uint16(ourAddr.Port)),
|
2015-03-25 17:45:53 +02:00
|
|
|
bonding: make(map[NodeID]*bondproc),
|
|
|
|
bondslots: make(chan struct{}, maxBondingPingPongs),
|
|
|
|
}
|
|
|
|
for i := 0; i < cap(tab.bondslots); i++ {
|
|
|
|
tab.bondslots <- struct{}{}
|
|
|
|
}
|
2015-01-27 15:33:26 +02:00
|
|
|
for i := range tab.buckets {
|
2015-02-06 15:40:53 +02:00
|
|
|
tab.buckets[i] = new(bucket)
|
2015-01-27 15:33:26 +02:00
|
|
|
}
|
|
|
|
return tab
|
|
|
|
}
|
|
|
|
|
2015-03-15 08:38:41 +02:00
|
|
|
// Self returns the local node.
|
|
|
|
func (tab *Table) Self() *Node {
|
|
|
|
return tab.self
|
2015-02-05 04:07:18 +02:00
|
|
|
}
|
|
|
|
|
2015-04-27 10:19:16 +03:00
|
|
|
// Close terminates the network listener and flushes the node database.
|
2015-02-05 04:07:18 +02:00
|
|
|
func (tab *Table) Close() {
|
|
|
|
tab.net.close()
|
2015-04-24 18:04:41 +03:00
|
|
|
tab.db.close()
|
2015-02-05 04:07:18 +02:00
|
|
|
}
|
|
|
|
|
2015-01-27 15:33:26 +02:00
|
|
|
// Bootstrap sets the bootstrap nodes. These nodes are used to connect
|
|
|
|
// to the network if the table is empty. Bootstrap will also attempt to
|
|
|
|
// fill the table by performing random lookup operations on the
|
|
|
|
// network.
|
2015-02-07 01:38:36 +02:00
|
|
|
func (tab *Table) Bootstrap(nodes []*Node) {
|
2015-01-27 15:33:26 +02:00
|
|
|
tab.mutex.Lock()
|
|
|
|
// TODO: maybe filter nodes with bad fields (nil, etc.) to avoid strange crashes
|
|
|
|
tab.nursery = make([]*Node, 0, len(nodes))
|
|
|
|
for _, n := range nodes {
|
2015-02-07 01:38:36 +02:00
|
|
|
cpy := *n
|
2015-04-23 13:11:21 +03:00
|
|
|
cpy.sha = crypto.Sha3Hash(n.ID[:])
|
2015-01-27 15:33:26 +02:00
|
|
|
tab.nursery = append(tab.nursery, &cpy)
|
|
|
|
}
|
|
|
|
tab.mutex.Unlock()
|
|
|
|
tab.refresh()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Lookup performs a network search for nodes close
|
|
|
|
// to the given target. It approaches the target by querying
|
|
|
|
// nodes that are closer to it on each iteration.
|
2015-04-27 01:50:18 +03:00
|
|
|
// The given target does not need to be an actual node
|
|
|
|
// identifier.
|
|
|
|
func (tab *Table) Lookup(targetID NodeID) []*Node {
|
2015-01-27 15:33:26 +02:00
|
|
|
var (
|
2015-04-27 01:50:18 +03:00
|
|
|
target = crypto.Sha3Hash(targetID[:])
|
2015-01-27 15:33:26 +02:00
|
|
|
asked = make(map[NodeID]bool)
|
|
|
|
seen = make(map[NodeID]bool)
|
|
|
|
reply = make(chan []*Node, alpha)
|
|
|
|
pendingQueries = 0
|
|
|
|
)
|
2015-04-27 01:50:18 +03:00
|
|
|
// don't query further if we hit ourself.
|
2015-01-27 15:33:26 +02:00
|
|
|
// unlikely to happen often in practice.
|
2015-02-09 12:02:32 +02:00
|
|
|
asked[tab.self.ID] = true
|
2015-01-27 15:33:26 +02:00
|
|
|
|
|
|
|
tab.mutex.Lock()
|
|
|
|
// update last lookup stamp (for refresh logic)
|
2015-04-27 01:50:18 +03:00
|
|
|
tab.buckets[logdist(tab.self.sha, target)].lastLookup = time.Now()
|
2015-01-27 15:33:26 +02:00
|
|
|
// generate initial result set
|
|
|
|
result := tab.closest(target, bucketSize)
|
|
|
|
tab.mutex.Unlock()
|
|
|
|
|
|
|
|
for {
|
2015-02-12 12:59:52 +02:00
|
|
|
// ask the alpha closest nodes that we haven't asked yet
|
2015-01-27 15:33:26 +02:00
|
|
|
for i := 0; i < len(result.entries) && pendingQueries < alpha; i++ {
|
|
|
|
n := result.entries[i]
|
|
|
|
if !asked[n.ID] {
|
|
|
|
asked[n.ID] = true
|
|
|
|
pendingQueries++
|
|
|
|
go func() {
|
2015-04-27 01:50:18 +03:00
|
|
|
r, _ := tab.net.findnode(n.ID, n.addr(), targetID)
|
2015-03-25 17:45:53 +02:00
|
|
|
reply <- tab.bondall(r)
|
2015-01-27 15:33:26 +02:00
|
|
|
}()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if pendingQueries == 0 {
|
|
|
|
// we have asked all closest nodes, stop the search
|
|
|
|
break
|
|
|
|
}
|
|
|
|
// wait for the next reply
|
|
|
|
for _, n := range <-reply {
|
2015-03-25 17:45:53 +02:00
|
|
|
if n != nil && !seen[n.ID] {
|
2015-01-27 15:33:26 +02:00
|
|
|
seen[n.ID] = true
|
2015-03-25 17:45:53 +02:00
|
|
|
result.push(n, bucketSize)
|
2015-01-27 15:33:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
pendingQueries--
|
|
|
|
}
|
|
|
|
return result.entries
|
|
|
|
}
|
|
|
|
|
|
|
|
// refresh performs a lookup for a random target to keep buckets full.
|
|
|
|
func (tab *Table) refresh() {
|
2015-04-27 01:50:18 +03:00
|
|
|
// The Kademlia paper specifies that the bucket refresh should
|
|
|
|
// perform a refresh in the least recently used bucket. We cannot
|
|
|
|
// adhere to this because the findnode target is a 512bit value
|
|
|
|
// (not hash-sized) and it is not easily possible to generate a
|
|
|
|
// sha3 preimage that falls into a chosen bucket.
|
|
|
|
//
|
|
|
|
// We perform a lookup with a random target instead.
|
|
|
|
var target NodeID
|
|
|
|
rand.Read(target[:])
|
|
|
|
result := tab.Lookup(target)
|
2015-01-27 15:33:26 +02:00
|
|
|
if len(result) == 0 {
|
2015-04-24 18:04:41 +03:00
|
|
|
// Pick a batch of previously know seeds to lookup with
|
|
|
|
seeds := tab.db.querySeeds(10)
|
2015-04-23 19:24:48 +03:00
|
|
|
for _, seed := range seeds {
|
2015-04-24 18:04:41 +03:00
|
|
|
glog.V(logger.Debug).Infoln("Seeding network with", seed)
|
2015-04-23 19:24:48 +03:00
|
|
|
}
|
|
|
|
// Bootstrap the table with a self lookup
|
|
|
|
all := tab.bondall(append(tab.nursery, seeds...))
|
2015-01-27 15:33:26 +02:00
|
|
|
tab.mutex.Lock()
|
2015-03-25 17:45:53 +02:00
|
|
|
tab.add(all)
|
2015-01-27 15:33:26 +02:00
|
|
|
tab.mutex.Unlock()
|
|
|
|
tab.Lookup(tab.self.ID)
|
|
|
|
// TODO: the Kademlia paper says that we're supposed to perform
|
|
|
|
// random lookups in all buckets further away than our closest neighbor.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// closest returns the n nodes in the table that are closest to the
|
|
|
|
// given id. The caller must hold tab.mutex.
|
2015-04-27 01:50:18 +03:00
|
|
|
func (tab *Table) closest(target common.Hash, nresults int) *nodesByDistance {
|
2015-01-27 15:33:26 +02:00
|
|
|
// This is a very wasteful way to find the closest nodes but
|
|
|
|
// obviously correct. I believe that tree-based buckets would make
|
|
|
|
// this easier to implement efficiently.
|
|
|
|
close := &nodesByDistance{target: target}
|
|
|
|
for _, b := range tab.buckets {
|
|
|
|
for _, n := range b.entries {
|
|
|
|
close.push(n, nresults)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return close
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tab *Table) len() (n int) {
|
|
|
|
for _, b := range tab.buckets {
|
|
|
|
n += len(b.entries)
|
|
|
|
}
|
|
|
|
return n
|
|
|
|
}
|
|
|
|
|
2015-03-25 17:45:53 +02:00
|
|
|
// bondall bonds with all given nodes concurrently and returns
|
|
|
|
// those nodes for which bonding has probably succeeded.
|
|
|
|
func (tab *Table) bondall(nodes []*Node) (result []*Node) {
|
|
|
|
rc := make(chan *Node, len(nodes))
|
|
|
|
for i := range nodes {
|
|
|
|
go func(n *Node) {
|
2015-04-18 02:50:31 +03:00
|
|
|
nn, _ := tab.bond(false, n.ID, n.addr(), uint16(n.TCP))
|
2015-03-25 17:45:53 +02:00
|
|
|
rc <- nn
|
|
|
|
}(nodes[i])
|
|
|
|
}
|
|
|
|
for _ = range nodes {
|
|
|
|
if n := <-rc; n != nil {
|
|
|
|
result = append(result, n)
|
2015-01-27 15:33:26 +02:00
|
|
|
}
|
|
|
|
}
|
2015-03-25 17:45:53 +02:00
|
|
|
return result
|
2015-01-27 15:33:26 +02:00
|
|
|
}
|
|
|
|
|
2015-03-25 17:45:53 +02:00
|
|
|
// bond ensures the local node has a bond with the given remote node.
|
|
|
|
// It also attempts to insert the node into the table if bonding succeeds.
|
|
|
|
// The caller must not hold tab.mutex.
|
|
|
|
//
|
|
|
|
// A bond is must be established before sending findnode requests.
|
|
|
|
// Both sides must have completed a ping/pong exchange for a bond to
|
|
|
|
// exist. The total number of active bonding processes is limited in
|
|
|
|
// order to restrain network use.
|
|
|
|
//
|
|
|
|
// bond is meant to operate idempotently in that bonding with a remote
|
|
|
|
// node which still remembers a previously established bond will work.
|
|
|
|
// The remote node will simply not send a ping back, causing waitping
|
|
|
|
// to time out.
|
|
|
|
//
|
|
|
|
// If pinged is true, the remote node has just pinged us and one half
|
|
|
|
// of the process can be skipped.
|
|
|
|
func (tab *Table) bond(pinged bool, id NodeID, addr *net.UDPAddr, tcpPort uint16) (*Node, error) {
|
|
|
|
var n *Node
|
2015-04-24 18:04:41 +03:00
|
|
|
if n = tab.db.node(id); n == nil {
|
2015-03-25 17:45:53 +02:00
|
|
|
tab.bondmu.Lock()
|
|
|
|
w := tab.bonding[id]
|
|
|
|
if w != nil {
|
|
|
|
// Wait for an existing bonding process to complete.
|
|
|
|
tab.bondmu.Unlock()
|
|
|
|
<-w.done
|
|
|
|
} else {
|
|
|
|
// Register a new bonding process.
|
|
|
|
w = &bondproc{done: make(chan struct{})}
|
|
|
|
tab.bonding[id] = w
|
|
|
|
tab.bondmu.Unlock()
|
|
|
|
// Do the ping/pong. The result goes into w.
|
|
|
|
tab.pingpong(w, pinged, id, addr, tcpPort)
|
|
|
|
// Unregister the process after it's done.
|
|
|
|
tab.bondmu.Lock()
|
|
|
|
delete(tab.bonding, id)
|
|
|
|
tab.bondmu.Unlock()
|
2015-01-27 15:33:26 +02:00
|
|
|
}
|
2015-03-25 17:45:53 +02:00
|
|
|
n = w.n
|
|
|
|
if w.err != nil {
|
|
|
|
return nil, w.err
|
2015-01-27 15:33:26 +02:00
|
|
|
}
|
2015-03-25 17:45:53 +02:00
|
|
|
}
|
|
|
|
tab.mutex.Lock()
|
|
|
|
defer tab.mutex.Unlock()
|
2015-04-27 01:50:18 +03:00
|
|
|
b := tab.buckets[logdist(tab.self.sha, n.sha)]
|
|
|
|
if !b.bump(n) {
|
2015-03-25 17:45:53 +02:00
|
|
|
tab.pingreplace(n, b)
|
|
|
|
}
|
|
|
|
return n, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tab *Table) pingpong(w *bondproc, pinged bool, id NodeID, addr *net.UDPAddr, tcpPort uint16) {
|
2015-04-24 18:04:41 +03:00
|
|
|
// Request a bonding slot to limit network usage
|
2015-03-25 17:45:53 +02:00
|
|
|
<-tab.bondslots
|
|
|
|
defer func() { tab.bondslots <- struct{}{} }()
|
2015-04-24 18:04:41 +03:00
|
|
|
|
|
|
|
// Ping the remote side and wait for a pong
|
2015-04-27 14:56:42 +03:00
|
|
|
if w.err = tab.ping(id, addr); w.err != nil {
|
2015-03-25 17:45:53 +02:00
|
|
|
close(w.done)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !pinged {
|
|
|
|
// Give the remote node a chance to ping us before we start
|
|
|
|
// sending findnode requests. If they still remember us,
|
|
|
|
// waitping will simply time out.
|
|
|
|
tab.net.waitping(id)
|
|
|
|
}
|
2015-04-24 18:04:41 +03:00
|
|
|
// Bonding succeeded, update the node database
|
2015-04-23 13:11:21 +03:00
|
|
|
w.n = newNode(id, addr.IP, uint16(addr.Port), tcpPort)
|
2015-04-24 18:04:41 +03:00
|
|
|
tab.db.updateNode(w.n)
|
2015-03-25 17:45:53 +02:00
|
|
|
close(w.done)
|
2015-01-27 15:33:26 +02:00
|
|
|
}
|
|
|
|
|
2015-03-25 17:45:53 +02:00
|
|
|
func (tab *Table) pingreplace(new *Node, b *bucket) {
|
|
|
|
if len(b.entries) == bucketSize {
|
|
|
|
oldest := b.entries[bucketSize-1]
|
2015-04-27 14:56:42 +03:00
|
|
|
if err := tab.ping(oldest.ID, oldest.addr()); err == nil {
|
2015-03-25 17:45:53 +02:00
|
|
|
// The node responded, we don't need to replace it.
|
|
|
|
return
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Add a slot at the end so the last entry doesn't
|
|
|
|
// fall off when adding the new node.
|
|
|
|
b.entries = append(b.entries, nil)
|
|
|
|
}
|
|
|
|
copy(b.entries[1:], b.entries)
|
|
|
|
b.entries[0] = new
|
2015-01-27 15:33:26 +02:00
|
|
|
}
|
|
|
|
|
2015-04-27 14:56:42 +03:00
|
|
|
// ping a remote endpoint and wait for a reply, also updating the node database
|
|
|
|
// accordingly.
|
|
|
|
func (tab *Table) ping(id NodeID, addr *net.UDPAddr) error {
|
|
|
|
// Update the last ping and send the message
|
|
|
|
tab.db.updateLastPing(id, time.Now())
|
|
|
|
if err := tab.net.ping(id, addr); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
// Pong received, update the database and return
|
|
|
|
tab.db.updateLastPong(id, time.Now())
|
2015-04-28 10:28:04 +03:00
|
|
|
tab.db.ensureExpirer()
|
2015-04-27 17:38:28 +03:00
|
|
|
|
2015-04-27 14:56:42 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-01-27 15:33:26 +02:00
|
|
|
// add puts the entries into the table if their corresponding
|
|
|
|
// bucket is not full. The caller must hold tab.mutex.
|
|
|
|
func (tab *Table) add(entries []*Node) {
|
|
|
|
outer:
|
|
|
|
for _, n := range entries {
|
2015-04-23 13:11:21 +03:00
|
|
|
if n.ID == tab.self.ID {
|
|
|
|
// don't add self.
|
2015-01-27 15:33:26 +02:00
|
|
|
continue
|
|
|
|
}
|
2015-04-27 01:50:18 +03:00
|
|
|
bucket := tab.buckets[logdist(tab.self.sha, n.sha)]
|
2015-01-27 15:33:26 +02:00
|
|
|
for i := range bucket.entries {
|
|
|
|
if bucket.entries[i].ID == n.ID {
|
|
|
|
// already in bucket
|
|
|
|
continue outer
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(bucket.entries) < bucketSize {
|
|
|
|
bucket.entries = append(bucket.entries, n)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-25 17:45:53 +02:00
|
|
|
func (b *bucket) bump(n *Node) bool {
|
|
|
|
for i := range b.entries {
|
|
|
|
if b.entries[i].ID == n.ID {
|
2015-01-27 15:33:26 +02:00
|
|
|
// move it to the front
|
2015-03-30 18:23:28 +03:00
|
|
|
copy(b.entries[1:], b.entries[:i])
|
2015-01-27 15:33:26 +02:00
|
|
|
b.entries[0] = n
|
2015-03-25 17:45:53 +02:00
|
|
|
return true
|
2015-01-27 15:33:26 +02:00
|
|
|
}
|
|
|
|
}
|
2015-03-25 17:45:53 +02:00
|
|
|
return false
|
2015-01-27 15:33:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// nodesByDistance is a list of nodes, ordered by
|
|
|
|
// distance to target.
|
|
|
|
type nodesByDistance struct {
|
|
|
|
entries []*Node
|
2015-04-27 01:50:18 +03:00
|
|
|
target common.Hash
|
2015-01-27 15:33:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// push adds the given node to the list, keeping the total size below maxElems.
|
|
|
|
func (h *nodesByDistance) push(n *Node, maxElems int) {
|
|
|
|
ix := sort.Search(len(h.entries), func(i int) bool {
|
2015-04-27 01:50:18 +03:00
|
|
|
return distcmp(h.target, h.entries[i].sha, n.sha) > 0
|
2015-01-27 15:33:26 +02:00
|
|
|
})
|
|
|
|
if len(h.entries) < maxElems {
|
|
|
|
h.entries = append(h.entries, n)
|
|
|
|
}
|
|
|
|
if ix == len(h.entries) {
|
|
|
|
// farther away than all nodes we already have.
|
|
|
|
// if there was room for it, the node is now the last element.
|
|
|
|
} else {
|
|
|
|
// slide existing entries down to make room
|
|
|
|
// this will overwrite the entry we just appended.
|
|
|
|
copy(h.entries[ix+1:], h.entries[ix:])
|
|
|
|
h.entries[ix] = n
|
|
|
|
}
|
|
|
|
}
|