// Editor initiates the contract revision process with a host, and returns // an Editor. func (c *Contractor) Editor(contract modules.RenterContract) (Editor, error) { c.mu.RLock() height := c.blockHeight c.mu.RUnlock() if height > contract.EndHeight() { return nil, errors.New("contract has already ended") } host, ok := c.hdb.Host(contract.NetAddress) if !ok { return nil, errors.New("no record of that host") } if host.StoragePrice.Cmp(maxStoragePrice) > 0 { return nil, errTooExpensive } // cap host.Collateral on new hosts if build.VersionCmp(host.Version, "0.6.0") > 0 { if host.Collateral.Cmp(maxUploadCollateral) > 0 { host.Collateral = maxUploadCollateral } } // create editor e, err := proto.NewEditor(host, contract, height) if proto.IsRevisionMismatch(err) { // try again with the cached revision c.mu.RLock() cached, ok := c.cachedRevisions[contract.ID] c.mu.RUnlock() if !ok { // nothing we can do; return original error return nil, err } c.log.Printf("host %v has different revision for %v; retrying with cached revision", contract.NetAddress, contract.ID) contract.LastRevision = cached.revision contract.MerkleRoots = cached.merkleRoots e, err = proto.NewEditor(host, contract, height) } if err != nil { return nil, err } // supply a SaveFn that saves the revision to the contractor's persist // (the existing revision will be overwritten when SaveFn is called) e.SaveFn = c.saveRevision(contract.ID) return &hostEditor{ editor: e, contract: contract, contractor: c, }, nil }
// Downloader initiates the download request loop with a host, and returns a // Downloader. func (c *Contractor) Downloader(contract modules.RenterContract) (Downloader, error) { c.mu.RLock() height := c.blockHeight c.mu.RUnlock() if height > contract.EndHeight() { return nil, errors.New("contract has already ended") } host, ok := c.hdb.Host(contract.NetAddress) if !ok { return nil, errors.New("no record of that host") } if host.DownloadBandwidthPrice.Cmp(maxDownloadPrice) > 0 { return nil, errTooExpensive } // create downloader d, err := proto.NewDownloader(host, contract) if proto.IsRevisionMismatch(err) { // try again with the cached revision c.mu.RLock() cached, ok := c.cachedRevisions[contract.ID] c.mu.RUnlock() if !ok { // nothing we can do; return original error return nil, err } c.log.Printf("host %v has different revision for %v; retrying with cached revision", contract.NetAddress, contract.ID) contract.LastRevision = cached.revision d, err = proto.NewDownloader(host, contract) } if err != nil { return nil, err } // supply a SaveFn that saves the revision to the contractor's persist // (the existing revision will be overwritten when SaveFn is called) d.SaveFn = c.saveRevision(contract.ID) return &hostDownloader{ downloader: d, contractor: c, }, nil }
// TestResync tests that the contractor can resync with a host after being // interrupted during contract revision. func TestResync(t *testing.T) { if testing.Short() { t.SkipNow() } t.Parallel() // create testing trio h, c, _, err := newTestingTrio("TestResync") if err != nil { t.Fatal(err) } // get the host's entry from the db hostEntry, ok := c.hdb.Host(h.ExternalSettings().NetAddress) if !ok { t.Fatal("no entry for host in db") } // form a contract with the host contract, err := c.managedNewContract(hostEntry, 10, c.blockHeight+100) if err != nil { t.Fatal(err) } // revise the contract editor, err := c.Editor(contract) if err != nil { t.Fatal(err) } data, err := crypto.RandBytes(int(modules.SectorSize)) if err != nil { t.Fatal(err) } root, err := editor.Upload(data) if err != nil { t.Fatal(err) } err = editor.Close() if err != nil { t.Fatal(err) } // download the data contract = c.contracts[contract.ID] downloader, err := c.Downloader(contract) if err != nil { t.Fatal(err) } retrieved, err := downloader.Sector(root) if err != nil { t.Fatal(err) } if !bytes.Equal(data, retrieved) { t.Fatal("downloaded data does not match original") } err = downloader.Close() if err != nil { t.Fatal(err) } contract = c.contracts[contract.ID] // corrupt contract and delete its cachedRevision badContract := contract badContract.LastRevision.NewRevisionNumber-- badContract.LastRevisionTxn.TransactionSignatures = nil // delete signatures c.mu.Lock() delete(c.cachedRevisions, contract.ID) c.mu.Unlock() // Editor and Downloader should fail with the bad contract _, err = c.Editor(badContract) if !proto.IsRevisionMismatch(err) { t.Fatal("expected revision mismatch, got", err) } _, err = c.Downloader(badContract) if !proto.IsRevisionMismatch(err) { t.Fatal("expected revision mismatch, got", err) } // add cachedRevision cachedRev := cachedRevision{contract.LastRevision, contract.MerkleRoots} c.mu.Lock() c.cachedRevisions[contract.ID] = cachedRev c.mu.Unlock() // Editor and Downloader should now succeed after loading the cachedRevision editor, err = c.Editor(badContract) if err != nil { t.Fatal(err) } editor.Close() downloader, err = c.Downloader(badContract) if err != nil { t.Fatal(err) } downloader.Close() // corrupt contract and delete its cachedRevision badContract = contract badContract.LastRevision.NewRevisionNumber-- badContract.MerkleRoots = nil // delete Merkle roots c.mu.Lock() delete(c.cachedRevisions, contract.ID) c.mu.Unlock() // Editor and Downloader should fail with the bad contract _, err = c.Editor(badContract) if !proto.IsRevisionMismatch(err) { t.Fatal("expected revision mismatch, got", err) } _, err = c.Downloader(badContract) if !proto.IsRevisionMismatch(err) { t.Fatal("expected revision mismatch, got", err) } // add cachedRevision c.mu.Lock() c.cachedRevisions[contract.ID] = cachedRev c.mu.Unlock() // should be able to upload after loading the cachedRevision editor, err = c.Editor(badContract) if err != nil { t.Fatal(err) } _, err = editor.Upload(data) if err != nil { t.Fatal(err) } editor.Close() }