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 }