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