gitlab.com/jokerrs1/Sia@v1.3.2/modules/renter/proto/downloader.go (about) 1 package proto 2 3 import ( 4 "errors" 5 "net" 6 "sync" 7 "time" 8 9 "github.com/NebulousLabs/Sia/crypto" 10 "github.com/NebulousLabs/Sia/encoding" 11 "github.com/NebulousLabs/Sia/modules" 12 "github.com/NebulousLabs/Sia/types" 13 ) 14 15 // A Downloader retrieves sectors by calling the download RPC on a host. 16 // Downloaders are NOT thread- safe; calls to Sector must be serialized. 17 type Downloader struct { 18 contractID types.FileContractID 19 contractSet *ContractSet 20 host modules.HostDBEntry 21 conn net.Conn 22 closeChan chan struct{} 23 once sync.Once 24 hdb hostDB 25 } 26 27 // Sector retrieves the sector with the specified Merkle root, and revises 28 // the underlying contract to pay the host proportionally to the data 29 // retrieve. 30 func (hd *Downloader) Sector(root crypto.Hash) (_ modules.RenterContract, _ []byte, err error) { 31 // Reset deadline when finished. 32 defer extendDeadline(hd.conn, time.Hour) // TODO: Constant. 33 34 // Acquire the contract. 35 // TODO: why not just lock the SafeContract directly? 36 sc, haveContract := hd.contractSet.Acquire(hd.contractID) 37 if !haveContract { 38 return modules.RenterContract{}, nil, errors.New("contract not present in contract set") 39 } 40 defer hd.contractSet.Return(sc) 41 contract := sc.header // for convenience 42 43 // calculate price 44 sectorPrice := hd.host.DownloadBandwidthPrice.Mul64(modules.SectorSize) 45 if contract.RenterFunds().Cmp(sectorPrice) < 0 { 46 return modules.RenterContract{}, nil, errors.New("contract has insufficient funds to support download") 47 } 48 // To mitigate small errors (e.g. differing block heights), fudge the 49 // price and collateral by 0.2%. 50 sectorPrice = sectorPrice.MulFloat(1 + hostPriceLeeway) 51 52 // create the download revision 53 rev := newDownloadRevision(contract.LastRevision(), sectorPrice) 54 55 // initiate download by confirming host settings 56 extendDeadline(hd.conn, modules.NegotiateSettingsTime) 57 if err := startDownload(hd.conn, hd.host); err != nil { 58 return modules.RenterContract{}, nil, err 59 } 60 61 // record the change we are about to make to the contract. If we lose power 62 // mid-revision, this allows us to restore either the pre-revision or 63 // post-revision contract. 64 walTxn, err := sc.recordDownloadIntent(rev, sectorPrice) 65 if err != nil { 66 return modules.RenterContract{}, nil, err 67 } 68 69 // send download action 70 extendDeadline(hd.conn, 2*time.Minute) // TODO: Constant. 71 err = encoding.WriteObject(hd.conn, []modules.DownloadAction{{ 72 MerkleRoot: root, 73 Offset: 0, 74 Length: modules.SectorSize, 75 }}) 76 if err != nil { 77 return modules.RenterContract{}, nil, err 78 } 79 80 // Increase Successful/Failed interactions accordingly 81 defer func() { 82 if err != nil { 83 hd.hdb.IncrementFailedInteractions(contract.HostPublicKey()) 84 } else if err == nil { 85 hd.hdb.IncrementSuccessfulInteractions(contract.HostPublicKey()) 86 } 87 }() 88 89 // send the revision to the host for approval 90 extendDeadline(hd.conn, 2*time.Minute) // TODO: Constant. 91 signedTxn, err := negotiateRevision(hd.conn, rev, contract.SecretKey) 92 if err == modules.ErrStopResponse { 93 // if host gracefully closed, close our connection as well; this will 94 // cause the next download to fail. However, we must delay closing 95 // until we've finished downloading the sector. 96 defer hd.conn.Close() 97 } else if err != nil { 98 return modules.RenterContract{}, nil, err 99 } 100 101 // read sector data, completing one iteration of the download loop 102 extendDeadline(hd.conn, modules.NegotiateDownloadTime) 103 var sectors [][]byte 104 if err := encoding.ReadObject(hd.conn, §ors, modules.SectorSize+16); err != nil { 105 return modules.RenterContract{}, nil, err 106 } else if len(sectors) != 1 { 107 return modules.RenterContract{}, nil, errors.New("host did not send enough sectors") 108 } 109 sector := sectors[0] 110 if uint64(len(sector)) != modules.SectorSize { 111 return modules.RenterContract{}, nil, errors.New("host did not send enough sector data") 112 } else if crypto.MerkleRoot(sector) != root { 113 return modules.RenterContract{}, nil, errors.New("host sent bad sector data") 114 } 115 116 // update contract and metrics 117 if err := sc.commitDownload(walTxn, signedTxn, sectorPrice); err != nil { 118 return modules.RenterContract{}, nil, err 119 } 120 121 return sc.Metadata(), sector, nil 122 } 123 124 // shutdown terminates the revision loop and signals the goroutine spawned in 125 // NewDownloader to return. 126 func (hd *Downloader) shutdown() { 127 extendDeadline(hd.conn, modules.NegotiateSettingsTime) 128 // don't care about these errors 129 _, _ = verifySettings(hd.conn, hd.host) 130 _ = modules.WriteNegotiationStop(hd.conn) 131 close(hd.closeChan) 132 } 133 134 // Close cleanly terminates the download loop with the host and closes the 135 // connection. 136 func (hd *Downloader) Close() error { 137 // using once ensures that Close is idempotent 138 hd.once.Do(hd.shutdown) 139 return hd.conn.Close() 140 } 141 142 // NewDownloader initiates the download request loop with a host, and returns a 143 // Downloader. 144 func (cs *ContractSet) NewDownloader(host modules.HostDBEntry, id types.FileContractID, hdb hostDB, cancel <-chan struct{}) (_ *Downloader, err error) { 145 sc, ok := cs.Acquire(id) 146 if !ok { 147 return nil, errors.New("invalid contract") 148 } 149 defer cs.Return(sc) 150 contract := sc.header 151 152 // check that contract has enough value to support a download 153 sectorPrice := host.DownloadBandwidthPrice.Mul64(modules.SectorSize) 154 if contract.RenterFunds().Cmp(sectorPrice) < 0 { 155 return nil, errors.New("contract has insufficient funds to support download") 156 } 157 158 // Increase Successful/Failed interactions accordingly 159 defer func() { 160 // A revision mismatch might not be the host's fault. 161 if err != nil && !IsRevisionMismatch(err) { 162 hdb.IncrementFailedInteractions(contract.HostPublicKey()) 163 } else if err == nil { 164 hdb.IncrementSuccessfulInteractions(contract.HostPublicKey()) 165 } 166 }() 167 168 conn, closeChan, err := initiateRevisionLoop(host, contract, modules.RPCDownload, cancel) 169 if IsRevisionMismatch(err) && len(sc.unappliedTxns) > 0 { 170 // we have desynced from the host. If we have unapplied updates from the 171 // WAL, try applying them. 172 conn, closeChan, err = initiateRevisionLoop(host, sc.unappliedHeader(), modules.RPCDownload, cancel) 173 if err != nil { 174 return nil, err 175 } 176 // applying the updates was successful; commit them to disk 177 if err := sc.commitTxns(); err != nil { 178 return nil, err 179 } 180 } else if err != nil { 181 return nil, err 182 } 183 // if we succeeded, we can safely discard the unappliedTxns 184 for _, txn := range sc.unappliedTxns { 185 txn.SignalUpdatesApplied() 186 } 187 sc.unappliedTxns = nil 188 189 // the host is now ready to accept revisions 190 return &Downloader{ 191 contractID: id, 192 contractSet: cs, 193 host: host, 194 conn: conn, 195 closeChan: closeChan, 196 hdb: hdb, 197 }, nil 198 }