gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/workeraccountpersist.go (about) 1 package renter 2 3 // TODO: Derive the account secret key using the wallet seed. Can use: 4 // `account specifier || wallet seed || host pubkey` I believe. 5 // 6 // If we derive the seeds deterministically, that may mean that we can 7 // regenerate accounts even we fail to load them from disk. When we make a new 8 // account with a host, we should always query that host for a balance even if 9 // we think this is a new account, some previous run on siad may have created 10 // the account for us. 11 // 12 // TODO: How long does the host keep an account open? Does it keep the account 13 // open for the entire period? If not, we should probably adjust that on the 14 // host side, otherwise renters that go offline for a while are going to lose 15 // their accounts because the hosts will expire them. Does the renter track the 16 // expiration date of the accounts? Will it know upload load that the account is 17 // missing from the host not because of malice but because they expired? 18 19 import ( 20 "bytes" 21 "io" 22 "math/big" 23 "os" 24 "path/filepath" 25 "sync" 26 27 "gitlab.com/NebulousLabs/encoding" 28 "gitlab.com/NebulousLabs/errors" 29 "gitlab.com/SkynetLabs/skyd/build" 30 skydPersist "gitlab.com/SkynetLabs/skyd/persist" 31 "gitlab.com/SkynetLabs/skyd/skymodules" 32 "go.sia.tech/siad/crypto" 33 "go.sia.tech/siad/modules" 34 "go.sia.tech/siad/persist" 35 "go.sia.tech/siad/types" 36 ) 37 38 const ( 39 // accountSize is the fixed account size in bytes 40 accountSize = 1 << 10 // 1024 bytes 41 accountSizeV150 = 1 << 8 // 256 bytes 42 accountsOffset = 1 << 12 // 4kib to sector align 43 ) 44 45 var ( 46 // accountsFilename is the filename of the accounts persistence file 47 accountsFilename = "accounts.dat" 48 49 // accountsTmpFilename is the filename of the temporary account file created 50 // when upgrading the account's persistence file. 51 accountsTmpFilename = "accounts.tmp.dat" 52 53 // Metadata 54 metadataHeader = types.NewSpecifier("Accounts\n") 55 metadataVersion = types.NewSpecifier("v1.6.2\n") 56 metadataVersionV161 = types.NewSpecifier("v1.6.1\n") 57 metadataSize = 2*types.SpecifierLen + 1 // 1 byte for 'clean' flag 58 59 // Metadata validation errors 60 errWrongHeader = errors.New("wrong header") 61 errWrongVersion = errors.New("wrong version") 62 63 // Persistence data validation errors 64 errInvalidChecksum = errors.New("invalid checksum") 65 ) 66 67 type ( 68 // accountManager tracks the set of accounts known to the renter. 69 accountManager struct { 70 accounts map[string]*account 71 72 // Utils. The file is global to all accounts, each account looks at a 73 // specific offset within the file. 74 mu sync.Mutex 75 staticFile modules.File 76 staticRenter *Renter 77 } 78 79 // accountsMetadata is the metadata of the accounts persist file 80 accountsMetadata struct { 81 Header types.Specifier 82 Version types.Specifier 83 Clean bool 84 } 85 86 // accountPersistence is the account's persistence object which holds all 87 // data that gets persisted for a single account. 88 accountPersistence struct { 89 AccountID modules.AccountID 90 HostKey types.SiaPublicKey 91 SecretKey crypto.SecretKey 92 93 // balance details, aside from the balance we keep track of the balance 94 // drift, in both directions, that may occur when the renter's account 95 // balance becomes out of sync with the host's version of the balance 96 Balance types.Currency 97 BalanceDriftPositive types.Currency 98 BalanceDriftNegative types.Currency 99 100 // spending details 101 SpendingDownloads types.Currency 102 SpendingRegistryReads types.Currency 103 SpendingRegistryWrites types.Currency 104 SpendingRepairDownloads types.Currency 105 SpendingRepairUploads types.Currency 106 SpendingSnapshotDownloads types.Currency 107 SpendingSnapshotUploads types.Currency 108 SpendingSubscriptions types.Currency 109 SpendingUploads types.Currency 110 111 // The following fields were added in v1.5.11 112 // 113 // residue is the amount of money that was still in the ephemeral 114 // account at the moment the contract renews and the FundAccountCost 115 // goes back to zero, we need to keep track of the residue in order to 116 // correctly report the spending details in the next period 117 Residue types.Currency 118 119 // The following fields were added in v1.6.1 120 // 121 // host balance stores a local version of the balance that resets on 122 // every balance sync we perform with the host. These fields are 123 // necessary to more accurately track the balance drift 124 HostBalance types.Currency 125 HostBalanceNegative types.Currency 126 127 // The following fields were added in v1.6.2 128 // 129 // the following spending details indicate the amount spent on 130 // maintenance functions, namely updating the price table and syncing 131 // the account balance, when they're paid for using the ephemeral 132 // account 133 SpendingAccountBalance types.Currency 134 SpendingUpdatePriceTable types.Currency 135 } 136 137 // accountPersistenceV150 is how the account persistence struct looked 138 // before adding the spending details in v156 139 accountPersistenceV150 struct { 140 AccountID modules.AccountID 141 Balance types.Currency 142 HostKey types.SiaPublicKey 143 SecretKey crypto.SecretKey 144 } 145 ) 146 147 // newAccountManager will initialize the account manager for the renter. 148 func (r *Renter) newAccountManager() error { 149 if r.staticAccountManager != nil { 150 return errors.New("account manager already exists") 151 } 152 153 r.staticAccountManager = &accountManager{ 154 accounts: make(map[string]*account), 155 156 staticRenter: r, 157 } 158 159 return r.staticAccountManager.load() 160 } 161 162 // managedPersist will write the account to the given file at the account's 163 // offset, without syncing the file. 164 func (a *account) managedPersist() error { 165 a.mu.Lock() 166 defer a.mu.Unlock() 167 return a.persist() 168 } 169 170 // persist will write the account to the given file at the account's offset, 171 // without syncing the file. 172 func (a *account) persist() error { 173 accountData := accountPersistence{ 174 AccountID: a.staticID, 175 HostKey: a.staticHostKey, 176 SecretKey: a.staticSecretKey, 177 178 // balance details 179 Balance: a.minExpectedBalance(), 180 BalanceDriftPositive: a.balanceDriftPositive, 181 BalanceDriftNegative: a.balanceDriftNegative, 182 183 // spending details 184 SpendingDownloads: a.spending.downloads, 185 SpendingRegistryReads: a.spending.registryReads, 186 SpendingRegistryWrites: a.spending.registryWrites, 187 SpendingRepairDownloads: a.spending.repairDownloads, 188 SpendingRepairUploads: a.spending.repairUploads, 189 SpendingSnapshotDownloads: a.spending.snapshotDownloads, 190 SpendingSnapshotUploads: a.spending.snapshotUploads, 191 SpendingSubscriptions: a.spending.subscriptions, 192 SpendingUploads: a.spending.uploads, 193 194 // residue 195 Residue: a.residue, 196 197 // host balance 198 // 199 // NOTE: we want to take into account pending withdrawals here, but we 200 // do not want to incorporate the negative balance into the host 201 // balance, as we persist that separately 202 HostBalance: minExpectedBalance(a.hostBalance, types.ZeroCurrency, a.pendingWithdrawals), 203 HostBalanceNegative: a.hostBalanceNegative, 204 205 // maintenance spending details 206 // 207 // NOTE: these fields are added to the bottom here to avoid writing too 208 // much compat code 209 SpendingAccountBalance: a.spending.accountBalance, 210 SpendingUpdatePriceTable: a.spending.updatePriceTable, 211 } 212 213 _, err := a.staticFile.WriteAt(accountData.bytes(), a.staticOffset) 214 return errors.AddContext(err, "unable to write the account to disk") 215 } 216 217 // bytes is a helper method on the persistence object that outputs the bytes to 218 // put on disk, these include the checksum and the marshaled persistence object. 219 func (ap accountPersistence) bytes() []byte { 220 accBytes := encoding.Marshal(ap) 221 accBytesMaxSize := accountSize - crypto.HashSize // leave room for checksum 222 if len(accBytes) > accBytesMaxSize { 223 build.Critical("marshaled object is larger than expected size", len(accBytes)) 224 return nil 225 } 226 227 // Calculate checksum on padded account bytes. Upon load, the padding will 228 // be ignored by the unmarshaling. 229 accBytesPadded := make([]byte, accBytesMaxSize) 230 copy(accBytesPadded, accBytes) 231 checksum := crypto.HashBytes(accBytesPadded) 232 233 // create final byte slice of account size 234 b := make([]byte, accountSize) 235 copy(b[:len(checksum)], checksum[:]) 236 copy(b[len(checksum):], accBytesPadded) 237 return b 238 } 239 240 // loadBytes is a helper method that takes a byte slice, containing a checksum 241 // and the account bytes, and unmarshals them onto the persistence object if the 242 // checksum is valid. 243 func (ap *accountPersistence) loadBytes(b []byte) error { 244 // extract checksum and verify it 245 checksum := b[:crypto.HashSize] 246 accBytes := b[crypto.HashSize:] 247 accHash := crypto.HashBytes(accBytes) 248 if !bytes.Equal(checksum, accHash[:]) { 249 return errInvalidChecksum 250 } 251 252 // unmarshal the account bytes onto the persistence object 253 return errors.AddContext(encoding.Unmarshal(accBytes, ap), "failed to unmarshal account bytes") 254 } 255 256 // EphemeralAccountSpending returns a breakdown of the costs of the account 257 // expenditures per spending category. 258 func (am *accountManager) EphemeralAccountSpending() []skymodules.EphemeralAccountSpending { 259 am.mu.Lock() 260 var accounts []*account 261 for _, account := range am.accounts { 262 accounts = append(accounts, account) 263 } 264 am.mu.Unlock() 265 266 var eass []skymodules.EphemeralAccountSpending 267 for _, account := range accounts { 268 account.mu.Lock() 269 b := account.balance 270 dp := account.balanceDriftPositive 271 dn := account.balanceDriftNegative 272 s := account.spending 273 r := account.residue 274 account.mu.Unlock() 275 276 var eas skymodules.EphemeralAccountSpending 277 eas.HostKey = account.staticHostKey 278 279 eas.AccountBalanceCost = s.accountBalance 280 eas.DownloadsCost = s.downloads 281 eas.RegistryReadsCost = s.registryReads 282 eas.RegistryWritesCost = s.registryWrites 283 eas.RepairDownloadsCost = s.repairDownloads 284 eas.RepairUploadsCost = s.repairUploads 285 eas.SnapshotDownloadsCost = s.snapshotDownloads 286 eas.SnapshotUploadsCost = s.snapshotUploads 287 eas.SubscriptionsCost = s.subscriptions 288 eas.UpdatePriceTableCost = s.updatePriceTable 289 eas.UploadsCost = s.uploads 290 291 eas.Balance = b 292 eas.Residue = r 293 294 var drift *big.Int 295 if dp.Cmp(dn) > 0 { 296 drift = dp.Sub(dn).Big() 297 } else { 298 drift = dn.Sub(dp).Big() 299 drift = drift.Neg(drift) 300 } 301 eas.BalanceDrift = *drift 302 303 eass = append(eass, eas) 304 } 305 return eass 306 } 307 308 // managedOpenAccount returns an account for the given host. If it does not 309 // exist already one is created. 310 func (am *accountManager) managedOpenAccount(hostKey types.SiaPublicKey) (acc *account, err error) { 311 // Check if we already have an account. Due to a race condition around 312 // account creation, we need to check that the account was persisted to disk 313 // before we can start using it, this happens with the 'staticReady' and 314 // 'externActive' variables of the account. See the rest of this functions 315 // implementation to understand how they are used in practice. 316 am.mu.Lock() 317 acc, exists := am.accounts[hostKey.String()] 318 if exists { 319 am.mu.Unlock() 320 <-acc.staticReady 321 if acc.externActive { 322 return acc, nil 323 } 324 return nil, errors.New("account creation failed") 325 } 326 // Open a new account. 327 offset := accountsOffset + len(am.accounts)*accountSize 328 aid, sk := modules.NewAccountID() 329 acc = &account{ 330 staticID: aid, 331 staticHostKey: hostKey, 332 staticSecretKey: sk, 333 334 staticFile: am.staticFile, 335 staticOffset: int64(offset), 336 337 staticAlerter: am.staticRenter.staticAlerter, 338 staticBalanceTarget: am.staticRenter.staticAccountBalanceTarget, 339 staticLog: am.staticRenter.staticLog, 340 staticReady: make(chan struct{}), 341 } 342 am.accounts[hostKey.String()] = acc 343 am.mu.Unlock() 344 // Defer a close on 'staticReady'. By default, 'externActive' is false, so 345 // if there is an error, the account will be marked as unusable. 346 defer close(acc.staticReady) 347 348 // Defer a function to delete the account if the persistence fails. This is 349 // technically a race condition, but the alternative is holding the lock on 350 // the account mangager while doing an fsync, which is not ideal. 351 defer func() { 352 if err != nil { 353 am.mu.Lock() 354 delete(am.accounts, hostKey.String()) 355 am.mu.Unlock() 356 } 357 }() 358 359 // Save the file. After the file gets written to disk, perform a sync 360 // because we want to ensure that the secret key of the account can be 361 // recovered before we start using the account. 362 err = acc.managedPersist() 363 if err != nil { 364 return nil, errors.AddContext(err, "failed to persist account") 365 } 366 err = acc.staticFile.Sync() 367 if err != nil { 368 return nil, errors.AddContext(err, "failed to sync accounts file") 369 } 370 371 // Mark the account as usable so that anyone who tried to open the account 372 // after this function ran will see that the account is persisted correctly. 373 acc.mu.Lock() 374 acc.externActive = true 375 acc.mu.Unlock() 376 return acc, nil 377 } 378 379 // managedSaveAndClose is called on shutdown and ensures the account data is 380 // properly persisted to disk 381 func (am *accountManager) managedSaveAndClose() error { 382 am.mu.Lock() 383 defer am.mu.Unlock() 384 385 // Save the account data to disk. 386 clean := true 387 var persistErrs error 388 for _, account := range am.accounts { 389 err := account.managedPersist() 390 if err != nil { 391 clean = false 392 persistErrs = errors.Compose(persistErrs, err) 393 continue 394 } 395 } 396 // If there was an error saving any of the accounts, the system is not clean 397 // and we do not need to update the metadata for the file. 398 if !clean { 399 return errors.AddContext(persistErrs, "unable to persist all accounts cleanly upon shutdown") 400 } 401 402 // Sync the file before updating the header. We want to make sure that the 403 // accounts have been put into a clean and finalized state before writing an 404 // update to the metadata. 405 err := am.staticFile.Sync() 406 if err != nil { 407 return errors.AddContext(err, "failed to sync accounts file") 408 } 409 410 // update the metadata and mark the file as clean 411 if err = am.updateMetadata(accountsMetadata{ 412 Header: metadataHeader, 413 Version: metadataVersion, 414 Clean: true, 415 }); err != nil { 416 return errors.AddContext(err, "failed to update accounts file metadata") 417 } 418 419 // Close the account file. 420 return am.staticFile.Close() 421 } 422 423 // checkMetadata will load the metadata from the account file and return whether 424 // or not the previous shutdown was clean. If the metadata does not match the 425 // expected metadata, an error will be returned. 426 // 427 // NOTE: If we change the version of the file, this is probably the function 428 // that should handle doing the persist upgrade. Inside of this function there 429 // would be a call to the upgrade function. 430 func (am *accountManager) checkMetadata() (bool, error) { 431 // Read metadata. 432 metadata, err := readAccountsMetadata(am.staticFile) 433 if err != nil { 434 return false, errors.AddContext(err, "failed to read metadata from accounts file") 435 } 436 437 // Validate the metadata. 438 if metadata.Header != metadataHeader { 439 return false, errors.AddContext(errWrongHeader, "failed to verify accounts metadata") 440 } 441 if metadata.Version != metadataVersion { 442 return false, errors.AddContext(errWrongVersion, "failed to verify accounts metadata") 443 } 444 return metadata.Clean, nil 445 } 446 447 // handleInterruptedUpgrade ensures that an interrupted upgrade can be recovered 448 // from. It does so by checking for the existence of a tmp accounts file, if 449 // that file is present we want to handle it occordingly. 450 func (am *accountManager) handleInterruptedUpgrade() error { 451 // convenience variables 452 r := am.staticRenter 453 tmpFilePath := filepath.Join(r.persistDir, accountsTmpFilename) 454 455 // check whether the tmp file exists 456 tmpFileExists, err := fileExists(tmpFilePath) 457 if err != nil { 458 return errors.AddContext(err, "error checking if tmp file exists") 459 } 460 461 // if the tmp file does not exist, we don't have to do anything 462 if !tmpFileExists { 463 return nil 464 } 465 466 // open the tmp file 467 tmpFile, err := r.staticDeps.OpenFile(tmpFilePath, os.O_RDWR, defaultFilePerm) 468 if err != nil { 469 return errors.AddContext(err, "error opening tmp account file") 470 } 471 472 // read the metadata, there can only be two scenarios: 473 // - the tmp file is clean, continue from that file 474 // - the tmp file is dirty, remove it 475 tmpFileMetadata, err := readAccountsMetadata(tmpFile) 476 if err == nil && tmpFileMetadata.Clean { 477 return am.upgradeCopyAccountsFromTmpFile(tmpFile) 478 } 479 480 return errors.Compose(tmpFile.Close(), r.staticDeps.RemoveFile(tmpFilePath)) 481 } 482 483 // managedLoad will pull all of the accounts off of disk and load them into the 484 // account manager. This should complete before the accountManager is made 485 // available to other processes. 486 func (am *accountManager) load() error { 487 // Open the accounts file. 488 clean, err := am.openFile() 489 if err != nil { 490 return errors.AddContext(err, "failed to open accounts file") 491 } 492 493 // Read the raw account data and decode them into accounts. We start at an 494 // offset of 'accountsOffset' because the metadata precedes the accounts 495 // data. 496 for offset := int64(accountsOffset); ; offset += accountSize { 497 // read the account at offset 498 acc, err := am.readAccountAt(offset) 499 if errors.Contains(err, io.EOF) { 500 break 501 } else if err != nil { 502 am.staticRenter.staticLog.Println("ERROR: could not load account", err) 503 continue 504 } 505 506 // reset the account balances after an unclean shutdown 507 if !clean { 508 acc.balance = types.ZeroCurrency 509 } 510 am.accounts[acc.staticHostKey.String()] = acc 511 } 512 513 // Ensure that when the renter is shut down, the save and close function 514 // runs. 515 if am.staticRenter.staticDeps.Disrupt("InterruptAccountSaveOnShutdown") { 516 // Dependency injection to simulate an unclean shutdown. 517 return nil 518 } 519 err = am.staticRenter.tg.AfterStop(am.managedSaveAndClose) 520 if err != nil { 521 return errors.AddContext(err, "unable to schedule a save and close with the thread group") 522 } 523 return nil 524 } 525 526 // openFile will open the file of the account manager and set the account 527 // manager's file variable. 528 // 529 // openFile will return 'true' if the previous shutdown was clean, and 'false' 530 // if the previous shutdown was not clean. 531 func (am *accountManager) openFile() (bool, error) { 532 r := am.staticRenter 533 534 // Sanity check that the file isn't already opened. 535 if am.staticFile != nil { 536 r.staticLog.Critical("double open detected on account manager") 537 return false, errors.New("accounts file already open") 538 } 539 540 // Open the accounts file 541 accountsFile, err := am.openAccountsFile(accountsFilename) 542 if err != nil { 543 return false, errors.AddContext(err, "error opening account file") 544 } 545 am.staticFile = accountsFile 546 547 // Read accounts metadata 548 metadata, err := readAccountsMetadata(am.staticFile) 549 if err != nil { 550 return false, errors.AddContext(err, "error reading account metadata") 551 } 552 553 // Handle a potentially interrupted upgrade 554 err = am.handleInterruptedUpgrade() 555 if err != nil { 556 return false, errors.AddContext(err, "error occurred while trying to recover from an intterupted upgrade") 557 } 558 559 // Check accounts metadata 560 _, err = am.checkMetadata() 561 if err != nil && !errors.Contains(err, errWrongVersion) { 562 return false, errors.AddContext(err, "error reading account metadata") 563 } 564 565 // If the metadata contains a wrong version, run the upgrade code 566 if errors.Contains(err, errWrongVersion) { 567 err = am.upgradeFromV161ToV162() 568 if err != nil && errors.Contains(err, errWrongVersion) { 569 err = am.upgradeFromV156ToV162() 570 if err != nil && errors.Contains(err, errWrongVersion) { 571 err = am.upgradeFromV150ToV162() 572 } 573 } 574 if err != nil { 575 return false, errors.AddContext(err, "error upgrading accounts file") 576 } 577 578 // log the successful upgrade 579 am.staticRenter.staticLog.Println("successfully upgraded accounts file to v161") 580 } 581 582 // Whether this is a new file or an existing file, we need to set the header 583 // on the metadata. When opening an account, the header should represent an 584 // unclean shutdown. This will be flipped to a header that represents a 585 // clean shutdown upon closing. 586 err = am.updateMetadata(accountsMetadata{ 587 Header: metadataHeader, 588 Version: metadataVersion, 589 Clean: false, 590 }) 591 if err != nil { 592 return false, errors.AddContext(err, "unable to update the account metadata") 593 } 594 595 // Sync the metadata to ensure the acounts will load as dirty before any 596 // accounts are created. 597 err = am.staticFile.Sync() 598 if err != nil { 599 return false, errors.AddContext(err, "failed to sync accounts file") 600 } 601 602 return metadata.Clean, nil 603 } 604 605 // openAccountsFile is a helper function that will open an accounts file with 606 // given filename. If the accounts file does not exist prior to calling this 607 // function, it will be created and provided with the metadata header. 608 func (am *accountManager) openAccountsFile(filename string) (modules.File, error) { 609 r := am.staticRenter 610 611 // check whether the file exists 612 accountsFilepath := filepath.Join(r.persistDir, filename) 613 accountsFileExists, err := fileExists(accountsFilepath) 614 if err != nil { 615 return nil, err 616 } 617 618 // open the file and create it if necessary 619 accountsFile, err := r.staticDeps.OpenFile(accountsFilepath, os.O_RDWR|os.O_CREATE, defaultFilePerm) 620 if err != nil { 621 return nil, errors.AddContext(err, "error opening account file") 622 } 623 624 // make sure a newly created accounts file has the metadata header 625 if !accountsFileExists { 626 _, err = accountsFile.WriteAt(encoding.Marshal(accountsMetadata{ 627 Header: metadataHeader, 628 Version: metadataVersion, 629 Clean: false, 630 }), 0) 631 err = errors.Compose(err, accountsFile.Sync()) 632 if err != nil { 633 return accountsFile, errors.AddContext(err, "error writing metadata to accounts file") 634 } 635 } 636 637 return accountsFile, nil 638 } 639 640 // readAccountAt tries to read an account object from the account persist file 641 // at the given offset. 642 func (am *accountManager) readAccountAt(offset int64) (*account, error) { 643 acc, err := readAccountFromFile(am.staticFile, offset) 644 if err != nil { 645 return nil, err 646 } 647 648 acc.staticAlerter = am.staticRenter.staticAlerter 649 acc.staticBalanceTarget = am.staticRenter.staticAccountBalanceTarget 650 acc.staticLog = am.staticRenter.staticLog 651 652 close(acc.staticReady) 653 return acc, nil 654 } 655 656 // upgradeFromV161ToV162 is compat code that upgrades the accounts file from 657 // v161 to v162. This version introduced two new maintenance spending fields. 658 func (am *accountManager) upgradeFromV161ToV162() error { 659 // open the accounts file 660 accountsFile, err := am.openAccountsFile(accountsFilename) 661 if err != nil { 662 return errors.AddContext(err, "error opening account file") 663 } 664 665 // read the metadata 666 metadata, err := readAccountsMetadata(accountsFile) 667 if err != nil { 668 return errors.AddContext(err, "failed to read accounts metadata") 669 } 670 671 // error out if this is not v161 672 if metadata.Version != metadataVersionV161 { 673 return errWrongVersion 674 } 675 676 // that's all it takes, the upgrade code will overwrite the header with the 677 // proper version and accounts will be loaded taking the original 'clean' 678 // flag into account 679 return nil 680 } 681 682 // upgradeFromV156ToV162 is compat code that upgrades the accounts file from 683 // v156 to v162. This version introduced a bug fix for the way we handle refunds 684 // and track drift, which require a reset of the account balances. 685 func (am *accountManager) upgradeFromV156ToV162() error { 686 // convenience variables 687 r := am.staticRenter 688 689 // open the accounts file 690 accountsFile, err := am.openAccountsFile(accountsFilename) 691 if err != nil { 692 return errors.AddContext(err, "error opening account file") 693 } 694 695 // read the metadata 696 metadata, err := readAccountsMetadata(accountsFile) 697 if err != nil { 698 return errors.AddContext(err, "failed to read accounts metadata") 699 } 700 701 // error out if this is not v156 702 if metadata.Version != persist.MetadataVersionv156 { 703 return errWrongVersion 704 } 705 706 // open a tmp accounts file 707 tmpFile, err := am.openAccountsFile(accountsTmpFilename) 708 if err != nil { 709 return errors.AddContext(err, "failed to open tmp accounts file") 710 } 711 712 // read the accounts from the accounts file, but link them to the tmp file, 713 // when calling persist on the account it will write the account into the 714 // tmp file 715 accounts := compatV156ReadAccounts(r.staticLog, am.staticFile, tmpFile) 716 for _, acc := range accounts { 717 // the v156 -> v161 requires a balance and drift reset 718 // the v161 -> v162 only entails a version bump 719 acc.balance = types.ZeroCurrency 720 acc.balanceDriftPositive = types.ZeroCurrency 721 acc.balanceDriftNegative = types.ZeroCurrency 722 if err := acc.managedPersist(); err != nil { 723 r.staticLog.Println("failed to upgrade account from v156 to v161", err) 724 } 725 } 726 727 // sync the tmp file 728 err = tmpFile.Sync() 729 if err != nil { 730 return errors.AddContext(err, "failed to sync tmp file") 731 } 732 733 // update the header and mark it clean 734 _, err = tmpFile.WriteAt(encoding.Marshal(accountsMetadata{ 735 Header: metadataHeader, 736 Version: metadataVersion, 737 Clean: true, 738 }), 0) 739 if err != nil { 740 return errors.AddContext(err, "failed to write header to tmp file") 741 } 742 743 // sync the tmp file, this step is very important because if it completes 744 // successfully, and the upgrade fails over this point, the tmp file will be 745 // used to recover from an interrupted upgrade. 746 err = tmpFile.Sync() 747 if err != nil { 748 return errors.AddContext(err, "failed to sync tmp file") 749 } 750 751 // copy the accounts from the tmp file to the accounts file, this is 752 // extracted into a separate method as the recovery flow might have to pick 753 // up from where we left off in case of failure during an initial attempt 754 return am.upgradeCopyAccountsFromTmpFile(tmpFile) 755 } 756 757 // upgradeFromV150ToV162 is compat code that upgrades the accounts file from 758 // v150 to v162. The new accounts take up more space on disk, so we have to read 759 // all of them, assign them new offets and rewrite them to the accounts file. 760 func (am *accountManager) upgradeFromV150ToV162() error { 761 // convenience variables 762 r := am.staticRenter 763 764 // open a tmp accounts file 765 tmpFile, err := am.openAccountsFile(accountsTmpFilename) 766 if err != nil { 767 return errors.AddContext(err, "failed to open tmp accounts file") 768 } 769 770 // read the accounts from the accounts file, but link them to the tmp file, 771 // when calling persist on the account it will write the account into the 772 // tmp file 773 accounts := compatV150ReadAccounts(r.staticLog, am.staticFile, tmpFile) 774 for _, acc := range accounts { 775 if err := acc.managedPersist(); err != nil { 776 r.staticLog.Println("failed to upgrade account from v150 to v161", err) 777 } 778 } 779 780 // sync the tmp file 781 err = tmpFile.Sync() 782 if err != nil { 783 return errors.AddContext(err, "failed to sync tmp file") 784 } 785 786 // update the header and mark it clean 787 _, err = tmpFile.WriteAt(encoding.Marshal(accountsMetadata{ 788 Header: metadataHeader, 789 Version: metadataVersion, 790 Clean: true, 791 }), 0) 792 if err != nil { 793 return errors.AddContext(err, "failed to write header to tmp file") 794 } 795 796 // sync the tmp file, this step is very important because if it completes 797 // successfully, and the upgrade fails over this point, the tmp file will be 798 // used to recover from an interrupted upgrade. 799 err = tmpFile.Sync() 800 if err != nil { 801 return errors.AddContext(err, "failed to sync tmp file") 802 } 803 804 // copy the accounts from the tmp file to the accounts file, this is 805 // extracted into a separate method as the recovery flow might have to pick 806 // up from where we left off in case of failure during an initial attempt 807 return am.upgradeCopyAccountsFromTmpFile(tmpFile) 808 } 809 810 // upgradeCopyAccountsFromTmpFile will copy the contents of the tmp file into 811 // the accounts file. This is a separate method as this function is called 812 // during the happy flow, but it is also potentially the steps required when 813 // trying to recover from a failed initial update attempt. 814 func (am *accountManager) upgradeCopyAccountsFromTmpFile(tmpFile modules.File) (err error) { 815 // convenience variables 816 r := am.staticRenter 817 tmpFilePath := filepath.Join(r.persistDir, accountsTmpFilename) 818 819 // copy the tmp file to the accounts file 820 _, err = io.Copy(am.staticFile, tmpFile) 821 if err != nil { 822 return errors.AddContext(err, "failed to copy the temporary accounts file to the actual accounts file location") 823 } 824 825 // sync the accounts file 826 err = am.staticFile.Sync() 827 if err != nil { 828 return errors.AddContext(err, "failed to sync accounts file") 829 } 830 831 // seek to the beginning of the file 832 _, err = am.staticFile.Seek(0, io.SeekStart) 833 if err != nil { 834 return errors.AddContext(err, "failed to seek to the beginning of the accounts file") 835 } 836 837 // delete the tmp file 838 return errors.AddContext(errors.Compose(tmpFile.Close(), r.staticDeps.RemoveFile(tmpFilePath)), "failed to delete accounts file") 839 } 840 841 // updateMetadata writes the given metadata to the accounts file. 842 func (am *accountManager) updateMetadata(meta accountsMetadata) error { 843 _, err := am.staticFile.WriteAt(encoding.Marshal(meta), 0) 844 return err 845 } 846 847 // compatV150ReadAccounts is a helper function that reads the accounts from the 848 // accounts file assuming they are persisted using the v150 persistence object 849 // and parameters. Extracted to keep the compat code clean. 850 func compatV150ReadAccounts(log *skydPersist.Logger, accountsFile modules.File, tmpFile modules.File) []*account { 851 // the offset needs to be the new accountsOffset 852 newOffset := int64(accountsOffset) 853 854 // collect all accounts from the current accounts file 855 var accounts []*account 856 for offset := int64(accountSizeV150); ; offset += accountSizeV150 { 857 // read account bytes 858 accountBytes := make([]byte, accountSizeV150) 859 _, err := accountsFile.ReadAt(accountBytes, offset) 860 if errors.Contains(err, io.EOF) { 861 break 862 } else if err != nil { 863 log.Println("ERROR: could not read account data", err) 864 continue 865 } 866 867 // load the account bytes onto the a persistence object 868 var accountDataV150 accountPersistenceV150 869 err = encoding.Unmarshal(accountBytes[crypto.HashSize:], &accountDataV150) 870 if err != nil { 871 log.Println("ERROR: could not load account bytes", err) 872 continue 873 } 874 875 accounts = append(accounts, &account{ 876 staticID: accountDataV150.AccountID, 877 staticHostKey: accountDataV150.HostKey, 878 staticSecretKey: accountDataV150.SecretKey, 879 880 balance: accountDataV150.Balance, 881 882 staticOffset: newOffset, 883 staticFile: tmpFile, 884 }) 885 newOffset += accountSize 886 } 887 888 return accounts 889 } 890 891 // compatV156ReadAccounts is a helper function that reads the accounts from the 892 // accounts file assuming they are persisted using the v156 persistence object 893 // and parameters. Extracted to keep the compat code clean. 894 func compatV156ReadAccounts(log *skydPersist.Logger, accountsFile modules.File, tmpFile modules.File) []*account { 895 // collect all accounts from the current accounts file 896 var accounts []*account 897 for offset := int64(accountSize); ; offset += accountSize { 898 // read the account from the file 899 acc, err := readAccountFromFile(accountsFile, offset) 900 if errors.Contains(err, io.EOF) { 901 break 902 } else if err != nil { 903 log.Println("ERROR: could not read account data", err) 904 continue 905 } 906 907 // add it to the accounts 908 accounts = append(accounts, acc) 909 } 910 911 return accounts 912 } 913 914 // fileExists is a small helper function that checks whether a file at given 915 // path exists, it abstracts checking whether the error from the stat is an 916 // `IsNotExists` error or not. 917 func fileExists(path string) (bool, error) { 918 _, statErr := os.Stat(path) 919 if statErr == nil { 920 return true, nil 921 } 922 if os.IsNotExist(statErr) { 923 return false, nil 924 } 925 return false, errors.AddContext(statErr, "error calling stat on file") 926 } 927 928 // readAccountsMetadata is a small helper function that tries to read the 929 // metadata object from the given file. 930 func readAccountsMetadata(file modules.File) (*accountsMetadata, error) { 931 // Seek to the beginning of the file 932 _, err := file.Seek(0, io.SeekStart) 933 if err != nil { 934 return nil, errors.AddContext(err, "failed to seek to the beginning of the file") 935 } 936 937 // Read metadata. 938 buffer := make([]byte, metadataSize) 939 _, err = io.ReadFull(file, buffer) 940 if err != nil { 941 return nil, errors.AddContext(err, "failed to read metadata from file") 942 } 943 944 // Seek to the beginning of the file 945 _, err = file.Seek(0, io.SeekStart) 946 if err != nil { 947 return nil, errors.AddContext(err, "failed to seek to the beginning of the file") 948 } 949 950 // Decode metadata 951 var metadata accountsMetadata 952 err = encoding.Unmarshal(buffer, &metadata) 953 if err != nil { 954 return nil, errors.AddContext(err, "failed to decode metadata") 955 } 956 957 return &metadata, nil 958 } 959 960 // readAccountFromFile is a helper function that reads an account from the given 961 // file at given offset. 962 func readAccountFromFile(file modules.File, offset int64) (*account, error) { 963 // read account bytes 964 accountBytes := make([]byte, accountSize) 965 _, err := file.ReadAt(accountBytes, offset) 966 if err != nil { 967 return nil, errors.AddContext(err, "failed to read account bytes") 968 } 969 970 // load the account bytes onto the a persistence object 971 var accountData accountPersistence 972 err = accountData.loadBytes(accountBytes) 973 if err != nil { 974 return nil, errors.AddContext(err, "failed to load account bytes") 975 } 976 977 return &account{ 978 staticID: accountData.AccountID, 979 staticHostKey: accountData.HostKey, 980 staticSecretKey: accountData.SecretKey, 981 982 // balance details 983 balance: accountData.Balance, 984 balanceDriftPositive: accountData.BalanceDriftPositive, 985 balanceDriftNegative: accountData.BalanceDriftNegative, 986 987 // spending details 988 spending: spendingDetails{ 989 downloads: accountData.SpendingDownloads, 990 registryReads: accountData.SpendingRegistryReads, 991 registryWrites: accountData.SpendingRegistryWrites, 992 repairDownloads: accountData.SpendingRepairDownloads, 993 repairUploads: accountData.SpendingRepairUploads, 994 snapshotDownloads: accountData.SpendingSnapshotDownloads, 995 snapshotUploads: accountData.SpendingSnapshotUploads, 996 subscriptions: accountData.SpendingSubscriptions, 997 uploads: accountData.SpendingUploads, 998 }, 999 1000 // residue 1001 residue: accountData.Residue, 1002 1003 // host balance 1004 hostBalance: accountData.HostBalance, 1005 hostBalanceNegative: accountData.HostBalanceNegative, 1006 1007 staticReady: make(chan struct{}), 1008 externActive: true, 1009 1010 staticOffset: offset, 1011 staticFile: file, 1012 }, nil 1013 }