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