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 }