github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/modules/renter/proto/editor.go (about) 1 package proto 2 3 import ( 4 "net" 5 "sync" 6 "time" 7 8 "SiaPrime/build" 9 "SiaPrime/crypto" 10 "SiaPrime/encoding" 11 "SiaPrime/modules" 12 "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 return modules.RenterContract{}, crypto.Hash{}, errors.New("contract has insufficient collateral to support upload") 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 he.hdb.IncrementFailedInteractions(he.host.PublicKey) 111 err = errors.Extend(err, modules.ErrHostFault) 112 } else { 113 he.hdb.IncrementSuccessfulInteractions(he.host.PublicKey) 114 } 115 116 // reset deadline 117 extendDeadline(he.conn, time.Hour) 118 }() 119 120 // initiate revision 121 extendDeadline(he.conn, modules.NegotiateSettingsTime) 122 if err := startRevision(he.conn, he.host); err != nil { 123 return modules.RenterContract{}, crypto.Hash{}, err 124 } 125 126 // record the change we are about to make to the contract. If we lose power 127 // mid-revision, this allows us to restore either the pre-revision or 128 // post-revision contract. 129 walTxn, err := sc.recordUploadIntent(rev, sectorRoot, sectorStoragePrice, sectorBandwidthPrice) 130 if err != nil { 131 return modules.RenterContract{}, crypto.Hash{}, err 132 } 133 134 // send actions 135 extendDeadline(he.conn, modules.NegotiateFileContractRevisionTime) 136 if err := encoding.WriteObject(he.conn, actions); err != nil { 137 return modules.RenterContract{}, crypto.Hash{}, err 138 } 139 140 // Disrupt here before sending the signed revision to the host. 141 if he.deps.Disrupt("InterruptUploadBeforeSendingRevision") { 142 return modules.RenterContract{}, crypto.Hash{}, 143 errors.New("InterruptUploadBeforeSendingRevision disrupt") 144 } 145 146 // send revision to host and exchange signatures 147 extendDeadline(he.conn, connTimeout) 148 signedTxn, err := negotiateRevision(he.conn, rev, contract.SecretKey, he.height) 149 if err == modules.ErrStopResponse { 150 // if host gracefully closed, close our connection as well; this will 151 // cause the next operation to fail 152 he.conn.Close() 153 } else if err != nil { 154 return modules.RenterContract{}, crypto.Hash{}, err 155 } 156 157 // Disrupt here before updating the contract. 158 if he.deps.Disrupt("InterruptUploadAfterSendingRevision") { 159 return modules.RenterContract{}, crypto.Hash{}, 160 errors.New("InterruptUploadAfterSendingRevision disrupt") 161 } 162 163 // update contract 164 err = sc.commitUpload(walTxn, signedTxn, sectorRoot, sectorStoragePrice, sectorBandwidthPrice) 165 if err != nil { 166 return modules.RenterContract{}, crypto.Hash{}, err 167 } 168 169 return sc.Metadata(), sectorRoot, nil 170 } 171 172 // NewEditor initiates the contract revision process with a host, and returns 173 // an Editor. 174 func (cs *ContractSet) NewEditor(host modules.HostDBEntry, id types.FileContractID, currentHeight types.BlockHeight, hdb hostDB, cancel <-chan struct{}) (_ *Editor, 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 // Increase Successful/Failed interactions accordingly 183 defer func() { 184 // a revision mismatch is not necessarily the host's fault 185 if err != nil && !IsRevisionMismatch(err) { 186 hdb.IncrementFailedInteractions(contract.HostPublicKey()) 187 err = errors.Extend(err, modules.ErrHostFault) 188 } else if err == nil { 189 hdb.IncrementSuccessfulInteractions(contract.HostPublicKey()) 190 } 191 }() 192 193 conn, closeChan, err := initiateRevisionLoop(host, sc, modules.RPCReviseContract, cancel, cs.rl) 194 if err != nil { 195 return nil, errors.AddContext(err, "failed to initiate revision loop") 196 } 197 // if we succeeded, we can safely discard the unappliedTxns 198 for _, txn := range sc.unappliedTxns { 199 txn.SignalUpdatesApplied() 200 } 201 sc.unappliedTxns = nil 202 203 // the host is now ready to accept revisions 204 return &Editor{ 205 host: host, 206 hdb: hdb, 207 contractID: id, 208 contractSet: cs, 209 conn: conn, 210 closeChan: closeChan, 211 deps: cs.deps, 212 213 height: currentHeight, 214 }, nil 215 } 216 217 // initiateRevisionLoop initiates either the editor or downloader loop with 218 // host, depending on which rpc was passed. 219 func initiateRevisionLoop(host modules.HostDBEntry, contract *SafeContract, rpc types.Specifier, cancel <-chan struct{}, rl *ratelimit.RateLimit) (net.Conn, chan struct{}, error) { 220 c, err := (&net.Dialer{ 221 Cancel: cancel, 222 Timeout: 45 * time.Second, // TODO: Constant 223 }).Dial("tcp", string(host.NetAddress)) 224 if err != nil { 225 return nil, nil, err 226 } 227 conn := ratelimit.NewRLConn(c, rl, cancel) 228 229 closeChan := make(chan struct{}) 230 go func() { 231 select { 232 case <-cancel: 233 conn.Close() 234 case <-closeChan: 235 } 236 }() 237 238 // allot 2 minutes for RPC request + revision exchange 239 extendDeadline(conn, modules.NegotiateRecentRevisionTime) 240 defer extendDeadline(conn, time.Hour) 241 if err := encoding.WriteObject(conn, rpc); err != nil { 242 conn.Close() 243 close(closeChan) 244 return nil, closeChan, errors.New("couldn't initiate RPC: " + err.Error()) 245 } 246 if err := verifyRecentRevision(conn, contract, host.Version); err != nil { 247 conn.Close() // TODO: close gracefully if host has entered revision loop 248 close(closeChan) 249 return nil, closeChan, err 250 } 251 return conn, closeChan, nil 252 }