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