gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/contractor/recovery.go (about)

     1  package contractor
     2  
     3  import (
     4  	"sync"
     5  	"sync/atomic"
     6  
     7  	"gitlab.com/NebulousLabs/errors"
     8  	"gitlab.com/NebulousLabs/fastrand"
     9  
    10  	"gitlab.com/SkynetLabs/skyd/skymodules"
    11  	"go.sia.tech/siad/crypto"
    12  	"go.sia.tech/siad/modules"
    13  	"go.sia.tech/siad/types"
    14  )
    15  
    16  // TODO If we already have an active contract with a host for
    17  // which we also have a recoverable contract, we might want to
    18  // handle that somehow. For now we probably want to ignore a
    19  // contract if we already have an active contract with the same
    20  // host but there could still be files which are only
    21  // accessible using one contract and not the other. We might
    22  // need to somehow merge them or download all the sectors from
    23  // the old one and upload them to the newer contract.  For now
    24  // we ignore that contract and don't delete it. We might want
    25  // to recover it later.
    26  
    27  // recoveryScanner is a scanner that subscribes to the consensus set from the
    28  // beginning and searches the blockchain for recoverable contracts. Potential
    29  // contracts will be added to the contractor which will then periodically try
    30  // to recover them.
    31  type recoveryScanner struct {
    32  	c  *Contractor
    33  	rs skymodules.RenterSeed
    34  }
    35  
    36  // newRecoveryScanner creates a new scanner from a seed.
    37  func newRecoveryScanner(c *Contractor, rs skymodules.RenterSeed) *recoveryScanner {
    38  	return &recoveryScanner{
    39  		c:  c,
    40  		rs: rs,
    41  	}
    42  }
    43  
    44  // threadedScan subscribes the scanner to cs and scans the blockchain for
    45  // filecontracts belonging to the wallet's seed. Once done, all recoverable
    46  // contracts should be known to the contractor after which it will periodically
    47  // try to recover them.
    48  func (rs *recoveryScanner) threadedScan(cs modules.ConsensusSet, scanStart modules.ConsensusChangeID, cancel <-chan struct{}) error {
    49  	if err := rs.c.staticTG.Add(); err != nil {
    50  		return err
    51  	}
    52  	defer rs.c.staticTG.Done()
    53  	// Check that the scanStart matches the recently missed change id.
    54  	rs.c.mu.RLock()
    55  	if scanStart != rs.c.recentRecoveryChange && scanStart != modules.ConsensusChangeBeginning {
    56  		rs.c.mu.RUnlock()
    57  		return errors.New("scanStart doesn't match recentRecoveryChange")
    58  	}
    59  	rs.c.mu.RUnlock()
    60  	// Subscribe to the consensus set from scanStart.
    61  	err := cs.ConsensusSetSubscribe(rs, scanStart, cancel)
    62  	if err != nil {
    63  		return err
    64  	}
    65  	// Unsubscribe once done.
    66  	cs.Unsubscribe(rs)
    67  	// If cancel is closed we need to assume that the scan didn't finish. Just to
    68  	// be safe we reset it to scanStart.
    69  	select {
    70  	case <-cancel:
    71  		rs.c.mu.Lock()
    72  		rs.c.recentRecoveryChange = scanStart
    73  		rs.c.mu.Unlock()
    74  	default:
    75  	}
    76  	return nil
    77  }
    78  
    79  // ProcessConsensusChange scans the blockchain for information relevant to the
    80  // recoveryScanner.
    81  func (rs *recoveryScanner) ProcessConsensusChange(cc modules.ConsensusChange) {
    82  	for _, block := range cc.AppliedBlocks {
    83  		// Find lost contracts for recovery.
    84  		rs.c.mu.Lock()
    85  		rs.c.findRecoverableContracts(rs.rs, block)
    86  		rs.c.mu.Unlock()
    87  		atomic.AddInt64(&rs.c.atomicRecoveryScanHeight, 1)
    88  	}
    89  	for range cc.RevertedBlocks {
    90  		atomic.AddInt64(&rs.c.atomicRecoveryScanHeight, -1)
    91  	}
    92  	// Update the recentRecoveryChange
    93  	rs.c.mu.Lock()
    94  	rs.c.recentRecoveryChange = cc.ID
    95  	rs.c.mu.Unlock()
    96  }
    97  
    98  // findRecoverableContracts scans the block for contracts that could
    99  // potentially be recovered. We are not going to recover them right away though
   100  // since many of them could already be expired. Recovery happens periodically
   101  // in threadedContractMaintenance.
   102  func (c *Contractor) findRecoverableContracts(renterSeed skymodules.RenterSeed, b types.Block) {
   103  	for _, txn := range b.Transactions {
   104  		// Check if the arbitrary data starts with the correct prefix.
   105  		csi, encryptedHostKey, hasIdentifier := hasFCIdentifier(txn)
   106  		if !hasIdentifier {
   107  			continue
   108  		}
   109  		// Get the total txnFees of the transaction.
   110  		var txnFee types.Currency
   111  		for _, mf := range txn.MinerFees {
   112  			txnFee = txnFee.Add(mf)
   113  		}
   114  		// Check if any contract should be recovered.
   115  		for i, fc := range txn.FileContracts {
   116  			// Create the EphemeralRenterSeed for this contract and wipe it
   117  			// afterwards.
   118  			rs := renterSeed.EphemeralRenterSeed(fc.WindowStart)
   119  			defer fastrand.Read(rs[:])
   120  			// Validate the identifier.
   121  			hostKey, valid, err := csi.IsValid(rs, txn, encryptedHostKey)
   122  			if err != nil && !errors.Contains(err, skymodules.ErrCSIDoesNotMatchSeed) {
   123  				c.staticLog.Println("WARN: error validating the identifier:", err)
   124  				continue
   125  			}
   126  			if !valid {
   127  				continue
   128  			}
   129  			// Make sure the contract belongs to us by comparing the unlock
   130  			// hash to what we would expect.
   131  			ourSK, ourPK := skymodules.GenerateContractKeyPair(rs, txn)
   132  			defer fastrand.Read(ourSK[:])
   133  			uc := types.UnlockConditions{
   134  				PublicKeys: []types.SiaPublicKey{
   135  					types.Ed25519PublicKey(ourPK),
   136  					hostKey,
   137  				},
   138  				SignaturesRequired: 2,
   139  			}
   140  			if fc.UnlockHash != uc.UnlockHash() {
   141  				continue
   142  			}
   143  			// Make sure we don't know about that contract already.
   144  			fcid := txn.FileContractID(uint64(i))
   145  			_, known := c.staticContracts.View(fcid)
   146  			if known {
   147  				continue
   148  			}
   149  			// Make sure we don't already track that contract as recoverable.
   150  			_, known = c.recoverableContracts[fcid]
   151  			if known {
   152  				continue
   153  			}
   154  
   155  			// Mark the contract for recovery.
   156  			c.recoverableContracts[fcid] = skymodules.RecoverableContract{
   157  				FileContract:  fc,
   158  				ID:            fcid,
   159  				HostPublicKey: hostKey,
   160  				InputParentID: txn.SiacoinInputs[0].ParentID,
   161  				TxnFee:        txnFee,
   162  				StartHeight:   c.blockHeight - 1, // Assume that it takes 1 block to mine the contract
   163  			}
   164  		}
   165  	}
   166  }
   167  
   168  // managedRecoverContract recovers a single contract by contacting the host it
   169  // was formed with and retrieving the latest revision and sector roots.
   170  func (c *Contractor) managedRecoverContract(rc skymodules.RecoverableContract, rs skymodules.EphemeralRenterSeed, blockHeight types.BlockHeight) (err error) {
   171  	// Get the corresponding host.
   172  	host, ok, err := c.staticHDB.Host(rc.HostPublicKey)
   173  	if err != nil {
   174  		return errors.AddContext(err, "error getting host from hostdb:")
   175  	}
   176  	if !ok {
   177  		return errors.New("Can't recover contract with unknown host")
   178  	}
   179  	// Generate the secret key for the handshake and wipe it after using it.
   180  	sk, _ := skymodules.GenerateContractKeyPairWithOutputID(rs, rc.InputParentID)
   181  	defer fastrand.Read(sk[:])
   182  	// Start a new RPC session.
   183  	s, err := c.staticContracts.NewRawSession(host, blockHeight, c.staticHDB, c.staticTG.StopChan())
   184  	if err != nil {
   185  		return err
   186  	}
   187  	defer func() {
   188  		err = errors.Compose(err, s.Close())
   189  	}()
   190  	// Get the most recent revision.
   191  	rev, sigs, err := s.Lock(rc.ID, sk)
   192  	if err != nil {
   193  		return err
   194  	}
   195  	// Build a transaction for the revision.
   196  	revTxn := types.Transaction{
   197  		FileContractRevisions: []types.FileContractRevision{rev},
   198  		TransactionSignatures: sigs,
   199  	}
   200  	// Get the merkle roots.
   201  	var roots []crypto.Hash
   202  	if rev.NewFileSize > 0 {
   203  		// TODO Followup: take host max download batch size into account.
   204  		revTxn, roots, err = s.RecoverSectorRoots(rev, sk)
   205  		if err != nil {
   206  			return err
   207  		}
   208  	}
   209  
   210  	// Insert the contract into the set.
   211  	contract, err := c.staticContracts.InsertContract(rc, revTxn, roots, sk)
   212  	if err != nil {
   213  		return err
   214  	}
   215  	// Add a mapping from the contract's id to the public key of the host.
   216  	c.mu.Lock()
   217  	defer c.mu.Unlock()
   218  	_, exists := c.pubKeysToContractID[contract.HostPublicKey.String()]
   219  	if exists {
   220  		// NOTE: There is a chance that this happens if c.recoverableContracts
   221  		// contains multiple recoverable contracts for a single host. In that
   222  		// case we don't update the mapping and let managedCheckForDuplicates
   223  		// and managedUpdatePubKeyToContractIDMap handle that later.
   224  		return errors.New("can't recover contract with a host that we already have a contract with")
   225  	}
   226  	c.pubKeysToContractID[contract.HostPublicKey.String()] = contract.ID
   227  
   228  	// Tell the watchdog to watch this transaction for revisions and storage
   229  	// proofs.
   230  	monitorContractArgs := monitorContractArgs{
   231  		recovered:   true,
   232  		fcID:        contract.ID,
   233  		revisionTxn: contract.Transaction,
   234  	}
   235  	err = c.staticWatchdog.callMonitorContract(monitorContractArgs)
   236  	if errors.Contains(err, errAlreadyWatchingContract) {
   237  		c.staticLog.Debugln("Watchdog already aware of recovered contract")
   238  		err = nil
   239  	}
   240  	return err
   241  }
   242  
   243  // callRecoverContracts recovers known recoverable contracts.
   244  func (c *Contractor) callRecoverContracts() {
   245  	if c.staticDeps.Disrupt("DisableContractRecovery") {
   246  		return
   247  	}
   248  	// Get the wallet seed.
   249  	ws, _, err := c.staticWallet.PrimarySeed()
   250  	if err != nil {
   251  		c.staticLog.Println("Can't recover contracts", err)
   252  		return
   253  	}
   254  	// Get the renter seed and wipe it once we are done with it.
   255  	renterSeed := skymodules.DeriveRenterSeed(ws)
   256  	defer fastrand.Read(renterSeed[:])
   257  	// Copy necessary fields to avoid having to hold the lock for too long.
   258  	c.mu.RLock()
   259  	blockHeight := c.blockHeight
   260  	recoverableContracts := make([]skymodules.RecoverableContract, 0, len(c.recoverableContracts))
   261  	for _, rc := range c.recoverableContracts {
   262  		recoverableContracts = append(recoverableContracts, rc)
   263  	}
   264  	c.mu.RUnlock()
   265  
   266  	// Remember the deleted contracts.
   267  	deleteContract := make([]bool, len(recoverableContracts))
   268  
   269  	// Try to recover the contracts in parallel.
   270  	var wg sync.WaitGroup
   271  	for i, recoverableContract := range recoverableContracts {
   272  		wg.Add(1)
   273  		go func(j int, rc skymodules.RecoverableContract) {
   274  			defer wg.Done()
   275  			if blockHeight >= rc.WindowEnd {
   276  				// No need to recover a contract if we are beyond the WindowEnd.
   277  				deleteContract[j] = true
   278  				c.staticLog.Printf("Not recovering contract since the current blockheight %v is >= the WindowEnd %v: %v",
   279  					blockHeight, rc.WindowEnd, rc.ID)
   280  				return
   281  			}
   282  			_, exists := c.staticContracts.View(rc.ID)
   283  			if exists {
   284  				c.staticLog.Debugln("Don't recover contract we already know", rc.ID)
   285  				return
   286  			}
   287  			// Get the ephemeral renter seed and wipe it after using it.
   288  			ers := renterSeed.EphemeralRenterSeed(rc.WindowStart)
   289  			defer fastrand.Read(ers[:])
   290  			// Recover contract.
   291  			err := c.managedRecoverContract(rc, ers, blockHeight)
   292  			if err != nil {
   293  				c.staticLog.Println("Failed to recover contract", rc.ID, err)
   294  				return
   295  			}
   296  			// Recovery was successful.
   297  			deleteContract[j] = true
   298  			c.staticLog.Println("Successfully recovered contract", rc.ID)
   299  		}(i, recoverableContract)
   300  	}
   301  
   302  	// Wait for the recovery to be done.
   303  	wg.Wait()
   304  
   305  	// Delete the contracts.
   306  	c.mu.Lock()
   307  	for i, rc := range recoverableContracts {
   308  		if deleteContract[i] {
   309  			delete(c.recoverableContracts, rc.ID)
   310  			c.staticLog.Println("Deleted contract from recoverable contracts:", rc.ID)
   311  		}
   312  	}
   313  	err = c.save()
   314  	if err != nil {
   315  		c.staticLog.Println("Unable to save while recovering contracts:", err)
   316  	}
   317  	c.mu.Unlock()
   318  }
   319  
   320  // removeRecoverableContracts removes contracts found in the block b from the
   321  // recoverableContracts map.
   322  func (c *Contractor) removeRecoverableContracts(b types.Block) {
   323  	for _, txn := range b.Transactions {
   324  		for i := range txn.FileContracts {
   325  			// Compute the contract id for that contract.
   326  			fcid := txn.FileContractID(uint64(i))
   327  			// Delete the contract from the map since we no longer need to
   328  			// recover it.
   329  			delete(c.recoverableContracts, fcid)
   330  		}
   331  	}
   332  }