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  }