gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/renter/hostdb/hostdb.go (about)

     1  // Package hostdb provides a HostDB object that implements the renter.hostDB
     2  // interface. The blockchain is scanned for host announcements and hosts that
     3  // are found get added to the host database. The database continually scans the
     4  // set of hosts it has found and updates who is online.
     5  package hostdb
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"reflect"
    12  	"sort"
    13  	"sync"
    14  	"time"
    15  
    16  	"gitlab.com/NebulousLabs/errors"
    17  	"gitlab.com/NebulousLabs/threadgroup"
    18  
    19  	"gitlab.com/SiaPrime/SiaPrime/build"
    20  	"gitlab.com/SiaPrime/SiaPrime/modules"
    21  	"gitlab.com/SiaPrime/SiaPrime/modules/renter/hostdb/hosttree"
    22  	"gitlab.com/SiaPrime/SiaPrime/persist"
    23  	"gitlab.com/SiaPrime/SiaPrime/types"
    24  )
    25  
    26  var (
    27  	// ErrInitialScanIncomplete is returned whenever an operation is not
    28  	// allowed to be executed before the initial host scan has finished.
    29  	ErrInitialScanIncomplete = errors.New("initial hostdb scan is not yet completed")
    30  	errNilCS                 = errors.New("cannot create hostdb with nil consensus set")
    31  	errNilGateway            = errors.New("cannot create hostdb with nil gateway")
    32  	errNilTPool              = errors.New("cannot create hostdb with nil transaction pool")
    33  )
    34  
    35  // contractInfo contains information about a contract relevant to the HostDB.
    36  type contractInfo struct {
    37  	HostPublicKey types.SiaPublicKey
    38  	StoredData    uint64 `json:"storeddata"`
    39  }
    40  
    41  // The HostDB is a database of potential hosts. It assigns a weight to each
    42  // host based on their hosting parameters, and then can select hosts at random
    43  // for uploading files.
    44  type HostDB struct {
    45  	// dependencies
    46  	cs      modules.ConsensusSet
    47  	deps    modules.Dependencies
    48  	gateway modules.Gateway
    49  	tpool   modules.TransactionPool
    50  
    51  	log        *persist.Logger
    52  	mu         sync.RWMutex
    53  	persistDir string
    54  	tg         threadgroup.ThreadGroup
    55  
    56  	// knownContracts are contracts which the HostDB was informed about by the
    57  	// Contractor. It contains infos about active contracts we have formed with
    58  	// hosts. The mapkey is a serialized SiaPublicKey.
    59  	knownContracts map[string]contractInfo
    60  
    61  	// The hostdb gets initialized with an allowance that can be modified. The
    62  	// allowance is used to build a weightFunc that the hosttree depends on to
    63  	// determine the weight of a host.
    64  	allowance  modules.Allowance
    65  	weightFunc hosttree.WeightFunc
    66  
    67  	// txnFees are the most recent fees used in the score estimation. It is
    68  	// used to determine if the transaction fees have changed enough to warrant
    69  	// rebuilding the hosttree with an updated weight function.
    70  	txnFees types.Currency
    71  
    72  	// The hostTree is the root node of the tree that organizes hosts by
    73  	// weight. The tree is necessary for selecting weighted hosts at
    74  	// random.
    75  	hostTree *hosttree.HostTree
    76  
    77  	// the scanPool is a set of hosts that need to be scanned. There are a
    78  	// handful of goroutines constantly waiting on the channel for hosts to
    79  	// scan. The scan map is used to prevent duplicates from entering the scan
    80  	// pool.
    81  	initialScanComplete  bool
    82  	initialScanLatencies []time.Duration
    83  	// NOTE: disableIPViolationCheck bool moved to HostTree where it is used
    84  	scanList        []modules.HostDBEntry
    85  	scanMap         map[string]struct{}
    86  	scanWait        bool
    87  	scanningThreads int
    88  
    89  	// filteredTree is a hosttree that only contains the hosts that align with
    90  	// the filterMode. The filteredHosts are the hosts that are submitted with
    91  	// the filterMode to determine which host should be in the filteredTree
    92  	filteredTree  *hosttree.HostTree
    93  	filteredHosts map[string]types.SiaPublicKey
    94  	filterMode    modules.FilterMode
    95  
    96  	blockHeight types.BlockHeight
    97  	lastChange  modules.ConsensusChangeID
    98  }
    99  
   100  // insert inserts the HostDBEntry into both hosttrees
   101  func (hdb *HostDB) insert(host modules.HostDBEntry) error {
   102  	err := hdb.hostTree.Insert(host)
   103  	_, ok := hdb.filteredHosts[host.PublicKey.String()]
   104  	isWhitelist := hdb.filterMode == modules.HostDBActiveWhitelist
   105  	if isWhitelist == ok {
   106  		errF := hdb.filteredTree.Insert(host)
   107  		if errF != nil && errF != hosttree.ErrHostExists {
   108  			err = errors.Compose(err, errF)
   109  		}
   110  	}
   111  	return err
   112  }
   113  
   114  // modify modifies the HostDBEntry in both hosttrees
   115  func (hdb *HostDB) modify(host modules.HostDBEntry) error {
   116  	err := hdb.hostTree.Modify(host)
   117  	_, ok := hdb.filteredHosts[host.PublicKey.String()]
   118  	isWhitelist := hdb.filterMode == modules.HostDBActiveWhitelist
   119  	if isWhitelist == ok {
   120  		err = errors.Compose(err, hdb.filteredTree.Modify(host))
   121  	}
   122  	return err
   123  }
   124  
   125  // remove removes the HostDBEntry from both hosttrees
   126  func (hdb *HostDB) remove(pk types.SiaPublicKey) error {
   127  	err := hdb.hostTree.Remove(pk)
   128  	_, ok := hdb.filteredHosts[pk.String()]
   129  	isWhitelist := hdb.filterMode == modules.HostDBActiveWhitelist
   130  	if isWhitelist == ok {
   131  		errF := hdb.filteredTree.Remove(pk)
   132  		if err == nil && errF == hosttree.ErrNoSuchHost {
   133  			return nil
   134  		}
   135  		err = errors.Compose(err, errF)
   136  	}
   137  	return err
   138  }
   139  
   140  // managedSetWeightFunction is a helper function that sets the weightFunc field
   141  // of the hostdb and also updates the the weight function used by the hosttrees
   142  // by rebuilding them. Apart from the constructor of the hostdb, this method
   143  // should be used to update the weight function in the hostdb and hosttrees.
   144  func (hdb *HostDB) managedSetWeightFunction(wf hosttree.WeightFunc) error {
   145  	// Set the weight function in the hostdb.
   146  	hdb.mu.Lock()
   147  	defer hdb.mu.Unlock()
   148  	hdb.weightFunc = wf
   149  	// Update the hosttree and also the filteredTree if they are not the same.
   150  	err := hdb.hostTree.SetWeightFunction(wf)
   151  	if hdb.filteredTree != hdb.hostTree {
   152  		err = errors.Compose(err, hdb.filteredTree.SetWeightFunction(wf))
   153  	}
   154  	return err
   155  }
   156  
   157  // updateContracts rebuilds the knownContracts of the HostDB using the provided
   158  // contracts.
   159  func (hdb *HostDB) updateContracts(contracts []modules.RenterContract) {
   160  	knownContracts := make(map[string]contractInfo)
   161  	for _, contract := range contracts {
   162  		if n := len(contract.Transaction.FileContractRevisions); n != 1 {
   163  			build.Critical("contract's transaction should contain 1 revision but had ", n)
   164  			continue
   165  		}
   166  		knownContracts[contract.HostPublicKey.String()] = contractInfo{
   167  			HostPublicKey: contract.HostPublicKey,
   168  			StoredData:    contract.Transaction.FileContractRevisions[0].NewFileSize,
   169  		}
   170  	}
   171  	hdb.knownContracts = knownContracts
   172  }
   173  
   174  // New returns a new HostDB.
   175  func New(g modules.Gateway, cs modules.ConsensusSet, tpool modules.TransactionPool, persistDir string) (*HostDB, error) {
   176  	// Create HostDB using production dependencies.
   177  	return NewCustomHostDB(g, cs, tpool, persistDir, modules.ProdDependencies)
   178  }
   179  
   180  // NewCustomHostDB creates a HostDB using the provided dependencies. It loads the old
   181  // persistence data, spawns the HostDB's scanning threads, and subscribes it to
   182  // the consensusSet.
   183  func NewCustomHostDB(g modules.Gateway, cs modules.ConsensusSet, tpool modules.TransactionPool, persistDir string, deps modules.Dependencies) (*HostDB, error) {
   184  	// Check for nil inputs.
   185  	if g == nil {
   186  		return nil, errNilGateway
   187  	}
   188  	if cs == nil {
   189  		return nil, errNilCS
   190  	}
   191  	if tpool == nil {
   192  		return nil, errNilTPool
   193  	}
   194  
   195  	// Create the HostDB object.
   196  	hdb := &HostDB{
   197  		cs:         cs,
   198  		deps:       deps,
   199  		gateway:    g,
   200  		persistDir: persistDir,
   201  		tpool:      tpool,
   202  
   203  		filteredHosts:  make(map[string]types.SiaPublicKey),
   204  		knownContracts: make(map[string]contractInfo),
   205  		scanMap:        make(map[string]struct{}),
   206  	}
   207  
   208  	// Set the allowance, txnFees and hostweight function.
   209  	hdb.allowance = modules.DefaultAllowance
   210  	_, hdb.txnFees = hdb.tpool.FeeEstimation()
   211  	hdb.weightFunc = hdb.managedCalculateHostWeightFn(hdb.allowance)
   212  
   213  	// Create the persist directory if it does not yet exist.
   214  	err := os.MkdirAll(persistDir, 0700)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  
   219  	// Create the logger.
   220  	logger, err := persist.NewFileLogger(filepath.Join(persistDir, "hostdb.log"))
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  	hdb.log = logger
   225  	err = hdb.tg.AfterStop(func() error {
   226  		if err := hdb.log.Close(); err != nil {
   227  			// Resort to println as the logger is in an uncertain state.
   228  			fmt.Println("Failed to close the hostdb logger:", err)
   229  			return err
   230  		}
   231  		return nil
   232  	})
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  
   237  	// The host tree is used to manage hosts and query them at random. The
   238  	// filteredTree is used when whitelist or blacklist is enabled
   239  	hdb.hostTree = hosttree.New(hdb.weightFunc, deps.Resolver())
   240  	hdb.filteredTree = hdb.hostTree
   241  
   242  	// Load the prior persistence structures.
   243  	hdb.mu.Lock()
   244  	err = hdb.load()
   245  	hdb.mu.Unlock()
   246  	if err != nil && !os.IsNotExist(err) {
   247  		return nil, err
   248  	}
   249  	err = hdb.tg.AfterStop(func() error {
   250  		hdb.mu.Lock()
   251  		err := hdb.saveSync()
   252  		hdb.mu.Unlock()
   253  		if err != nil {
   254  			hdb.log.Println("Unable to save the hostdb:", err)
   255  			return err
   256  		}
   257  		return nil
   258  	})
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	// Loading is complete, establish the save loop.
   264  	go hdb.threadedSaveLoop()
   265  
   266  	// Don't perform the remaining startup in the presence of a quitAfterLoad
   267  	// disruption.
   268  	if hdb.deps.Disrupt("quitAfterLoad") {
   269  		return hdb, nil
   270  	}
   271  
   272  	// COMPATv1.1.0
   273  	//
   274  	// If the block height has loaded as zero, the most recent consensus change
   275  	// needs to be set to perform a full rescan. This will also help the hostdb
   276  	// to pick up any hosts that it has incorrectly dropped in the past.
   277  	hdb.mu.Lock()
   278  	if hdb.blockHeight == 0 {
   279  		hdb.lastChange = modules.ConsensusChangeBeginning
   280  	}
   281  	hdb.mu.Unlock()
   282  
   283  	err = cs.ConsensusSetSubscribe(hdb, hdb.lastChange, hdb.tg.StopChan())
   284  	if err == modules.ErrInvalidConsensusChangeID {
   285  		// Subscribe again using the new ID. This will cause a triggered scan
   286  		// on all of the hosts, but that should be acceptable.
   287  		hdb.mu.Lock()
   288  		hdb.blockHeight = 0
   289  		hdb.lastChange = modules.ConsensusChangeBeginning
   290  		hdb.mu.Unlock()
   291  		err = cs.ConsensusSetSubscribe(hdb, hdb.lastChange, hdb.tg.StopChan())
   292  	}
   293  	if err != nil {
   294  		return nil, errors.New("hostdb subscription failed: " + err.Error())
   295  	}
   296  	err = hdb.tg.OnStop(func() error {
   297  		cs.Unsubscribe(hdb)
   298  		return nil
   299  	})
   300  	if err != nil {
   301  		return nil, err
   302  	}
   303  
   304  	// Spawn the scan loop during production, but allow it to be disrupted
   305  	// during testing. Primary reason is so that we can fill the hostdb with
   306  	// fake hosts and not have them marked as offline as the scanloop operates.
   307  	if !hdb.deps.Disrupt("disableScanLoop") {
   308  		go hdb.threadedScan()
   309  	} else {
   310  		hdb.initialScanComplete = true
   311  	}
   312  
   313  	return hdb, nil
   314  }
   315  
   316  // ActiveHosts returns a list of hosts that are currently online, sorted by
   317  // weight. If hostdb is in black or white list mode, then only active hosts from
   318  // the filteredTree will be returned
   319  func (hdb *HostDB) ActiveHosts() (activeHosts []modules.HostDBEntry) {
   320  	hdb.mu.RLock()
   321  	allHosts := hdb.filteredTree.All()
   322  	hdb.mu.RUnlock()
   323  	for _, entry := range allHosts {
   324  		if len(entry.ScanHistory) == 0 {
   325  			continue
   326  		}
   327  		if !entry.ScanHistory[len(entry.ScanHistory)-1].Success {
   328  			continue
   329  		}
   330  		if !entry.AcceptingContracts {
   331  			continue
   332  		}
   333  		activeHosts = append(activeHosts, entry)
   334  	}
   335  	return activeHosts
   336  }
   337  
   338  // AllHosts returns all of the hosts known to the hostdb, including the inactive
   339  // ones. AllHosts is not filtered by blacklist or whitelist mode.
   340  func (hdb *HostDB) AllHosts() (allHosts []modules.HostDBEntry) {
   341  	hdb.mu.RLock()
   342  	defer hdb.mu.RUnlock()
   343  	return hdb.hostTree.All()
   344  }
   345  
   346  // CheckForIPViolations accepts a number of host public keys and returns the
   347  // ones that violate the rules of the addressFilter.
   348  func (hdb *HostDB) CheckForIPViolations(hosts []types.SiaPublicKey) []types.SiaPublicKey {
   349  	// If the check was disabled we don't return any bad hosts.
   350  	hdb.mu.RLock()
   351  	defer hdb.mu.RUnlock()
   352  
   353  	if !hdb.hostTree.FilterByIPEnabled() {
   354  		return nil
   355  	}
   356  
   357  	var entries []modules.HostDBEntry
   358  	var badHosts []types.SiaPublicKey
   359  
   360  	// Get the entries which correspond to the keys.
   361  	for _, host := range hosts {
   362  		entry, exists := hdb.hostTree.Select(host)
   363  		if !exists {
   364  			// A host that's not in the hostdb is bad.
   365  			badHosts = append(badHosts, host)
   366  			continue
   367  		}
   368  		entries = append(entries, entry)
   369  	}
   370  
   371  	// Sort the entries by the amount of time they have occupied their
   372  	// corresponding subnets. This is the order in which they will be passed
   373  	// into the filter which prioritizes entries which are passed in earlier.
   374  	// That means 'younger' entries will be replaced in case of a violation.
   375  	sort.Slice(entries, func(i, j int) bool {
   376  		return entries[i].LastIPNetChange.Before(entries[j].LastIPNetChange)
   377  	})
   378  
   379  	// Create a filter and apply it.
   380  	filter := hosttree.NewFilter(hdb.deps.Resolver())
   381  	for _, entry := range entries {
   382  		// Check if the host violates the rules.
   383  		if filter.Filtered(entry.NetAddress) {
   384  			badHosts = append(badHosts, entry.PublicKey)
   385  			continue
   386  		}
   387  		// If it didn't then we add it to the filter.
   388  		filter.Add(entry.NetAddress)
   389  	}
   390  	return badHosts
   391  }
   392  
   393  // Close closes the hostdb, terminating its scanning threads
   394  func (hdb *HostDB) Close() error {
   395  	return hdb.tg.Stop()
   396  }
   397  
   398  // Host returns the HostSettings associated with the specified pubkey. If no
   399  // matching host is found, Host returns false.  For black and white list modes,
   400  // the Filtered field for the HostDBEntry is set to indicate it the host is
   401  // being filtered from the filtered hosttree
   402  func (hdb *HostDB) Host(spk types.SiaPublicKey) (modules.HostDBEntry, bool) {
   403  	hdb.mu.Lock()
   404  	whitelist := hdb.filterMode == modules.HostDBActiveWhitelist
   405  	filteredHosts := hdb.filteredHosts
   406  	hdb.mu.Unlock()
   407  	host, exists := hdb.hostTree.Select(spk)
   408  	if !exists {
   409  		return host, exists
   410  	}
   411  	_, ok := filteredHosts[spk.String()]
   412  	host.Filtered = whitelist != ok
   413  	hdb.mu.RLock()
   414  	updateHostHistoricInteractions(&host, hdb.blockHeight)
   415  	hdb.mu.RUnlock()
   416  	return host, exists
   417  }
   418  
   419  // Filter returns the hostdb's filterMode and filteredHosts
   420  func (hdb *HostDB) Filter() (modules.FilterMode, map[string]types.SiaPublicKey) {
   421  	hdb.mu.RLock()
   422  	defer hdb.mu.RUnlock()
   423  	filteredHosts := make(map[string]types.SiaPublicKey)
   424  	for k, v := range hdb.filteredHosts {
   425  		filteredHosts[k] = v
   426  	}
   427  	return hdb.filterMode, filteredHosts
   428  }
   429  
   430  // SetFilterMode sets the hostdb filter mode
   431  func (hdb *HostDB) SetFilterMode(fm modules.FilterMode, hosts []types.SiaPublicKey) error {
   432  	if err := hdb.tg.Add(); err != nil {
   433  		return err
   434  	}
   435  	defer hdb.tg.Done()
   436  	hdb.mu.Lock()
   437  	defer hdb.mu.Unlock()
   438  
   439  	// Check for error
   440  	if fm == modules.HostDBFilterError {
   441  		return errors.New("Cannot set hostdb filter mode, provided filter mode is an error")
   442  	}
   443  	// Check if disabling
   444  	if fm == modules.HostDBDisableFilter {
   445  		// Reset filtered field for hosts
   446  		for _, pk := range hdb.filteredHosts {
   447  			err := hdb.hostTree.SetFiltered(pk, false)
   448  			if err != nil {
   449  				hdb.log.Println("Unable to mark entry as not filtered:", err)
   450  			}
   451  		}
   452  		// Reset filtered fields
   453  		hdb.filteredTree = hdb.hostTree
   454  		hdb.filteredHosts = make(map[string]types.SiaPublicKey)
   455  		hdb.filterMode = fm
   456  		return nil
   457  	}
   458  
   459  	// Check for no hosts submitted with whitelist enabled
   460  	isWhitelist := fm == modules.HostDBActiveWhitelist
   461  	if len(hosts) == 0 && isWhitelist {
   462  		return errors.New("cannot enable whitelist without hosts")
   463  	}
   464  
   465  	// Create filtered HostTree
   466  	hdb.filteredTree = hosttree.New(hdb.weightFunc, modules.ProdDependencies.Resolver())
   467  
   468  	// Create filteredHosts map
   469  	filteredHosts := make(map[string]types.SiaPublicKey)
   470  	for _, h := range hosts {
   471  		// Add host to filtered host map
   472  		if _, ok := filteredHosts[h.String()]; ok {
   473  			continue
   474  		}
   475  		filteredHosts[h.String()] = h
   476  
   477  		// Update host in unfiltered hosttree
   478  		err := hdb.hostTree.SetFiltered(h, true)
   479  		if err != nil {
   480  			hdb.log.Println("Unable to mark entry as filtered:", err)
   481  		}
   482  	}
   483  	var allErrs error
   484  	allHosts := hdb.hostTree.All()
   485  	for _, host := range allHosts {
   486  		// Add hosts to filtered tree
   487  		_, ok := filteredHosts[host.PublicKey.String()]
   488  		if isWhitelist != ok {
   489  			continue
   490  		}
   491  		err := hdb.filteredTree.Insert(host)
   492  		if err != nil {
   493  			allErrs = errors.Compose(allErrs, err)
   494  		}
   495  	}
   496  	hdb.filteredHosts = filteredHosts
   497  	hdb.filterMode = fm
   498  	return errors.Compose(allErrs, hdb.saveSync())
   499  }
   500  
   501  // InitialScanComplete returns a boolean indicating if the initial scan of the
   502  // hostdb is completed.
   503  func (hdb *HostDB) InitialScanComplete() (complete bool, err error) {
   504  	if err = hdb.tg.Add(); err != nil {
   505  		return
   506  	}
   507  	defer hdb.tg.Done()
   508  	hdb.mu.Lock()
   509  	defer hdb.mu.Unlock()
   510  	complete = hdb.initialScanComplete
   511  	return
   512  }
   513  
   514  // IPViolationsCheck returns a boolean indicating if the IP violation check is
   515  // enabled or not.
   516  func (hdb *HostDB) IPViolationsCheck() bool {
   517  	return hdb.hostTree.FilterByIPEnabled()
   518  }
   519  
   520  // SetAllowance updates the allowance used by the hostdb for weighing hosts by
   521  // updating the host weight function. It will completely rebuild the hosttree so
   522  // it should be used with care.
   523  func (hdb *HostDB) SetAllowance(allowance modules.Allowance) error {
   524  	// If the allowance is empty, set it to the default allowance. This ensures
   525  	// that the estimates are at least moderately grounded.
   526  	if reflect.DeepEqual(allowance, modules.Allowance{}) {
   527  		allowance = modules.DefaultAllowance
   528  	}
   529  
   530  	// Update the allowance.
   531  	hdb.mu.Lock()
   532  	hdb.allowance = allowance
   533  	hdb.mu.Unlock()
   534  
   535  	// Update the weight function.
   536  	wf := hdb.managedCalculateHostWeightFn(allowance)
   537  	return hdb.managedSetWeightFunction(wf)
   538  }
   539  
   540  // SetIPViolationCheck enables or disables the IP violation check. If disabled,
   541  // CheckForIPViolations won't return bad hosts and RandomHosts will return the
   542  // address blacklist.
   543  func (hdb *HostDB) SetIPViolationCheck(enabled bool) {
   544  	hdb.hostTree.SetFilterByIPEnabled(enabled)
   545  }
   546  
   547  // UpdateContracts rebuilds the knownContracts of the HostBD using the provided
   548  // contracts.
   549  func (hdb *HostDB) UpdateContracts(contracts []modules.RenterContract) error {
   550  	if err := hdb.tg.Add(); err != nil {
   551  		return err
   552  	}
   553  	defer hdb.tg.Done()
   554  	hdb.mu.Lock()
   555  	defer hdb.mu.Unlock()
   556  	hdb.updateContracts(contracts)
   557  	return nil
   558  }