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 }