github.com/core-coin/go-core/v2@v2.1.9/les/serverpool.go (about)

     1  // Copyright 2016 by the Authors
     2  // This file is part of the go-core library.
     3  //
     4  // The go-core library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-core library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-core library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package les
    18  
    19  import (
    20  	"errors"
    21  	"math/rand"
    22  	"reflect"
    23  	"sync"
    24  	"sync/atomic"
    25  	"time"
    26  
    27  	"github.com/core-coin/go-core/v2/xcbdb"
    28  
    29  	"github.com/core-coin/go-core/v2/common/mclock"
    30  	lpc "github.com/core-coin/go-core/v2/les/lespay/client"
    31  	"github.com/core-coin/go-core/v2/les/utils"
    32  	"github.com/core-coin/go-core/v2/log"
    33  	"github.com/core-coin/go-core/v2/p2p/enode"
    34  	"github.com/core-coin/go-core/v2/p2p/enr"
    35  	"github.com/core-coin/go-core/v2/p2p/nodestate"
    36  	"github.com/core-coin/go-core/v2/rlp"
    37  )
    38  
    39  const (
    40  	minTimeout          = time.Millisecond * 500 // minimum request timeout suggested by the server pool
    41  	timeoutRefresh      = time.Second * 5        // recalculate timeout if older than this
    42  	dialCost            = 10000                  // cost of a TCP dial (used for known node selection weight calculation)
    43  	dialWaitStep        = 1.5                    // exponential multiplier of redial wait time when no value was provided by the server
    44  	queryCost           = 500                    // cost of a UDP pre-negotiation query
    45  	queryWaitStep       = 1.02                   // exponential multiplier of redial wait time when no value was provided by the server
    46  	waitThreshold       = time.Hour * 2000       // drop node if waiting time is over the threshold
    47  	nodeWeightMul       = 1000000                // multiplier constant for node weight calculation
    48  	nodeWeightThreshold = 100                    // minimum weight for keeping a node in the the known (valuable) set
    49  	minRedialWait       = 10                     // minimum redial wait time in seconds
    50  	preNegLimit         = 5                      // maximum number of simultaneous pre-negotiation queries
    51  	maxQueryFails       = 100                    // number of consecutive UDP query failures before we print a warning
    52  )
    53  
    54  // serverPool provides a node iterator for dial candidates. The output is a mix of newly discovered
    55  // nodes, a weighted random selection of known (previously valuable) nodes and trusted/paid nodes.
    56  type serverPool struct {
    57  	clock    mclock.Clock
    58  	unixTime func() int64
    59  	db       xcbdb.KeyValueStore
    60  
    61  	ns           *nodestate.NodeStateMachine
    62  	vt           *lpc.ValueTracker
    63  	mixer        *enode.FairMix
    64  	mixSources   []enode.Iterator
    65  	dialIterator enode.Iterator
    66  	validSchemes enr.IdentityScheme
    67  	trustedURLs  []string
    68  	fillSet      *lpc.FillSet
    69  	queryFails   uint32
    70  
    71  	timeoutLock      sync.RWMutex
    72  	timeout          time.Duration
    73  	timeWeights      lpc.ResponseTimeWeights
    74  	timeoutRefreshed mclock.AbsTime
    75  }
    76  
    77  // nodeHistory keeps track of dial costs which determine node weight together with the
    78  // service value calculated by lpc.ValueTracker.
    79  type nodeHistory struct {
    80  	dialCost                       utils.ExpiredValue
    81  	redialWaitStart, redialWaitEnd int64 // unix time (seconds)
    82  }
    83  
    84  type nodeHistoryEnc struct {
    85  	DialCost                       utils.ExpiredValue
    86  	RedialWaitStart, RedialWaitEnd uint64
    87  }
    88  
    89  // queryFunc sends a pre-negotiation query and blocks until a response arrives or timeout occurs.
    90  // It returns 1 if the remote node has confirmed that connection is possible, 0 if not
    91  // possible and -1 if no response arrived (timeout).
    92  type queryFunc func(*enode.Node) int
    93  
    94  var (
    95  	serverPoolSetup    = &nodestate.Setup{Version: 1}
    96  	sfHasValue         = serverPoolSetup.NewPersistentFlag("hasValue")
    97  	sfQueried          = serverPoolSetup.NewFlag("queried")
    98  	sfCanDial          = serverPoolSetup.NewFlag("canDial")
    99  	sfDialing          = serverPoolSetup.NewFlag("dialed")
   100  	sfWaitDialTimeout  = serverPoolSetup.NewFlag("dialTimeout")
   101  	sfConnected        = serverPoolSetup.NewFlag("connected")
   102  	sfRedialWait       = serverPoolSetup.NewFlag("redialWait")
   103  	sfAlwaysConnect    = serverPoolSetup.NewFlag("alwaysConnect")
   104  	sfDisableSelection = nodestate.MergeFlags(sfQueried, sfCanDial, sfDialing, sfConnected, sfRedialWait)
   105  
   106  	sfiNodeHistory = serverPoolSetup.NewPersistentField("nodeHistory", reflect.TypeOf(nodeHistory{}),
   107  		func(field interface{}) ([]byte, error) {
   108  			if n, ok := field.(nodeHistory); ok {
   109  				ne := nodeHistoryEnc{
   110  					DialCost:        n.dialCost,
   111  					RedialWaitStart: uint64(n.redialWaitStart),
   112  					RedialWaitEnd:   uint64(n.redialWaitEnd),
   113  				}
   114  				enc, err := rlp.EncodeToBytes(&ne)
   115  				return enc, err
   116  			}
   117  			return nil, errors.New("invalid field type")
   118  		},
   119  		func(enc []byte) (interface{}, error) {
   120  			var ne nodeHistoryEnc
   121  			err := rlp.DecodeBytes(enc, &ne)
   122  			n := nodeHistory{
   123  				dialCost:        ne.DialCost,
   124  				redialWaitStart: int64(ne.RedialWaitStart),
   125  				redialWaitEnd:   int64(ne.RedialWaitEnd),
   126  			}
   127  			return n, err
   128  		},
   129  	)
   130  	sfiNodeWeight     = serverPoolSetup.NewField("nodeWeight", reflect.TypeOf(uint64(0)))
   131  	sfiConnectedStats = serverPoolSetup.NewField("connectedStats", reflect.TypeOf(lpc.ResponseTimeStats{}))
   132  )
   133  
   134  // newServerPool creates a new server pool
   135  func newServerPool(db xcbdb.KeyValueStore, dbKey []byte, vt *lpc.ValueTracker, discovery enode.Iterator, mixTimeout time.Duration, query queryFunc, clock mclock.Clock, trustedURLs []string) *serverPool {
   136  	s := &serverPool{
   137  		db:           db,
   138  		clock:        clock,
   139  		unixTime:     func() int64 { return time.Now().Unix() },
   140  		validSchemes: enode.ValidSchemes,
   141  		trustedURLs:  trustedURLs,
   142  		vt:           vt,
   143  		ns:           nodestate.NewNodeStateMachine(db, []byte(string(dbKey)+"ns:"), clock, serverPoolSetup),
   144  	}
   145  	s.recalTimeout()
   146  	s.mixer = enode.NewFairMix(mixTimeout)
   147  	knownSelector := lpc.NewWrsIterator(s.ns, sfHasValue, sfDisableSelection, sfiNodeWeight)
   148  	alwaysConnect := lpc.NewQueueIterator(s.ns, sfAlwaysConnect, sfDisableSelection, true, nil)
   149  	s.mixSources = append(s.mixSources, knownSelector)
   150  	s.mixSources = append(s.mixSources, alwaysConnect)
   151  	if discovery != nil {
   152  		s.mixSources = append(s.mixSources, discovery)
   153  	}
   154  
   155  	iter := enode.Iterator(s.mixer)
   156  	if query != nil {
   157  		iter = s.addPreNegFilter(iter, query)
   158  	}
   159  	s.dialIterator = enode.Filter(iter, func(node *enode.Node) bool {
   160  		s.ns.SetState(node, sfDialing, sfCanDial, 0)
   161  		s.ns.SetState(node, sfWaitDialTimeout, nodestate.Flags{}, time.Second*10)
   162  		return true
   163  	})
   164  
   165  	s.ns.SubscribeState(nodestate.MergeFlags(sfWaitDialTimeout, sfConnected), func(n *enode.Node, oldState, newState nodestate.Flags) {
   166  		if oldState.Equals(sfWaitDialTimeout) && newState.IsEmpty() {
   167  			// dial timeout, no connection
   168  			s.setRedialWait(n, dialCost, dialWaitStep)
   169  			s.ns.SetStateSub(n, nodestate.Flags{}, sfDialing, 0)
   170  		}
   171  	})
   172  
   173  	s.ns.AddLogMetrics(sfHasValue, sfDisableSelection, "selectable", nil, nil, serverSelectableGauge)
   174  	s.ns.AddLogMetrics(sfDialing, nodestate.Flags{}, "dialed", serverDialedMeter, nil, nil)
   175  	s.ns.AddLogMetrics(sfConnected, nodestate.Flags{}, "connected", nil, nil, serverConnectedGauge)
   176  	return s
   177  }
   178  
   179  // addPreNegFilter installs a node filter mechanism that performs a pre-negotiation query.
   180  // Nodes that are filtered out and does not appear on the output iterator are put back
   181  // into redialWait state.
   182  func (s *serverPool) addPreNegFilter(input enode.Iterator, query queryFunc) enode.Iterator {
   183  	s.fillSet = lpc.NewFillSet(s.ns, input, sfQueried)
   184  	s.ns.SubscribeState(sfQueried, func(n *enode.Node, oldState, newState nodestate.Flags) {
   185  		if newState.Equals(sfQueried) {
   186  			fails := atomic.LoadUint32(&s.queryFails)
   187  			if fails == maxQueryFails {
   188  				log.Warn("UDP pre-negotiation query does not seem to work")
   189  			}
   190  			if fails > maxQueryFails {
   191  				fails = maxQueryFails
   192  			}
   193  			if rand.Intn(maxQueryFails*2) < int(fails) {
   194  				// skip pre-negotiation with increasing chance, max 50%
   195  				// this ensures that the client can operate even if UDP is not working at all
   196  				s.ns.SetStateSub(n, sfCanDial, nodestate.Flags{}, time.Second*10)
   197  				// set canDial before resetting queried so that FillSet will not read more
   198  				// candidates unnecessarily
   199  				s.ns.SetStateSub(n, nodestate.Flags{}, sfQueried, 0)
   200  				return
   201  			}
   202  			go func() {
   203  				q := query(n)
   204  				if q == -1 {
   205  					atomic.AddUint32(&s.queryFails, 1)
   206  				} else {
   207  					atomic.StoreUint32(&s.queryFails, 0)
   208  				}
   209  				s.ns.Operation(func() {
   210  					// we are no longer running in the operation that the callback belongs to, start a new one because of setRedialWait
   211  					if q == 1 {
   212  						s.ns.SetStateSub(n, sfCanDial, nodestate.Flags{}, time.Second*10)
   213  					} else {
   214  						s.setRedialWait(n, queryCost, queryWaitStep)
   215  					}
   216  					s.ns.SetStateSub(n, nodestate.Flags{}, sfQueried, 0)
   217  				})
   218  			}()
   219  		}
   220  	})
   221  	return lpc.NewQueueIterator(s.ns, sfCanDial, nodestate.Flags{}, false, func(waiting bool) {
   222  		if waiting {
   223  			s.fillSet.SetTarget(preNegLimit)
   224  		} else {
   225  			s.fillSet.SetTarget(0)
   226  		}
   227  	})
   228  }
   229  
   230  // start starts the server pool. Note that NodeStateMachine should be started first.
   231  func (s *serverPool) start() {
   232  	s.ns.Start()
   233  	for _, iter := range s.mixSources {
   234  		// add sources to mixer at startup because the mixer instantly tries to read them
   235  		// which should only happen after NodeStateMachine has been started
   236  		s.mixer.AddSource(iter)
   237  	}
   238  	for _, url := range s.trustedURLs {
   239  		if node, err := enode.Parse(s.validSchemes, url); err == nil {
   240  			s.ns.SetState(node, sfAlwaysConnect, nodestate.Flags{}, 0)
   241  		} else {
   242  			log.Error("Invalid trusted server URL", "url", url, "error", err)
   243  		}
   244  	}
   245  	unixTime := s.unixTime()
   246  	s.ns.Operation(func() {
   247  		s.ns.ForEach(sfHasValue, nodestate.Flags{}, func(node *enode.Node, state nodestate.Flags) {
   248  			s.calculateWeight(node)
   249  			if n, ok := s.ns.GetField(node, sfiNodeHistory).(nodeHistory); ok && n.redialWaitEnd > unixTime {
   250  				wait := n.redialWaitEnd - unixTime
   251  				lastWait := n.redialWaitEnd - n.redialWaitStart
   252  				if wait > lastWait {
   253  					// if the time until expiration is larger than the last suggested
   254  					// waiting time then the system clock was probably adjusted
   255  					wait = lastWait
   256  				}
   257  				s.ns.SetStateSub(node, sfRedialWait, nodestate.Flags{}, time.Duration(wait)*time.Second)
   258  			}
   259  		})
   260  	})
   261  }
   262  
   263  // stop stops the server pool
   264  func (s *serverPool) stop() {
   265  	s.dialIterator.Close()
   266  	if s.fillSet != nil {
   267  		s.fillSet.Close()
   268  	}
   269  	s.ns.Operation(func() {
   270  		s.ns.ForEach(sfConnected, nodestate.Flags{}, func(n *enode.Node, state nodestate.Flags) {
   271  			// recalculate weight of connected nodes in order to update hasValue flag if necessary
   272  			s.calculateWeight(n)
   273  		})
   274  	})
   275  	s.ns.Stop()
   276  }
   277  
   278  // registerPeer implements serverPeerSubscriber
   279  func (s *serverPool) registerPeer(p *serverPeer) {
   280  	s.ns.SetState(p.Node(), sfConnected, sfDialing.Or(sfWaitDialTimeout), 0)
   281  	nvt := s.vt.Register(p.ID())
   282  	s.ns.SetField(p.Node(), sfiConnectedStats, nvt.RtStats())
   283  	p.setValueTracker(s.vt, nvt)
   284  	p.updateVtParams()
   285  }
   286  
   287  // unregisterPeer implements serverPeerSubscriber
   288  func (s *serverPool) unregisterPeer(p *serverPeer) {
   289  	s.ns.Operation(func() {
   290  		s.setRedialWait(p.Node(), dialCost, dialWaitStep)
   291  		s.ns.SetStateSub(p.Node(), nodestate.Flags{}, sfConnected, 0)
   292  		s.ns.SetFieldSub(p.Node(), sfiConnectedStats, nil)
   293  	})
   294  	s.vt.Unregister(p.ID())
   295  	p.setValueTracker(nil, nil)
   296  }
   297  
   298  // recalTimeout calculates the current recommended timeout. This value is used by
   299  // the client as a "soft timeout" value. It also affects the service value calculation
   300  // of individual nodes.
   301  func (s *serverPool) recalTimeout() {
   302  	// Use cached result if possible, avoid recalculating too frequently.
   303  	s.timeoutLock.RLock()
   304  	refreshed := s.timeoutRefreshed
   305  	s.timeoutLock.RUnlock()
   306  	now := s.clock.Now()
   307  	if refreshed != 0 && time.Duration(now-refreshed) < timeoutRefresh {
   308  		return
   309  	}
   310  	// Cached result is stale, recalculate a new one.
   311  	rts := s.vt.RtStats()
   312  
   313  	// Add a fake statistic here. It is an easy way to initialize with some
   314  	// conservative values when the database is new. As soon as we have a
   315  	// considerable amount of real stats this small value won't matter.
   316  	rts.Add(time.Second*2, 10, s.vt.StatsExpFactor())
   317  
   318  	// Use either 10% failure rate timeout or twice the median response time
   319  	// as the recommended timeout.
   320  	timeout := minTimeout
   321  	if t := rts.Timeout(0.1); t > timeout {
   322  		timeout = t
   323  	}
   324  	if t := rts.Timeout(0.5) * 2; t > timeout {
   325  		timeout = t
   326  	}
   327  	s.timeoutLock.Lock()
   328  	if s.timeout != timeout {
   329  		s.timeout = timeout
   330  		s.timeWeights = lpc.TimeoutWeights(s.timeout)
   331  
   332  		suggestedTimeoutGauge.Update(int64(s.timeout / time.Millisecond))
   333  		totalValueGauge.Update(int64(rts.Value(s.timeWeights, s.vt.StatsExpFactor())))
   334  	}
   335  	s.timeoutRefreshed = now
   336  	s.timeoutLock.Unlock()
   337  }
   338  
   339  // getTimeout returns the recommended request timeout.
   340  func (s *serverPool) getTimeout() time.Duration {
   341  	s.recalTimeout()
   342  	s.timeoutLock.RLock()
   343  	defer s.timeoutLock.RUnlock()
   344  	return s.timeout
   345  }
   346  
   347  // getTimeoutAndWeight returns the recommended request timeout as well as the
   348  // response time weight which is necessary to calculate service value.
   349  func (s *serverPool) getTimeoutAndWeight() (time.Duration, lpc.ResponseTimeWeights) {
   350  	s.recalTimeout()
   351  	s.timeoutLock.RLock()
   352  	defer s.timeoutLock.RUnlock()
   353  	return s.timeout, s.timeWeights
   354  }
   355  
   356  // addDialCost adds the given amount of dial cost to the node history and returns the current
   357  // amount of total dial cost
   358  func (s *serverPool) addDialCost(n *nodeHistory, amount int64) uint64 {
   359  	logOffset := s.vt.StatsExpirer().LogOffset(s.clock.Now())
   360  	if amount > 0 {
   361  		n.dialCost.Add(amount, logOffset)
   362  	}
   363  	totalDialCost := n.dialCost.Value(logOffset)
   364  	if totalDialCost < dialCost {
   365  		totalDialCost = dialCost
   366  	}
   367  	return totalDialCost
   368  }
   369  
   370  // serviceValue returns the service value accumulated in this session and in total
   371  func (s *serverPool) serviceValue(node *enode.Node) (sessionValue, totalValue float64) {
   372  	nvt := s.vt.GetNode(node.ID())
   373  	if nvt == nil {
   374  		return 0, 0
   375  	}
   376  	currentStats := nvt.RtStats()
   377  	_, timeWeights := s.getTimeoutAndWeight()
   378  	expFactor := s.vt.StatsExpFactor()
   379  
   380  	totalValue = currentStats.Value(timeWeights, expFactor)
   381  	if connStats, ok := s.ns.GetField(node, sfiConnectedStats).(lpc.ResponseTimeStats); ok {
   382  		diff := currentStats
   383  		diff.SubStats(&connStats)
   384  		sessionValue = diff.Value(timeWeights, expFactor)
   385  		sessionValueMeter.Mark(int64(sessionValue))
   386  	}
   387  	return
   388  }
   389  
   390  // updateWeight calculates the node weight and updates the nodeWeight field and the
   391  // hasValue flag. It also saves the node state if necessary.
   392  // Note: this function should run inside a NodeStateMachine operation
   393  func (s *serverPool) updateWeight(node *enode.Node, totalValue float64, totalDialCost uint64) {
   394  	weight := uint64(totalValue * nodeWeightMul / float64(totalDialCost))
   395  	if weight >= nodeWeightThreshold {
   396  		s.ns.SetStateSub(node, sfHasValue, nodestate.Flags{}, 0)
   397  		s.ns.SetFieldSub(node, sfiNodeWeight, weight)
   398  	} else {
   399  		s.ns.SetStateSub(node, nodestate.Flags{}, sfHasValue, 0)
   400  		s.ns.SetFieldSub(node, sfiNodeWeight, nil)
   401  		s.ns.SetFieldSub(node, sfiNodeHistory, nil)
   402  	}
   403  	s.ns.Persist(node) // saved if node history or hasValue changed
   404  }
   405  
   406  // setRedialWait calculates and sets the redialWait timeout based on the service value
   407  // and dial cost accumulated during the last session/attempt and in total.
   408  // The waiting time is raised exponentially if no service value has been received in order
   409  // to prevent dialing an unresponsive node frequently for a very long time just because it
   410  // was useful in the past. It can still be occasionally dialed though and once it provides
   411  // a significant amount of service value again its waiting time is quickly reduced or reset
   412  // to the minimum.
   413  // Note: node weight is also recalculated and updated by this function.
   414  // Note 2: this function should run inside a NodeStateMachine operation
   415  func (s *serverPool) setRedialWait(node *enode.Node, addDialCost int64, waitStep float64) {
   416  	n, _ := s.ns.GetField(node, sfiNodeHistory).(nodeHistory)
   417  	sessionValue, totalValue := s.serviceValue(node)
   418  	totalDialCost := s.addDialCost(&n, addDialCost)
   419  
   420  	// if the current dial session has yielded at least the average value/dial cost ratio
   421  	// then the waiting time should be reset to the minimum. If the session value
   422  	// is below average but still positive then timeout is limited to the ratio of
   423  	// average / current service value multiplied by the minimum timeout. If the attempt
   424  	// was unsuccessful then timeout is raised exponentially without limitation.
   425  	// Note: dialCost is used in the formula below even if dial was not attempted at all
   426  	// because the pre-negotiation query did not return a positive result. In this case
   427  	// the ratio has no meaning anyway and waitFactor is always raised, though in smaller
   428  	// steps because queries are cheaper and therefore we can allow more failed attempts.
   429  	unixTime := s.unixTime()
   430  	plannedTimeout := float64(n.redialWaitEnd - n.redialWaitStart) // last planned redialWait timeout
   431  	var actualWait float64                                         // actual waiting time elapsed
   432  	if unixTime > n.redialWaitEnd {
   433  		// the planned timeout has elapsed
   434  		actualWait = plannedTimeout
   435  	} else {
   436  		// if the node was redialed earlier then we do not raise the planned timeout
   437  		// exponentially because that could lead to the timeout rising very high in
   438  		// a short amount of time
   439  		// Note that in case of an early redial actualWait also includes the dial
   440  		// timeout or connection time of the last attempt but it still serves its
   441  		// purpose of preventing the timeout rising quicker than linearly as a function
   442  		// of total time elapsed without a successful connection.
   443  		actualWait = float64(unixTime - n.redialWaitStart)
   444  	}
   445  	// raise timeout exponentially if the last planned timeout has elapsed
   446  	// (use at least the last planned timeout otherwise)
   447  	nextTimeout := actualWait * waitStep
   448  	if plannedTimeout > nextTimeout {
   449  		nextTimeout = plannedTimeout
   450  	}
   451  	// we reduce the waiting time if the server has provided service value during the
   452  	// connection (but never under the minimum)
   453  	a := totalValue * dialCost * float64(minRedialWait)
   454  	b := float64(totalDialCost) * sessionValue
   455  	if a < b*nextTimeout {
   456  		nextTimeout = a / b
   457  	}
   458  	if nextTimeout < minRedialWait {
   459  		nextTimeout = minRedialWait
   460  	}
   461  	wait := time.Duration(float64(time.Second) * nextTimeout)
   462  	if wait < waitThreshold {
   463  		n.redialWaitStart = unixTime
   464  		n.redialWaitEnd = unixTime + int64(nextTimeout)
   465  		s.ns.SetFieldSub(node, sfiNodeHistory, n)
   466  		s.ns.SetStateSub(node, sfRedialWait, nodestate.Flags{}, wait)
   467  		s.updateWeight(node, totalValue, totalDialCost)
   468  	} else {
   469  		// discard known node statistics if waiting time is very long because the node
   470  		// hasn't been responsive for a very long time
   471  		s.ns.SetFieldSub(node, sfiNodeHistory, nil)
   472  		s.ns.SetFieldSub(node, sfiNodeWeight, nil)
   473  		s.ns.SetStateSub(node, nodestate.Flags{}, sfHasValue, 0)
   474  	}
   475  }
   476  
   477  // calculateWeight calculates and sets the node weight without altering the node history.
   478  // This function should be called during startup and shutdown only, otherwise setRedialWait
   479  // will keep the weights updated as the underlying statistics are adjusted.
   480  // Note: this function should run inside a NodeStateMachine operation
   481  func (s *serverPool) calculateWeight(node *enode.Node) {
   482  	n, _ := s.ns.GetField(node, sfiNodeHistory).(nodeHistory)
   483  	_, totalValue := s.serviceValue(node)
   484  	totalDialCost := s.addDialCost(&n, 0)
   485  	s.updateWeight(node, totalValue, totalDialCost)
   486  }