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