gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/renter/proto/editor.go (about) 1 package proto 2 3 import ( 4 "net" 5 "sync" 6 "time" 7 8 "gitlab.com/SiaPrime/SiaPrime/build" 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 "gitlab.com/NebulousLabs/ratelimit" 16 ) 17 18 // cachedMerkleRoot calculates the root of a set of existing Merkle roots. 19 func cachedMerkleRoot(roots []crypto.Hash) crypto.Hash { 20 tree := crypto.NewCachedTree(sectorHeight) // NOTE: height is not strictly necessary here 21 for _, h := range roots { 22 tree.Push(h) 23 } 24 return tree.Root() 25 } 26 27 // A Editor modifies a Contract by calling the revise RPC on a host. It 28 // Editors are NOT thread-safe; calls to Upload must happen in serial. 29 type Editor struct { 30 contractID types.FileContractID 31 contractSet *ContractSet 32 conn net.Conn 33 closeChan chan struct{} 34 deps modules.Dependencies 35 hdb hostDB 36 host modules.HostDBEntry 37 once sync.Once 38 39 height types.BlockHeight 40 } 41 42 // shutdown terminates the revision loop and signals the goroutine spawned in 43 // NewEditor to return. 44 func (he *Editor) shutdown() { 45 extendDeadline(he.conn, modules.NegotiateSettingsTime) 46 // don't care about these errors 47 _, _ = verifySettings(he.conn, he.host) 48 _ = modules.WriteNegotiationStop(he.conn) 49 close(he.closeChan) 50 } 51 52 // Close cleanly terminates the revision loop with the host and closes the 53 // connection. 54 func (he *Editor) Close() error { 55 // using once ensures that Close is idempotent 56 he.once.Do(he.shutdown) 57 return he.conn.Close() 58 } 59 60 // Upload negotiates a revision that adds a sector to a file contract. 61 func (he *Editor) Upload(data []byte) (_ modules.RenterContract, _ crypto.Hash, err error) { 62 // Acquire the contract. 63 sc, haveContract := he.contractSet.Acquire(he.contractID) 64 if !haveContract { 65 return modules.RenterContract{}, crypto.Hash{}, errors.New("contract not present in contract set") 66 } 67 defer he.contractSet.Return(sc) 68 contract := sc.header // for convenience 69 70 // calculate price 71 // TODO: height is never updated, so we'll wind up overpaying on long-running uploads 72 blockBytes := types.NewCurrency64(modules.SectorSize * uint64(contract.LastRevision().NewWindowEnd-he.height)) 73 sectorStoragePrice := he.host.StoragePrice.Mul(blockBytes) 74 sectorBandwidthPrice := he.host.UploadBandwidthPrice.Mul64(modules.SectorSize) 75 sectorCollateral := he.host.Collateral.Mul(blockBytes) 76 77 // to mitigate small errors (e.g. differing block heights), fudge the 78 // price and collateral by 0.2%. This is only applied to hosts above 79 // v1.0.1; older hosts use stricter math. 80 if build.VersionCmp(he.host.Version, "1.0.1") > 0 { 81 sectorStoragePrice = sectorStoragePrice.MulFloat(1 + hostPriceLeeway) 82 sectorBandwidthPrice = sectorBandwidthPrice.MulFloat(1 + hostPriceLeeway) 83 sectorCollateral = sectorCollateral.MulFloat(1 - hostPriceLeeway) 84 } 85 86 sectorPrice := sectorStoragePrice.Add(sectorBandwidthPrice) 87 if contract.RenterFunds().Cmp(sectorPrice) < 0 { 88 return modules.RenterContract{}, crypto.Hash{}, errors.New("contract has insufficient funds to support upload") 89 } 90 if contract.LastRevision().NewMissedProofOutputs[1].Value.Cmp(sectorCollateral) < 0 { 91 sectorCollateral = contract.LastRevision().NewMissedProofOutputs[1].Value 92 } 93 94 // calculate the new Merkle root 95 sectorRoot := crypto.MerkleRoot(data) 96 merkleRoot := sc.merkleRoots.checkNewRoot(sectorRoot) 97 98 // create the action and revision 99 actions := []modules.RevisionAction{{ 100 Type: modules.ActionInsert, 101 SectorIndex: uint64(sc.merkleRoots.len()), 102 Data: data, 103 }} 104 rev := newUploadRevision(contract.LastRevision(), merkleRoot, sectorPrice, sectorCollateral) 105 106 // run the revision iteration 107 defer func() { 108 // Increase Successful/Failed interactions accordingly 109 if err != nil { 110 // If the host was OOS, we update the contract utility. 111 if modules.IsOOSErr(err) { 112 u := sc.Utility() 113 u.GoodForUpload = false // Stop uploading to such a host immediately. 114 u.LastOOSErr = he.height 115 err = errors.Compose(err, sc.UpdateUtility(u)) 116 } 117 he.hdb.IncrementFailedInteractions(he.host.PublicKey) 118 err = errors.Extend(err, modules.ErrHostFault) 119 } else { 120 he.hdb.IncrementSuccessfulInteractions(he.host.PublicKey) 121 } 122 123 // reset deadline 124 extendDeadline(he.conn, time.Hour) 125 }() 126 127 // initiate revision 128 extendDeadline(he.conn, modules.NegotiateSettingsTime) 129 if err := startRevision(he.conn, he.host); err != nil { 130 return modules.RenterContract{}, crypto.Hash{}, err 131 } 132 133 // record the change we are about to make to the contract. If we lose power 134 // mid-revision, this allows us to restore either the pre-revision or 135 // post-revision contract. 136 walTxn, err := sc.managedRecordUploadIntent(rev, sectorRoot, sectorStoragePrice, sectorBandwidthPrice) 137 if err != nil { 138 return modules.RenterContract{}, crypto.Hash{}, err 139 } 140 141 // send actions 142 extendDeadline(he.conn, modules.NegotiateFileContractRevisionTime) 143 if err := encoding.WriteObject(he.conn, actions); err != nil { 144 return modules.RenterContract{}, crypto.Hash{}, err 145 } 146 147 // Disrupt here before sending the signed revision to the host. 148 if he.deps.Disrupt("InterruptUploadBeforeSendingRevision") { 149 return modules.RenterContract{}, crypto.Hash{}, 150 errors.New("InterruptUploadBeforeSendingRevision disrupt") 151 } 152 153 // send revision to host and exchange signatures 154 extendDeadline(he.conn, connTimeout) 155 signedTxn, err := negotiateRevision(he.conn, rev, contract.SecretKey, he.height) 156 if err == modules.ErrStopResponse { 157 // if host gracefully closed, close our connection as well; this will 158 // cause the next operation to fail 159 he.conn.Close() 160 } else if err != nil { 161 return modules.RenterContract{}, crypto.Hash{}, err 162 } 163 164 // Disrupt here before updating the contract. 165 if he.deps.Disrupt("InterruptUploadAfterSendingRevision") { 166 return modules.RenterContract{}, crypto.Hash{}, 167 errors.New("InterruptUploadAfterSendingRevision disrupt") 168 } 169 170 // update contract 171 err = sc.managedCommitUpload(walTxn, signedTxn, sectorRoot, sectorStoragePrice, sectorBandwidthPrice) 172 if err != nil { 173 return modules.RenterContract{}, crypto.Hash{}, err 174 } 175 176 return sc.Metadata(), sectorRoot, nil 177 } 178 179 // NewEditor initiates the contract revision process with a host, and returns 180 // an Editor. 181 func (cs *ContractSet) NewEditor(host modules.HostDBEntry, id types.FileContractID, currentHeight types.BlockHeight, hdb hostDB, cancel <-chan struct{}) (_ *Editor, err error) { 182 sc, ok := cs.Acquire(id) 183 if !ok { 184 return nil, errors.New("invalid contract") 185 } 186 defer cs.Return(sc) 187 contract := sc.header 188 189 // Increase Successful/Failed interactions accordingly 190 defer func() { 191 // a revision mismatch is not necessarily the host's fault 192 if err != nil && !IsRevisionMismatch(err) { 193 hdb.IncrementFailedInteractions(contract.HostPublicKey()) 194 err = errors.Extend(err, modules.ErrHostFault) 195 } else if err == nil { 196 hdb.IncrementSuccessfulInteractions(contract.HostPublicKey()) 197 } 198 }() 199 200 conn, closeChan, err := initiateRevisionLoop(host, sc, modules.RPCReviseContract, cancel, cs.rl) 201 if err != nil { 202 return nil, errors.AddContext(err, "failed to initiate revision loop") 203 } 204 // if we succeeded, we can safely discard the unappliedTxns 205 for _, txn := range sc.unappliedTxns { 206 txn.SignalUpdatesApplied() 207 } 208 sc.unappliedTxns = nil 209 210 // the host is now ready to accept revisions 211 return &Editor{ 212 host: host, 213 hdb: hdb, 214 contractID: id, 215 contractSet: cs, 216 conn: conn, 217 closeChan: closeChan, 218 deps: cs.deps, 219 220 height: currentHeight, 221 }, nil 222 } 223 224 // initiateRevisionLoop initiates either the editor or downloader loop with 225 // host, depending on which rpc was passed. 226 func initiateRevisionLoop(host modules.HostDBEntry, contract *SafeContract, rpc types.Specifier, cancel <-chan struct{}, rl *ratelimit.RateLimit) (net.Conn, chan struct{}, error) { 227 c, err := (&net.Dialer{ 228 Cancel: cancel, 229 Timeout: 45 * time.Second, // TODO: Constant 230 }).Dial("tcp", string(host.NetAddress)) 231 if err != nil { 232 return nil, nil, err 233 } 234 // Apply the local ratelimit. 235 conn := ratelimit.NewRLConn(c, rl, cancel) 236 // Apply the global ratelimit. 237 conn = ratelimit.NewRLConn(conn, modules.GlobalRateLimits, cancel) 238 239 closeChan := make(chan struct{}) 240 go func() { 241 select { 242 case <-cancel: 243 conn.Close() 244 case <-closeChan: 245 } 246 }() 247 248 // allot 2 minutes for RPC request + revision exchange 249 extendDeadline(conn, modules.NegotiateRecentRevisionTime) 250 defer extendDeadline(conn, time.Hour) 251 if err := encoding.WriteObject(conn, rpc); err != nil { 252 conn.Close() 253 close(closeChan) 254 return nil, closeChan, errors.New("couldn't initiate RPC: " + err.Error()) 255 } 256 if err := verifyRecentRevision(conn, contract, host.Version); err != nil { 257 conn.Close() // TODO: close gracefully if host has entered revision loop 258 close(closeChan) 259 return nil, closeChan, err 260 } 261 return conn, closeChan, nil 262 }