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