gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/proto/contractset.go (about)

     1  package proto
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"sync"
    10  
    11  	"gitlab.com/NebulousLabs/encoding"
    12  	"gitlab.com/NebulousLabs/errors"
    13  	"gitlab.com/NebulousLabs/ratelimit"
    14  	"gitlab.com/NebulousLabs/writeaheadlog"
    15  
    16  	"gitlab.com/SkynetLabs/skyd/build"
    17  	"gitlab.com/SkynetLabs/skyd/skymodules"
    18  	"go.sia.tech/siad/crypto"
    19  	"go.sia.tech/siad/modules"
    20  	"go.sia.tech/siad/types"
    21  )
    22  
    23  // A ContractSet provides safe concurrent access to a set of contracts. Its
    24  // purpose is to serialize modifications to individual contracts, as well as
    25  // to provide operations on the set as a whole.
    26  type ContractSet struct {
    27  	contracts  map[types.FileContractID]*SafeContract
    28  	pubKeys    map[string]types.FileContractID
    29  	staticDeps modules.Dependencies
    30  	staticDir  string
    31  	mu         sync.Mutex
    32  	staticRL   *ratelimit.RateLimit
    33  	staticWal  *writeaheadlog.WAL
    34  }
    35  
    36  // Acquire looks up the contract for the specified host key and locks it before
    37  // returning it. If the contract is not present in the set, Acquire returns
    38  // false and a zero-valued RenterContract.
    39  func (cs *ContractSet) Acquire(id types.FileContractID) (*SafeContract, bool) {
    40  	cs.mu.Lock()
    41  	safeContract, ok := cs.contracts[id]
    42  	cs.mu.Unlock()
    43  	if !ok {
    44  		return nil, false
    45  	}
    46  	safeContract.revisionMu.Lock()
    47  	// We need to check if the contract is still in the map or if it has been
    48  	// deleted in the meantime.
    49  	cs.mu.Lock()
    50  	_, ok = cs.contracts[id]
    51  	cs.mu.Unlock()
    52  	if !ok {
    53  		safeContract.revisionMu.Unlock()
    54  		return nil, false
    55  	}
    56  	return safeContract, true
    57  }
    58  
    59  // Delete removes a contract from the set. The contract must have been
    60  // previously acquired by Acquire. If the contract is not present in the set,
    61  // Delete is a no-op.
    62  func (cs *ContractSet) Delete(c *SafeContract) {
    63  	cs.mu.Lock()
    64  	_, ok := cs.contracts[c.header.ID()]
    65  	if !ok {
    66  		cs.mu.Unlock()
    67  		build.Critical("Delete called on already deleted contract")
    68  		return
    69  	}
    70  	delete(cs.contracts, c.header.ID())
    71  	delete(cs.pubKeys, c.header.HostPublicKey().String())
    72  	unappliedTxns := c.unappliedTxns
    73  	cs.mu.Unlock()
    74  	c.revisionMu.Unlock()
    75  	// delete contract file
    76  	headerPath := filepath.Join(cs.staticDir, c.header.ID().String()+contractHeaderExtension)
    77  	rootsPath := filepath.Join(cs.staticDir, c.header.ID().String()+contractRootsExtension)
    78  	// close header and root files.
    79  	err := errors.Compose(c.staticHeaderFile.Close(), c.merkleRoots.rootsFile.Close())
    80  	// remove the files.
    81  	err = errors.Compose(err, os.Remove(headerPath), os.Remove(rootsPath))
    82  	if err != nil {
    83  		build.Critical("Failed to delete SafeContract from disk:", err)
    84  	}
    85  	for _, txn := range unappliedTxns {
    86  		err = txn.SignalUpdatesApplied()
    87  		if err != nil {
    88  			build.Critical("Delete: failed to signal applied updates for contract", c.header.ID())
    89  		}
    90  	}
    91  }
    92  
    93  // IDs returns the fcid of each contract with in the set. The contracts are not
    94  // locked.
    95  func (cs *ContractSet) IDs() []types.FileContractID {
    96  	cs.mu.Lock()
    97  	defer cs.mu.Unlock()
    98  	pks := make([]types.FileContractID, 0, len(cs.contracts))
    99  	for fcid := range cs.contracts {
   100  		pks = append(pks, fcid)
   101  	}
   102  	return pks
   103  }
   104  
   105  // InsertContract inserts an existing contract into the set.
   106  func (cs *ContractSet) InsertContract(rc skymodules.RecoverableContract, revTxn types.Transaction, roots []crypto.Hash, sk crypto.SecretKey) (skymodules.RenterContract, error) {
   107  	// Estimate the totalCost.
   108  	// NOTE: The actual totalCost is the funding amount. Which means
   109  	// renterPayout + txnFee + basePrice + contractPrice.
   110  	// Since we don't know the basePrice and contractPrice, we don't add them.
   111  	var totalCost types.Currency
   112  	totalCost = totalCost.Add(rc.FileContract.ValidRenterPayout())
   113  	totalCost = totalCost.Add(rc.TxnFee)
   114  	return cs.managedInsertContract(contractHeader{
   115  		Transaction: revTxn,
   116  		SecretKey:   sk,
   117  		StartHeight: rc.StartHeight,
   118  		TotalCost:   totalCost,
   119  		TxnFee:      rc.TxnFee,
   120  		SiafundFee:  types.Tax(rc.StartHeight, rc.Payout),
   121  	}, roots)
   122  }
   123  
   124  // Len returns the number of contracts in the set.
   125  func (cs *ContractSet) Len() int {
   126  	cs.mu.Lock()
   127  	defer cs.mu.Unlock()
   128  	return len(cs.contracts)
   129  }
   130  
   131  // Return returns a locked contract to the set and unlocks it. The contract
   132  // must have been previously acquired by Acquire. If the contract is not
   133  // present in the set, Return panics.
   134  func (cs *ContractSet) Return(c *SafeContract) {
   135  	cs.mu.Lock()
   136  	_, ok := cs.contracts[c.header.ID()]
   137  	if !ok {
   138  		cs.mu.Unlock()
   139  		build.Critical("no contract with that key")
   140  	}
   141  	cs.mu.Unlock()
   142  	c.revisionMu.Unlock()
   143  }
   144  
   145  // View returns a copy of the contract with the specified host key. The contract
   146  // is not locked. Certain fields, including the MerkleRoots, are set to nil for
   147  // safety reasons. If the contract is not present in the set, View returns false
   148  // and a zero-valued RenterContract.
   149  func (cs *ContractSet) View(id types.FileContractID) (skymodules.RenterContract, bool) {
   150  	cs.mu.Lock()
   151  	defer cs.mu.Unlock()
   152  	safeContract, ok := cs.contracts[id]
   153  	if !ok {
   154  		return skymodules.RenterContract{}, false
   155  	}
   156  	return safeContract.Metadata(), true
   157  }
   158  
   159  // PublicKey returns the public key capable of verifying the renter's signature
   160  // on a contract.
   161  func (cs *ContractSet) PublicKey(id types.FileContractID) (crypto.PublicKey, bool) {
   162  	cs.mu.Lock()
   163  	safeContract, ok := cs.contracts[id]
   164  	cs.mu.Unlock()
   165  	if !ok {
   166  		return crypto.PublicKey{}, false
   167  	}
   168  	return safeContract.PublicKey(), true
   169  }
   170  
   171  // ViewAll returns the metadata of each contract in the set. The contracts are
   172  // not locked.
   173  func (cs *ContractSet) ViewAll() []skymodules.RenterContract {
   174  	cs.mu.Lock()
   175  	defer cs.mu.Unlock()
   176  	contracts := make([]skymodules.RenterContract, 0, len(cs.contracts))
   177  	for _, safeContract := range cs.contracts {
   178  		contracts = append(contracts, safeContract.Metadata())
   179  	}
   180  	return contracts
   181  }
   182  
   183  // Close closes all contracts in a contract set, this means rendering it unusable for I/O
   184  func (cs *ContractSet) Close() error {
   185  	cs.mu.Lock()
   186  	defer cs.mu.Unlock()
   187  	var err error
   188  	for _, c := range cs.contracts {
   189  		err = errors.Compose(err, c.staticHeaderFile.Close())
   190  		err = errors.Compose(err, c.merkleRoots.rootsFile.Close())
   191  	}
   192  	_, errWal := cs.staticWal.CloseIncomplete()
   193  	return errors.Compose(err, errWal)
   194  }
   195  
   196  // NewContractSet returns a ContractSet storing its contracts in the specified
   197  // dir.
   198  func NewContractSet(dir string, rl *ratelimit.RateLimit, deps modules.Dependencies) (*ContractSet, error) {
   199  	if err := os.MkdirAll(dir, 0700); err != nil {
   200  		return nil, err
   201  	}
   202  	d, err := os.Open(dir)
   203  	if err != nil {
   204  		return nil, err
   205  	} else if stat, err := d.Stat(); err != nil {
   206  		return nil, err
   207  	} else if !stat.IsDir() {
   208  		return nil, errors.New("not a directory")
   209  	}
   210  	if err := d.Close(); err != nil {
   211  		return nil, err
   212  	}
   213  
   214  	// Load the WAL. Any recovered updates will be applied after loading
   215  	// contracts.
   216  	//
   217  	// COMPATv1.3.1RC2 Rename old wals to have the 'wal' extension if new file
   218  	// doesn't exist.
   219  	if err := v131RC2RenameWAL(dir); err != nil {
   220  		return nil, err
   221  	}
   222  	walTxns, wal, err := writeaheadlog.New(filepath.Join(dir, "contractset.wal"))
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  
   227  	cs := &ContractSet{
   228  		contracts: make(map[types.FileContractID]*SafeContract),
   229  		pubKeys:   make(map[string]types.FileContractID),
   230  
   231  		staticDeps: deps,
   232  		staticDir:  dir,
   233  		staticRL:   rl,
   234  		staticWal:  wal,
   235  	}
   236  	// Set the initial rate limit to 'unlimited' bandwidth with 4kib packets.
   237  	cs.staticRL = ratelimit.NewRateLimit(0, 0, 0)
   238  
   239  	// Some vars for unmarshaling. These are pulled out of the loop to
   240  	// prevent rapid allocations.
   241  	var ush updateSetHeader
   242  	var usr updateSetRoot
   243  
   244  	// Before loading the contract files apply the updates which were meant to
   245  	// create new contracts and filter them out.
   246  	unappliedWalTxns := make(map[types.FileContractID][]*unappliedWalTxn)
   247  	for _, txn := range walTxns {
   248  		if len(txn.Updates) == 0 {
   249  			build.Critical("empty txn found")
   250  			continue // no updates
   251  		}
   252  		switch update := txn.Updates[0]; update.Name {
   253  		case updateNameInsertContract:
   254  			if len(txn.Updates) != 1 {
   255  				if !deps.Disrupt("IgnoreInvalidUpdate") {
   256  					build.Critical("insert contract txns should only have 1 update")
   257  				}
   258  				err = txn.SignalUpdatesApplied()
   259  				if err != nil {
   260  					return nil, errors.AddContext(err, "failed to apply unknown update")
   261  				}
   262  				continue
   263  			}
   264  			// Apply unfinished insert contract updates.
   265  			_, err := cs.managedApplyInsertContractUpdate(txn.Updates[0])
   266  			if err != nil {
   267  				return nil, errors.AddContext(err, "failed to apply insertContractUpdate on startup")
   268  			}
   269  			err = txn.SignalUpdatesApplied()
   270  			if err != nil {
   271  				return nil, errors.AddContext(err, "failed to apply insertContractUpdate on startup")
   272  			}
   273  		case updateNameSetHeader:
   274  			// Unfinished set header updates are collected.
   275  			if err := unmarshalHeader(update.Instructions, &ush); err != nil {
   276  				return nil, errors.AddContext(err, "unable to unmarshal the contract header during wal txn recovery")
   277  			}
   278  			unappliedWalTxns[ush.ID] = append(unappliedWalTxns[ush.ID], newUnappliedWalTxn(txn))
   279  		case updateNameSetRoot:
   280  			// Unfinished set root updates are collected.
   281  			if err := encoding.Unmarshal(update.Instructions, &usr); err != nil {
   282  				return nil, errors.AddContext(err, "unable to unmarshal the update root set during wal txn recovery")
   283  			}
   284  			unappliedWalTxns[usr.ID] = append(unappliedWalTxns[usr.ID], newUnappliedWalTxn(txn))
   285  		default:
   286  			// Unknown updates are applied.
   287  			if !deps.Disrupt("IgnoreInvalidUpdate") {
   288  				build.Critical("unknown update", update.Name)
   289  			}
   290  			err = txn.SignalUpdatesApplied()
   291  			if err != nil {
   292  				return nil, errors.AddContext(err, "failed to apply unknown update")
   293  			}
   294  		}
   295  	}
   296  
   297  	// Check for legacy contracts and split them up.
   298  	if err := cs.managedV146SplitContractHeaderAndRoots(dir); err != nil {
   299  		return nil, err
   300  	}
   301  
   302  	// Load the contract files.
   303  	fis, err := ioutil.ReadDir(dir)
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  	for _, fi := range fis {
   308  		filename := fi.Name()
   309  		if filepath.Ext(filename) != contractHeaderExtension {
   310  			continue
   311  		}
   312  		nameNoExt := strings.TrimSuffix(filename, contractHeaderExtension)
   313  		headerPath := filepath.Join(dir, filename)
   314  		rootsPath := filepath.Join(dir, nameNoExt+contractRootsExtension)
   315  		refCounterPath := filepath.Join(dir, nameNoExt+refCounterExtension)
   316  
   317  		if err := cs.loadSafeContract(headerPath, rootsPath, refCounterPath, unappliedWalTxns); err != nil {
   318  			extErr := fmt.Errorf("failed to load safecontract for header %v", headerPath)
   319  			return nil, errors.Compose(extErr, err)
   320  		}
   321  	}
   322  
   323  	// Apply all the txns we don't have contracts for.
   324  	for fcid, txns := range unappliedWalTxns {
   325  		_, exists := cs.contracts[fcid]
   326  		if exists {
   327  			continue
   328  		}
   329  		if build.Release == "testing" {
   330  			build.Critical("regular testing should never leave txns to unknown contracts", fcid)
   331  		}
   332  		for _, txn := range txns {
   333  			err = txn.SignalUpdatesApplied()
   334  			if err != nil {
   335  				return nil, errors.AddContext(err, "failed to apply unused wal txn")
   336  			}
   337  		}
   338  	}
   339  	return cs, nil
   340  }
   341  
   342  // v131RC2RenameWAL renames an existing old wal file from contractset.log to
   343  // contractset.wal
   344  func v131RC2RenameWAL(dir string) error {
   345  	oldPath := filepath.Join(dir, "contractset.log")
   346  	newPath := filepath.Join(dir, "contractset.wal")
   347  	_, errOld := os.Stat(oldPath)
   348  	_, errNew := os.Stat(newPath)
   349  	if !os.IsNotExist(errOld) && os.IsNotExist(errNew) {
   350  		return build.ExtendErr("failed to rename contractset.log to contractset.wal",
   351  			os.Rename(oldPath, newPath))
   352  	}
   353  	return nil
   354  }
   355  
   356  // managedV146SplitContractHeaderAndRoots goes through all the legacy contracts
   357  // in a directory and splits the file up into a header and roots file.
   358  func (cs *ContractSet) managedV146SplitContractHeaderAndRoots(dir string) error {
   359  	// Load the contract files.
   360  	fis, err := ioutil.ReadDir(dir)
   361  	if err != nil {
   362  		return err
   363  	}
   364  
   365  	oldHeaderSize := 4088 // declared here to avoid cluttering of non-legacy codebase
   366  	for _, fi := range fis {
   367  		filename := fi.Name()
   368  		if filepath.Ext(filename) != v146ContractExtension {
   369  			continue
   370  		}
   371  		path := filepath.Join(cs.staticDir, filename)
   372  		f, err := os.Open(path)
   373  		if err != nil {
   374  			return err
   375  		}
   376  		rootsSection := newFileSection(f, int64(oldHeaderSize), -1)
   377  
   378  		// Load header.
   379  		header, err := loadSafeContractHeader(f, oldHeaderSize*decodeMaxSizeMultiplier)
   380  		if err != nil {
   381  			return errors.Compose(err, f.Close())
   382  		}
   383  		// Load roots.
   384  		roots, unappliedTxns, err := loadExistingMerkleRootsFromSection(rootsSection)
   385  		if err != nil {
   386  			return errors.Compose(err, f.Close())
   387  		}
   388  		if unappliedTxns {
   389  			build.Critical("can't upgrade contractset after an unclean shutdown, please downgrade Sia, start it, stop it cleanly and then try to upgrade again")
   390  			return errors.Compose(errors.New("upgrade failed due to unclean shutdown"), f.Close())
   391  		}
   392  		merkleRoots, err := roots.merkleRoots()
   393  		if err != nil {
   394  			return errors.Compose(err, f.Close())
   395  		}
   396  		// Insert contract into the set.
   397  		_, err = cs.managedInsertContract(header, merkleRoots)
   398  		if err != nil {
   399  			return errors.Compose(err, f.Close())
   400  		}
   401  		// Close the file.
   402  		err = f.Close()
   403  		if err != nil {
   404  			return err
   405  		}
   406  		// Delete the file.
   407  		err = os.Remove(path)
   408  		if err != nil {
   409  			return err
   410  		}
   411  	}
   412  	// Delete the contract from memory again. We only needed to split them up on
   413  	// disk. They will be correctly loaded with the non-legacy contracts during
   414  	// the regular startup.
   415  	cs.mu.Lock()
   416  	cs.contracts = make(map[types.FileContractID]*SafeContract)
   417  	cs.mu.Unlock()
   418  	return nil
   419  }