// managedRPCRecentRevision sends the most recent known file contract // revision, including signatures, to the renter, for the file contract with // the input id. func (h *Host) managedRPCRecentRevision(conn net.Conn) (types.FileContractID, *storageObligation, error) { // Set the negotiation deadline. conn.SetDeadline(time.Now().Add(modules.NegotiateRecentRevisionTime)) // Receive the file contract id from the renter. var fcid types.FileContractID err := encoding.ReadObject(conn, &fcid, uint64(len(fcid))) if err != nil { return types.FileContractID{}, nil, err } // Send a challenge to the renter to verify that the renter has write // access to the revision being opened. var challenge crypto.Hash _, err = rand.Read(challenge[:]) if err != nil { return types.FileContractID{}, nil, err } err = encoding.WriteObject(conn, challenge) if err != nil { return types.FileContractID{}, nil, err } // Read the signed response from the renter. var challengeResponse crypto.Signature err = encoding.ReadObject(conn, &challengeResponse, uint64(len(challengeResponse))) if err != nil { return types.FileContractID{}, nil, err } // Verify the response. In the process, fetch the related storage // obligation, file contract revision, and transaction signatures. so, recentRevision, revisionSigs, err := h.verifyChallengeResponse(fcid, challenge, challengeResponse) if err != nil { return types.FileContractID{}, nil, modules.WriteNegotiationRejection(conn, err) } // Send the file contract revision and the corresponding signatures to the // renter. err = modules.WriteNegotiationAcceptance(conn) if err != nil { return types.FileContractID{}, nil, err } err = encoding.WriteObject(conn, recentRevision) if err != nil { return types.FileContractID{}, nil, err } err = encoding.WriteObject(conn, revisionSigs) if err != nil { return types.FileContractID{}, nil, err } return fcid, so, nil }
// TestNegotiateRevisionStopResponse tests that when the host sends // StopResponse, the renter continues processing the revision instead of // immediately terminating. func TestNegotiateRevisionStopResponse(t *testing.T) { // simulate a renter-host connection rConn, hConn := net.Pipe() // handle the host's half of the pipe go func() { defer hConn.Close() // read revision encoding.ReadObject(hConn, new(types.FileContractRevision), 1<<22) // write acceptance modules.WriteNegotiationAcceptance(hConn) // read txn signature encoding.ReadObject(hConn, new(types.TransactionSignature), 1<<22) // write StopResponse modules.WriteNegotiationStop(hConn) // write txn signature encoding.WriteObject(hConn, types.TransactionSignature{}) }() // since the host wrote StopResponse, we should proceed to validating the // transaction. This will return a known error because we are supplying an // empty revision. _, err := negotiateRevision(rConn, types.FileContractRevision{}, crypto.SecretKey{}) if err != types.ErrFileContractWindowStartViolation { t.Fatalf("expected %q, got \"%v\"", types.ErrFileContractWindowStartViolation, err) } rConn.Close() // same as above, but send an error instead of StopResponse. The error // should be returned by negotiateRevision immediately (if it is not, we // should expect to see a transaction validation error instead). rConn, hConn = net.Pipe() go func() { defer hConn.Close() encoding.ReadObject(hConn, new(types.FileContractRevision), 1<<22) modules.WriteNegotiationAcceptance(hConn) encoding.ReadObject(hConn, new(types.TransactionSignature), 1<<22) // write a sentinel error modules.WriteNegotiationRejection(hConn, errors.New("sentinel")) encoding.WriteObject(hConn, types.TransactionSignature{}) }() expectedErr := "host did not accept transaction signature: sentinel" _, err = negotiateRevision(rConn, types.FileContractRevision{}, crypto.SecretKey{}) if err == nil || err.Error() != expectedErr { t.Fatalf("expected %q, got \"%v\"", expectedErr, err) } rConn.Close() }
// managedRevisionIteration handles one iteration of the revision loop. As a // performance optimization, multiple iterations of revisions are allowed to be // made over the same connection. func (h *Host) managedRevisionIteration(conn net.Conn, so *storageObligation, finalIter bool) error { // Send the settings to the renter. The host will keep going even if it is // not accepting contracts, because in this case the contract already // exists. err := h.managedRPCSettings(conn) if err != nil { return err } // Set the negotiation deadline. conn.SetDeadline(time.Now().Add(modules.NegotiateFileContractRevisionTime)) // The renter will either accept or reject the settings + revision // transaction. It may also return a stop response to indicate that it // wishes to terminate the revision loop. err = modules.ReadNegotiationAcceptance(conn) if err != nil { return err } // Read some variables from the host for use later in the function. h.mu.RLock() settings := h.settings secretKey := h.secretKey blockHeight := h.blockHeight h.mu.RUnlock() // The renter is going to send its intended modifications, followed by the // file contract revision that pays for them. var modifications []modules.RevisionAction var revision types.FileContractRevision err = encoding.ReadObject(conn, &modifications, settings.MaxReviseBatchSize) if err != nil { return err } err = encoding.ReadObject(conn, &revision, modules.NegotiateMaxFileContractRevisionSize) if err != nil { return err } // First read all of the modifications. Then make the modifications, but // with the ability to reverse them. Then verify the file contract revision // correctly accounts for the changes. var bandwidthRevenue types.Currency // Upload bandwidth. var storageRevenue types.Currency var newCollateral types.Currency var sectorsRemoved []crypto.Hash var sectorsGained []crypto.Hash var gainedSectorData [][]byte err = func() error { for _, modification := range modifications { // Check that the index points to an existing sector root. If the type // is ActionInsert, we permit inserting at the end. if modification.Type == modules.ActionInsert { if modification.SectorIndex > uint64(len(so.SectorRoots)) { return errBadModificationIndex } } else if modification.SectorIndex >= uint64(len(so.SectorRoots)) { return errBadModificationIndex } // Check that the data sent for the sector is not too large. if uint64(len(modification.Data)) > modules.SectorSize { return errLargeSector } switch modification.Type { case modules.ActionDelete: // There is no financial information to change, it is enough to // remove the sector. sectorsRemoved = append(sectorsRemoved, so.SectorRoots[modification.SectorIndex]) so.SectorRoots = append(so.SectorRoots[0:modification.SectorIndex], so.SectorRoots[modification.SectorIndex+1:]...) case modules.ActionInsert: // Check that the sector size is correct. if uint64(len(modification.Data)) != modules.SectorSize { return errBadSectorSize } // Update finances. blocksRemaining := so.proofDeadline() - blockHeight blockBytesCurrency := types.NewCurrency64(uint64(blocksRemaining)).Mul64(modules.SectorSize) bandwidthRevenue = bandwidthRevenue.Add(settings.MinUploadBandwidthPrice.Mul64(modules.SectorSize)) storageRevenue = storageRevenue.Add(settings.MinStoragePrice.Mul(blockBytesCurrency)) newCollateral = newCollateral.Add(settings.Collateral.Mul(blockBytesCurrency)) // Insert the sector into the root list. newRoot := crypto.MerkleRoot(modification.Data) sectorsGained = append(sectorsGained, newRoot) gainedSectorData = append(gainedSectorData, modification.Data) so.SectorRoots = append(so.SectorRoots[:modification.SectorIndex], append([]crypto.Hash{newRoot}, so.SectorRoots[modification.SectorIndex:]...)...) case modules.ActionModify: // Check that the offset and length are okay. Length is already // known to be appropriately small, but the offset needs to be // checked for being appropriately small as well otherwise there is // a risk of overflow. if modification.Offset > modules.SectorSize || modification.Offset+uint64(len(modification.Data)) > modules.SectorSize { return errIllegalOffsetAndLength } // Get the data for the new sector. sector, err := h.ReadSector(so.SectorRoots[modification.SectorIndex]) if err != nil { return err } copy(sector[modification.Offset:], modification.Data) // Update finances. bandwidthRevenue = bandwidthRevenue.Add(settings.MinUploadBandwidthPrice.Mul64(uint64(len(modification.Data)))) // Update the sectors removed and gained to indicate that the old // sector has been replaced with a new sector. newRoot := crypto.MerkleRoot(sector) sectorsRemoved = append(sectorsRemoved, so.SectorRoots[modification.SectorIndex]) sectorsGained = append(sectorsGained, newRoot) gainedSectorData = append(gainedSectorData, sector) so.SectorRoots[modification.SectorIndex] = newRoot default: return errUnknownModification } } newRevenue := storageRevenue.Add(bandwidthRevenue) return verifyRevision(so, revision, blockHeight, newRevenue, newCollateral) }() if err != nil { return modules.WriteNegotiationRejection(conn, err) } // Revision is acceptable, write an acceptance string. err = modules.WriteNegotiationAcceptance(conn) if err != nil { return err } // Renter will send a transaction signature for the file contract revision. var renterSig types.TransactionSignature err = encoding.ReadObject(conn, &renterSig, modules.NegotiateMaxTransactionSignatureSize) if err != nil { return err } // Verify that the signature is valid and get the host's signature. txn, err := createRevisionSignature(revision, renterSig, secretKey, blockHeight) if err != nil { return modules.WriteNegotiationRejection(conn, err) } so.PotentialStorageRevenue = so.PotentialStorageRevenue.Add(storageRevenue) so.RiskedCollateral = so.RiskedCollateral.Add(newCollateral) so.PotentialUploadRevenue = so.PotentialUploadRevenue.Add(bandwidthRevenue) so.RevisionTransactionSet = []types.Transaction{txn} err = h.modifyStorageObligation(so, sectorsRemoved, sectorsGained, gainedSectorData) if err != nil { return modules.WriteNegotiationRejection(conn, err) } // Host will now send acceptance and its signature to the renter. This // iteration is complete. If the finalIter flag is set, StopResponse will // be sent instead. This indicates to the renter that the host wishes to // terminate the revision loop. if finalIter { err = modules.WriteNegotiationStop(conn) } else { err = modules.WriteNegotiationAcceptance(conn) } if err != nil { return err } return encoding.WriteObject(conn, txn.TransactionSignatures[1]) }
// Renew negotiates a new contract for data already stored with a host, and // submits the new contract transaction to tpool. func Renew(contract modules.RenterContract, params ContractParams, txnBuilder transactionBuilder, tpool transactionPool) (modules.RenterContract, error) { // extract vars from params, for convenience host, filesize, startHeight, endHeight, refundAddress := params.Host, params.Filesize, params.StartHeight, params.EndHeight, params.RefundAddress ourSK := contract.SecretKey // calculate cost to renter and cost to host storageAllocation := host.StoragePrice.Mul64(filesize).Mul64(uint64(endHeight - startHeight)) hostCollateral := host.Collateral.Mul64(filesize).Mul64(uint64(endHeight - startHeight)) if hostCollateral.Cmp(host.MaxCollateral) > 0 { // TODO: if we have to cap the collateral, it probably means we shouldn't be using this host // (ok within a factor of 2) hostCollateral = host.MaxCollateral } // Calculate additional basePrice and baseCollateral. If the contract // height did not increase, basePrice and baseCollateral are zero. var basePrice, baseCollateral types.Currency if endHeight+host.WindowSize > contract.LastRevision.NewWindowEnd { timeExtension := uint64((endHeight + host.WindowSize) - contract.LastRevision.NewWindowEnd) basePrice = host.StoragePrice.Mul64(contract.LastRevision.NewFileSize).Mul64(timeExtension) // cost of data already covered by contract, i.e. lastrevision.Filesize baseCollateral = host.Collateral.Mul64(contract.LastRevision.NewFileSize).Mul64(timeExtension) // same but collateral } hostPayout := hostCollateral.Add(host.ContractPrice).Add(basePrice) payout := storageAllocation.Add(hostCollateral.Add(host.ContractPrice)).Mul64(10406).Div64(10000) // renter covers siafund fee renterCost := payout.Sub(hostCollateral) // check for negative currency if types.PostTax(startHeight, payout).Cmp(hostPayout) < 0 { return modules.RenterContract{}, errors.New("payout smaller than host payout") } else if hostCollateral.Cmp(baseCollateral) < 0 { return modules.RenterContract{}, errors.New("new collateral smaller than old collateral") } // create file contract fc := types.FileContract{ FileSize: contract.LastRevision.NewFileSize, FileMerkleRoot: contract.LastRevision.NewFileMerkleRoot, WindowStart: endHeight, WindowEnd: endHeight + host.WindowSize, Payout: payout, UnlockHash: contract.LastRevision.NewUnlockHash, RevisionNumber: 0, ValidProofOutputs: []types.SiacoinOutput{ // renter {Value: types.PostTax(startHeight, payout).Sub(hostPayout), UnlockHash: refundAddress}, // host {Value: hostPayout, UnlockHash: host.UnlockHash}, }, MissedProofOutputs: []types.SiacoinOutput{ // renter {Value: types.PostTax(startHeight, payout).Sub(hostPayout), UnlockHash: refundAddress}, // host gets its unused collateral back, plus the contract price {Value: hostCollateral.Sub(baseCollateral).Add(host.ContractPrice), UnlockHash: host.UnlockHash}, // void gets the spent storage fees, plus the collateral being risked {Value: basePrice.Add(baseCollateral), UnlockHash: types.UnlockHash{}}, }, } // calculate transaction fee _, maxFee := tpool.FeeEstimation() fee := maxFee.Mul64(estTxnSize) // build transaction containing fc err := txnBuilder.FundSiacoins(renterCost.Add(fee)) if err != nil { return modules.RenterContract{}, err } txnBuilder.AddFileContract(fc) // add miner fee txnBuilder.AddMinerFee(fee) // create initial transaction set txn, parentTxns := txnBuilder.View() txnSet := append(parentTxns, txn) // initiate connection conn, err := net.DialTimeout("tcp", string(host.NetAddress), 15*time.Second) if err != nil { return modules.RenterContract{}, err } defer func() { _ = conn.Close() }() // allot time for sending RPC ID, verifyRecentRevision, and verifySettings extendDeadline(conn, modules.NegotiateRecentRevisionTime+modules.NegotiateSettingsTime) if err = encoding.WriteObject(conn, modules.RPCRenewContract); err != nil { return modules.RenterContract{}, errors.New("couldn't initiate RPC: " + err.Error()) } // verify that both parties are renewing the same contract if err = verifyRecentRevision(conn, contract); err != nil { return modules.RenterContract{}, errors.New("revision exchange failed: " + err.Error()) } // verify the host's settings and confirm its identity host, err = verifySettings(conn, host) if err != nil { return modules.RenterContract{}, errors.New("settings exchange failed: " + err.Error()) } if !host.AcceptingContracts { return modules.RenterContract{}, errors.New("host is not accepting contracts") } // allot time for negotiation extendDeadline(conn, modules.NegotiateRenewContractTime) // send acceptance, txn signed by us, and pubkey if err = modules.WriteNegotiationAcceptance(conn); err != nil { return modules.RenterContract{}, errors.New("couldn't send initial acceptance: " + err.Error()) } if err = encoding.WriteObject(conn, txnSet); err != nil { return modules.RenterContract{}, errors.New("couldn't send the contract signed by us: " + err.Error()) } if err = encoding.WriteObject(conn, ourSK.PublicKey()); err != nil { return modules.RenterContract{}, errors.New("couldn't send our public key: " + err.Error()) } // read acceptance and txn signed by host if err = modules.ReadNegotiationAcceptance(conn); err != nil { return modules.RenterContract{}, errors.New("host did not accept our proposed contract: " + err.Error()) } // host now sends any new parent transactions, inputs and outputs that // were added to the transaction var newParents []types.Transaction var newInputs []types.SiacoinInput var newOutputs []types.SiacoinOutput if err = encoding.ReadObject(conn, &newParents, types.BlockSizeLimit); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's added parents: " + err.Error()) } if err = encoding.ReadObject(conn, &newInputs, types.BlockSizeLimit); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's added inputs: " + err.Error()) } if err = encoding.ReadObject(conn, &newOutputs, types.BlockSizeLimit); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's added outputs: " + err.Error()) } // merge txnAdditions with txnSet txnBuilder.AddParents(newParents) for _, input := range newInputs { txnBuilder.AddSiacoinInput(input) } for _, output := range newOutputs { txnBuilder.AddSiacoinOutput(output) } // sign the txn signedTxnSet, err := txnBuilder.Sign(true) if err != nil { return modules.RenterContract{}, modules.WriteNegotiationRejection(conn, errors.New("failed to sign transaction: "+err.Error())) } // calculate signatures added by the transaction builder var addedSignatures []types.TransactionSignature _, _, _, addedSignatureIndices := txnBuilder.ViewAdded() for _, i := range addedSignatureIndices { addedSignatures = append(addedSignatures, signedTxnSet[len(signedTxnSet)-1].TransactionSignatures[i]) } // create initial (no-op) revision, transaction, and signature initRevision := types.FileContractRevision{ ParentID: signedTxnSet[len(signedTxnSet)-1].FileContractID(0), UnlockConditions: contract.LastRevision.UnlockConditions, NewRevisionNumber: 1, NewFileSize: fc.FileSize, NewFileMerkleRoot: fc.FileMerkleRoot, NewWindowStart: fc.WindowStart, NewWindowEnd: fc.WindowEnd, NewValidProofOutputs: fc.ValidProofOutputs, NewMissedProofOutputs: fc.MissedProofOutputs, NewUnlockHash: fc.UnlockHash, } renterRevisionSig := types.TransactionSignature{ ParentID: crypto.Hash(initRevision.ParentID), PublicKeyIndex: 0, CoveredFields: types.CoveredFields{ FileContractRevisions: []uint64{0}, }, } revisionTxn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{initRevision}, TransactionSignatures: []types.TransactionSignature{renterRevisionSig}, } encodedSig, err := crypto.SignHash(revisionTxn.SigHash(0), ourSK) if err != nil { return modules.RenterContract{}, modules.WriteNegotiationRejection(conn, errors.New("failed to sign revision transaction: "+err.Error())) } revisionTxn.TransactionSignatures[0].Signature = encodedSig[:] // Send acceptance and signatures if err = modules.WriteNegotiationAcceptance(conn); err != nil { return modules.RenterContract{}, errors.New("couldn't send transaction acceptance: " + err.Error()) } if err = encoding.WriteObject(conn, addedSignatures); err != nil { return modules.RenterContract{}, errors.New("couldn't send added signatures: " + err.Error()) } if err = encoding.WriteObject(conn, revisionTxn.TransactionSignatures[0]); err != nil { return modules.RenterContract{}, errors.New("couldn't send revision signature: " + err.Error()) } // Read the host acceptance and signatures. err = modules.ReadNegotiationAcceptance(conn) if err != nil { return modules.RenterContract{}, errors.New("host did not accept our signatures: " + err.Error()) } var hostSigs []types.TransactionSignature if err = encoding.ReadObject(conn, &hostSigs, 2e3); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's signatures: " + err.Error()) } for _, sig := range hostSigs { txnBuilder.AddTransactionSignature(sig) } var hostRevisionSig types.TransactionSignature if err = encoding.ReadObject(conn, &hostRevisionSig, 2e3); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's revision signature: " + err.Error()) } revisionTxn.TransactionSignatures = append(revisionTxn.TransactionSignatures, hostRevisionSig) // Construct the final transaction. txn, parentTxns = txnBuilder.View() txnSet = append(parentTxns, txn) // Submit to blockchain. err = tpool.AcceptTransactionSet(txnSet) if err == modules.ErrDuplicateTransactionSet { // as long as it made it into the transaction pool, we're good err = nil } if err != nil { return modules.RenterContract{}, err } // calculate contract ID fcid := txn.FileContractID(0) return modules.RenterContract{ FileContract: fc, ID: fcid, LastRevision: initRevision, LastRevisionTxn: revisionTxn, MerkleRoots: contract.MerkleRoots, NetAddress: host.NetAddress, SecretKey: ourSK, }, nil }
// managedRenewContract accepts a request to renew a file contract. func (h *Host) managedRPCRenewContract(conn net.Conn) error { // Perform the recent revision protocol to get the file contract being // revised. _, so, err := h.managedRPCRecentRevision(conn) if err != nil { return extendErr("RPCRecentRevision failed: ", err) } // The storage obligation is received with a lock. Defer a call to unlock // the storage obligation. defer func() { h.managedUnlockStorageObligation(so.id()) }() // Perform the host settings exchange with the renter. err = h.managedRPCSettings(conn) if err != nil { return extendErr("RPCSettings failed: ", err) } // Set the renewal deadline. conn.SetDeadline(time.Now().Add(modules.NegotiateRenewContractTime)) // The renter will either accept or reject the host's settings. err = modules.ReadNegotiationAcceptance(conn) if err != nil { return extendErr("renter rejected the host settings: ", ErrorCommunication(err.Error())) } // If the renter sends an acceptance of the settings, it will be followed // by an unsigned transaction containing funding from the renter and a file // contract which matches what the final file contract should look like. // After the file contract, the renter will send a public key which is the // renter's public key in the unlock conditions that protect the file // contract from revision. var txnSet []types.Transaction var renterPK crypto.PublicKey err = encoding.ReadObject(conn, &txnSet, modules.NegotiateMaxFileContractSetLen) if err != nil { return extendErr("unable to read transaction set: ", ErrorConnection(err.Error())) } err = encoding.ReadObject(conn, &renterPK, modules.NegotiateMaxSiaPubkeySize) if err != nil { return extendErr("unable to read renter public key: ", ErrorConnection(err.Error())) } h.mu.RLock() settings := h.externalSettings() h.mu.RUnlock() // Verify that the transaction coming over the wire is a proper renewal. err = h.managedVerifyRenewedContract(so, txnSet, renterPK) if err != nil { modules.WriteNegotiationRejection(conn, err) // Error is ignored to preserve type for extendErr return extendErr("verification of renewal failed: ", err) } txnBuilder, newParents, newInputs, newOutputs, err := h.managedAddRenewCollateral(so, settings, txnSet) if err != nil { modules.WriteNegotiationRejection(conn, err) // Error is ignored to preserve type for extendErr return extendErr("failed to add collateral: ", err) } // The host indicates acceptance, then sends the new parents, inputs, and // outputs to the transaction. err = modules.WriteNegotiationAcceptance(conn) if err != nil { return extendErr("failed to write acceptance: ", ErrorConnection(err.Error())) } err = encoding.WriteObject(conn, newParents) if err != nil { return extendErr("failed to write new parents: ", ErrorConnection(err.Error())) } err = encoding.WriteObject(conn, newInputs) if err != nil { return extendErr("failed to write new inputs: ", ErrorConnection(err.Error())) } err = encoding.WriteObject(conn, newOutputs) if err != nil { return extendErr("failed to write new outputs: ", ErrorConnection(err.Error())) } // The renter will send a negotiation response, followed by transaction // signatures for the file contract transaction in the case of acceptance. // The transaction signatures will be followed by another transaction // signature to sign the no-op file contract revision associated with the // new file contract. err = modules.ReadNegotiationAcceptance(conn) if err != nil { return extendErr("renter rejected collateral extension: ", ErrorCommunication(err.Error())) } var renterTxnSignatures []types.TransactionSignature var renterRevisionSignature types.TransactionSignature err = encoding.ReadObject(conn, &renterTxnSignatures, modules.NegotiateMaxTransactionSignatureSize) if err != nil { return extendErr("failed to read renter transaction signatures: ", ErrorConnection(err.Error())) } err = encoding.ReadObject(conn, &renterRevisionSignature, modules.NegotiateMaxTransactionSignatureSize) if err != nil { return extendErr("failed to read renter revision signatures: ", ErrorConnection(err.Error())) } // The host adds the renter transaction signatures, then signs the // transaction and submits it to the blockchain, creating a storage // obligation in the process. The host's part is now complete and the // contract is finalized, but to give confidence to the renter the host // will send the sigantures so that the renter can immediately have the // completed file contract. // // During finalization the signatures sent by the renter are all checked. h.mu.RLock() fc := txnSet[len(txnSet)-1].FileContracts[0] renewCollateral := renewContractCollateral(so, settings, fc) renewRevenue := renewBasePrice(so, settings, fc) renewRisk := renewBaseCollateral(so, settings, fc) h.mu.RUnlock() hostTxnSignatures, hostRevisionSignature, newSOID, err := h.managedFinalizeContract(txnBuilder, renterPK, renterTxnSignatures, renterRevisionSignature, so.SectorRoots, renewCollateral, renewRevenue, renewRisk) if err != nil { modules.WriteNegotiationRejection(conn, err) // Error is ignored to preserve type for extendErr return extendErr("failed to finalize contract: ", err) } defer h.managedUnlockStorageObligation(newSOID) err = modules.WriteNegotiationAcceptance(conn) if err != nil { return extendErr("failed to write acceptance: ", ErrorConnection(err.Error())) } // The host sends the transaction signatures to the renter, followed by the // revision signature. Negotiation is complete. err = encoding.WriteObject(conn, hostTxnSignatures) if err != nil { return extendErr("failed to write transaction signatures: ", ErrorConnection(err.Error())) } err = encoding.WriteObject(conn, hostRevisionSignature) if err != nil { return extendErr("failed to write revision signature: ", ErrorConnection(err.Error())) } return nil }
// managedRenewContract accepts a request to renew a file contract. func (h *Host) managedRPCRenewContract(conn net.Conn) error { // Perform the recent revision protocol to get the file contract being // revised. _, so, err := h.managedRPCRecentRevision(conn) if err != nil { return err } // Lock the storage obligation for the remainder of the connection. h.mu.Lock() err = h.lockStorageObligation(so) h.mu.Unlock() if err != nil { return err } defer func() { h.mu.Lock() err = h.unlockStorageObligation(so) h.mu.Unlock() if err != nil { h.log.Critical(err) } }() // Perform the host settings exchange with the renter. err = h.managedRPCSettings(conn) if err != nil { return err } // Set the renewal deadline. conn.SetDeadline(time.Now().Add(modules.NegotiateRenewContractTime)) // The renter will either accept or reject the host's settings. err = modules.ReadNegotiationAcceptance(conn) if err != nil { return err } // If the renter sends an acceptance of the settings, it will be followed // by an unsigned transaction containing funding from the renter and a file // contract which matches what the final file contract should look like. // After the file contract, the renter will send a public key which is the // renter's public key in the unlock conditions that protect the file // contract from revision. var txnSet []types.Transaction var renterPK crypto.PublicKey err = encoding.ReadObject(conn, &txnSet, modules.NegotiateMaxFileContractSetLen) if err != nil { return err } err = encoding.ReadObject(conn, &renterPK, modules.NegotiateMaxSiaPubkeySize) if err != nil { return err } h.mu.RLock() settings := h.externalSettings() h.mu.RUnlock() // Verify that the transaction coming over the wire is a proper renewal. err = h.managedVerifyRenewedContract(so, txnSet, renterPK) if err != nil { return modules.WriteNegotiationRejection(conn, err) } txnBuilder, newParents, newInputs, newOutputs, err := h.managedAddRenewCollateral(so, settings, txnSet) if err != nil { return modules.WriteNegotiationRejection(conn, err) } // The host indicates acceptance, then sends the new parents, inputs, and // outputs to the transaction. err = modules.WriteNegotiationAcceptance(conn) if err != nil { return err } err = encoding.WriteObject(conn, newParents) if err != nil { return err } err = encoding.WriteObject(conn, newInputs) if err != nil { return err } err = encoding.WriteObject(conn, newOutputs) if err != nil { return err } // The renter will send a negotiation response, followed by transaction // signatures for the file contract transaction in the case of acceptance. // The transaction signatures will be followed by another transaction // signature to sign the no-op file contract revision associated with the // new file contract. err = modules.ReadNegotiationAcceptance(conn) if err != nil { return err } var renterTxnSignatures []types.TransactionSignature var renterRevisionSignature types.TransactionSignature err = encoding.ReadObject(conn, &renterTxnSignatures, modules.NegotiateMaxTransactionSignatureSize) if err != nil { return err } err = encoding.ReadObject(conn, &renterRevisionSignature, modules.NegotiateMaxTransactionSignatureSize) if err != nil { return err } // The host adds the renter transaction signatures, then signs the // transaction and submits it to the blockchain, creating a storage // obligation in the process. The host's part is now complete and the // contract is finalized, but to give confidence to the renter the host // will send the sigantures so that the renter can immediately have the // completed file contract. // // During finalization the signatures sent by the renter are all checked. hostTxnSignatures, hostRevisionSignature, err := h.managedFinalizeContract(txnBuilder, renterPK, renterTxnSignatures, renterRevisionSignature) if err != nil { return modules.WriteNegotiationRejection(conn, err) } err = modules.WriteNegotiationAcceptance(conn) if err != nil { return err } // The host sends the transaction signatures to the renter, followed by the // revision signature. Negotiation is complete. err = encoding.WriteObject(conn, hostTxnSignatures) if err != nil { return err } return encoding.WriteObject(conn, hostRevisionSignature) }
// managedRPCFormContract accepts a file contract from a renter, checks the // file contract for compliance with the host settings, and then commits to the // file contract, creating a storage obligation and submitting the contract to // the blockchain. func (h *Host) managedRPCFormContract(conn net.Conn) error { // Send the host settings to the renter. err := h.managedRPCSettings(conn) if err != nil { return extendErr("failed RPCSettings: ", err) } // If the host is not accepting contracts, the connection can be closed. // The renter has been given enough information in the host settings to // understand that the connection is going to be closed. h.mu.RLock() settings := h.settings h.mu.RUnlock() if !settings.AcceptingContracts { h.log.Debugln("Turning down contract because the host is not accepting contracts.") return nil } // Extend the deadline to meet the rest of file contract negotiation. conn.SetDeadline(time.Now().Add(modules.NegotiateFileContractTime)) // The renter will either accept or reject the host's settings. err = modules.ReadNegotiationAcceptance(conn) if err != nil { return extendErr("renter did not accept settings: ", ErrorCommunication(err.Error())) } // If the renter sends an acceptance of the settings, it will be followed // by an unsigned transaction containing funding from the renter and a file // contract which matches what the final file contract should look like. // After the file contract, the renter will send a public key which is the // renter's public key in the unlock conditions that protect the file // contract from revision. var txnSet []types.Transaction var renterPK crypto.PublicKey err = encoding.ReadObject(conn, &txnSet, modules.NegotiateMaxFileContractSetLen) if err != nil { return extendErr("could not read renter transaction set: ", ErrorConnection(err.Error())) } err = encoding.ReadObject(conn, &renterPK, modules.NegotiateMaxSiaPubkeySize) if err != nil { return extendErr("could not read renter public key: ", ErrorConnection(err.Error())) } // The host verifies that the file contract coming over the wire is // acceptable. err = h.managedVerifyNewContract(txnSet, renterPK) if err != nil { // The incoming file contract is not acceptable to the host, indicate // why to the renter. modules.WriteNegotiationRejection(conn, err) // Error ignored to preserve type in extendErr return extendErr("contract verification failed: ", err) } // The host adds collateral to the transaction. txnBuilder, newParents, newInputs, newOutputs, err := h.managedAddCollateral(settings, txnSet) if err != nil { modules.WriteNegotiationRejection(conn, err) // Error ignored to preserve type in extendErr return extendErr("failed to add collateral: ", err) } // The host indicates acceptance, and then sends any new parent // transactions, inputs and outputs that were added to the transaction. err = modules.WriteNegotiationAcceptance(conn) if err != nil { return extendErr("accepting verified contract failed: ", ErrorConnection(err.Error())) } err = encoding.WriteObject(conn, newParents) if err != nil { return extendErr("failed to write new parents: ", ErrorConnection(err.Error())) } err = encoding.WriteObject(conn, newInputs) if err != nil { return extendErr("failed to write new inputs: ", ErrorConnection(err.Error())) } err = encoding.WriteObject(conn, newOutputs) if err != nil { return extendErr("failed to write new outputs: ", ErrorConnection(err.Error())) } // The renter will now send a negotiation response, followed by transaction // signatures for the file contract transaction in the case of acceptance. // The transaction signatures will be followed by another transaction // siganture, to sign a no-op file contract revision. err = modules.ReadNegotiationAcceptance(conn) if err != nil { return extendErr("renter did not accept updated transactions: ", ErrorCommunication(err.Error())) } var renterTxnSignatures []types.TransactionSignature var renterRevisionSignature types.TransactionSignature err = encoding.ReadObject(conn, &renterTxnSignatures, modules.NegotiateMaxTransactionSignaturesSize) if err != nil { return extendErr("could not read renter transaction signatures: ", ErrorConnection(err.Error())) } err = encoding.ReadObject(conn, &renterRevisionSignature, modules.NegotiateMaxTransactionSignatureSize) if err != nil { return extendErr("could not read renter revision signatures: ", ErrorConnection(err.Error())) } // The host adds the renter transaction signatures, then signs the // transaction and submits it to the blockchain, creating a storage // obligation in the process. The host's part is done before anything is // written to the renter, but to give the renter confidence, the host will // send the signatures so that the renter can immediately have the // completed file contract. // // During finalization, the siganture for the revision is also checked, and // signatures for the revision transaction are created. h.mu.RLock() hostCollateral := contractCollateral(h.settings, txnSet[len(txnSet)-1].FileContracts[0]) h.mu.RUnlock() hostTxnSignatures, hostRevisionSignature, newSOID, err := h.managedFinalizeContract(txnBuilder, renterPK, renterTxnSignatures, renterRevisionSignature, nil, hostCollateral, types.ZeroCurrency, types.ZeroCurrency) if err != nil { // The incoming file contract is not acceptable to the host, indicate // why to the renter. modules.WriteNegotiationRejection(conn, err) // Error ignored to preserve type in extendErr return extendErr("contract finalization failed: ", err) } defer h.managedUnlockStorageObligation(newSOID) err = modules.WriteNegotiationAcceptance(conn) if err != nil { return extendErr("failed to write acceptance after contract finalization: ", ErrorConnection(err.Error())) } // The host sends the transaction signatures to the renter, followed by the // revision signature. Negotiation is complete. err = encoding.WriteObject(conn, hostTxnSignatures) if err != nil { return extendErr("failed to write host transaction signatures: ", ErrorConnection(err.Error())) } err = encoding.WriteObject(conn, hostRevisionSignature) if err != nil { return extendErr("failed to write host revision signatures: ", ErrorConnection(err.Error())) } return nil }
// FormContract forms a contract with a host and submits the contract // transaction to tpool. func FormContract(params ContractParams, txnBuilder transactionBuilder, tpool transactionPool) (modules.RenterContract, error) { // extract vars from params, for convenience host, filesize, startHeight, endHeight, refundAddress := params.Host, params.Filesize, params.StartHeight, params.EndHeight, params.RefundAddress // create our key ourSK, ourPK, err := crypto.GenerateKeyPair() if err != nil { return modules.RenterContract{}, err } ourPublicKey := types.SiaPublicKey{ Algorithm: types.SignatureEd25519, Key: ourPK[:], } // create unlock conditions uc := types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{ourPublicKey, host.PublicKey}, SignaturesRequired: 2, } // calculate cost to renter and cost to host // TODO: clarify/abstract this math storageAllocation := host.StoragePrice.Mul64(filesize).Mul64(uint64(endHeight - startHeight)) hostCollateral := host.Collateral.Mul64(filesize).Mul64(uint64(endHeight - startHeight)) if hostCollateral.Cmp(host.MaxCollateral) > 0 { // TODO: if we have to cap the collateral, it probably means we shouldn't be using this host // (ok within a factor of 2) hostCollateral = host.MaxCollateral } hostPayout := hostCollateral.Add(host.ContractPrice) payout := storageAllocation.Add(hostPayout).Mul64(10406).Div64(10000) // renter pays for siafund fee renterCost := payout.Sub(hostCollateral) // check for negative currency if types.PostTax(startHeight, payout).Cmp(hostPayout) < 0 { return modules.RenterContract{}, errors.New("payout smaller than host payout") } // create file contract fc := types.FileContract{ FileSize: 0, FileMerkleRoot: crypto.Hash{}, // no proof possible without data WindowStart: endHeight, WindowEnd: endHeight + host.WindowSize, Payout: payout, UnlockHash: uc.UnlockHash(), RevisionNumber: 0, ValidProofOutputs: []types.SiacoinOutput{ // outputs need to account for tax {Value: types.PostTax(startHeight, payout).Sub(hostPayout), UnlockHash: refundAddress}, // collateral is returned to host {Value: hostPayout, UnlockHash: host.UnlockHash}, }, MissedProofOutputs: []types.SiacoinOutput{ // same as above {Value: types.PostTax(startHeight, payout).Sub(hostPayout), UnlockHash: refundAddress}, // same as above {Value: hostPayout, UnlockHash: host.UnlockHash}, // once we start doing revisions, we'll move some coins to the host and some to the void {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, }, } // calculate transaction fee _, maxFee := tpool.FeeEstimation() fee := maxFee.Mul64(estTxnSize) // build transaction containing fc err = txnBuilder.FundSiacoins(renterCost.Add(fee)) if err != nil { return modules.RenterContract{}, err } txnBuilder.AddFileContract(fc) // add miner fee txnBuilder.AddMinerFee(fee) // create initial transaction set txn, parentTxns := txnBuilder.View() txnSet := append(parentTxns, txn) // initiate connection conn, err := net.DialTimeout("tcp", string(host.NetAddress), 15*time.Second) if err != nil { return modules.RenterContract{}, err } defer func() { _ = conn.Close() }() // allot time for sending RPC ID + verifySettings extendDeadline(conn, modules.NegotiateSettingsTime) if err = encoding.WriteObject(conn, modules.RPCFormContract); err != nil { return modules.RenterContract{}, err } // verify the host's settings and confirm its identity host, err = verifySettings(conn, host) if err != nil { return modules.RenterContract{}, err } if !host.AcceptingContracts { return modules.RenterContract{}, errors.New("host is not accepting contracts") } // allot time for negotiation extendDeadline(conn, modules.NegotiateFileContractTime) // send acceptance, txn signed by us, and pubkey if err = modules.WriteNegotiationAcceptance(conn); err != nil { return modules.RenterContract{}, errors.New("couldn't send initial acceptance: " + err.Error()) } if err = encoding.WriteObject(conn, txnSet); err != nil { return modules.RenterContract{}, errors.New("couldn't send the contract signed by us: " + err.Error()) } if err = encoding.WriteObject(conn, ourSK.PublicKey()); err != nil { return modules.RenterContract{}, errors.New("couldn't send our public key: " + err.Error()) } // read acceptance and txn signed by host if err = modules.ReadNegotiationAcceptance(conn); err != nil { return modules.RenterContract{}, errors.New("host did not accept our proposed contract: " + err.Error()) } // host now sends any new parent transactions, inputs and outputs that // were added to the transaction var newParents []types.Transaction var newInputs []types.SiacoinInput var newOutputs []types.SiacoinOutput if err = encoding.ReadObject(conn, &newParents, types.BlockSizeLimit); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's added parents: " + err.Error()) } if err = encoding.ReadObject(conn, &newInputs, types.BlockSizeLimit); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's added inputs: " + err.Error()) } if err = encoding.ReadObject(conn, &newOutputs, types.BlockSizeLimit); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's added outputs: " + err.Error()) } // merge txnAdditions with txnSet txnBuilder.AddParents(newParents) for _, input := range newInputs { txnBuilder.AddSiacoinInput(input) } for _, output := range newOutputs { txnBuilder.AddSiacoinOutput(output) } // sign the txn signedTxnSet, err := txnBuilder.Sign(true) if err != nil { return modules.RenterContract{}, modules.WriteNegotiationRejection(conn, errors.New("failed to sign transaction: "+err.Error())) } // calculate signatures added by the transaction builder var addedSignatures []types.TransactionSignature _, _, _, addedSignatureIndices := txnBuilder.ViewAdded() for _, i := range addedSignatureIndices { addedSignatures = append(addedSignatures, signedTxnSet[len(signedTxnSet)-1].TransactionSignatures[i]) } // create initial (no-op) revision, transaction, and signature initRevision := types.FileContractRevision{ ParentID: signedTxnSet[len(signedTxnSet)-1].FileContractID(0), UnlockConditions: uc, NewRevisionNumber: 1, NewFileSize: fc.FileSize, NewFileMerkleRoot: fc.FileMerkleRoot, NewWindowStart: fc.WindowStart, NewWindowEnd: fc.WindowEnd, NewValidProofOutputs: fc.ValidProofOutputs, NewMissedProofOutputs: fc.MissedProofOutputs, NewUnlockHash: fc.UnlockHash, } renterRevisionSig := types.TransactionSignature{ ParentID: crypto.Hash(initRevision.ParentID), PublicKeyIndex: 0, CoveredFields: types.CoveredFields{ FileContractRevisions: []uint64{0}, }, } revisionTxn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{initRevision}, TransactionSignatures: []types.TransactionSignature{renterRevisionSig}, } encodedSig, err := crypto.SignHash(revisionTxn.SigHash(0), ourSK) if err != nil { return modules.RenterContract{}, modules.WriteNegotiationRejection(conn, errors.New("failed to sign revision transaction: "+err.Error())) } revisionTxn.TransactionSignatures[0].Signature = encodedSig[:] // Send acceptance and signatures if err = modules.WriteNegotiationAcceptance(conn); err != nil { return modules.RenterContract{}, errors.New("couldn't send transaction acceptance: " + err.Error()) } if err = encoding.WriteObject(conn, addedSignatures); err != nil { return modules.RenterContract{}, errors.New("couldn't send added signatures: " + err.Error()) } if err = encoding.WriteObject(conn, revisionTxn.TransactionSignatures[0]); err != nil { return modules.RenterContract{}, errors.New("couldn't send revision signature: " + err.Error()) } // Read the host acceptance and signatures. err = modules.ReadNegotiationAcceptance(conn) if err != nil { return modules.RenterContract{}, errors.New("host did not accept our signatures: " + err.Error()) } var hostSigs []types.TransactionSignature if err = encoding.ReadObject(conn, &hostSigs, 2e3); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's signatures: " + err.Error()) } for _, sig := range hostSigs { txnBuilder.AddTransactionSignature(sig) } var hostRevisionSig types.TransactionSignature if err = encoding.ReadObject(conn, &hostRevisionSig, 2e3); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's revision signature: " + err.Error()) } revisionTxn.TransactionSignatures = append(revisionTxn.TransactionSignatures, hostRevisionSig) // Construct the final transaction. txn, parentTxns = txnBuilder.View() txnSet = append(parentTxns, txn) // Submit to blockchain. err = tpool.AcceptTransactionSet(txnSet) if err == modules.ErrDuplicateTransactionSet { // as long as it made it into the transaction pool, we're good err = nil } if err != nil { return modules.RenterContract{}, err } // calculate contract ID fcid := txn.FileContractID(0) return modules.RenterContract{ FileContract: fc, ID: fcid, LastRevision: initRevision, LastRevisionTxn: revisionTxn, NetAddress: host.NetAddress, SecretKey: ourSK, }, nil }
// managedDownloadIteration is responsible for managing a single iteration of // the download loop for RPCDownload. func (h *Host) managedDownloadIteration(conn net.Conn, so *storageObligation) error { // Exchange settings with the renter. err := h.managedRPCSettings(conn) if err != nil { return extendErr("RPCSettings failed: ", err) } // Extend the deadline for the download. conn.SetDeadline(time.Now().Add(modules.NegotiateDownloadTime)) // The renter will either accept or reject the host's settings. err = modules.ReadNegotiationAcceptance(conn) if err != nil { return extendErr("renter rejected host settings: ", ErrorCommunication(err.Error())) } // Grab a set of variables that will be useful later in the function. h.mu.RLock() blockHeight := h.blockHeight secretKey := h.secretKey settings := h.settings h.mu.RUnlock() // Read the download requests, followed by the file contract revision that // pays for them. var requests []modules.DownloadAction var paymentRevision types.FileContractRevision err = encoding.ReadObject(conn, &requests, modules.NegotiateMaxDownloadActionRequestSize) if err != nil { return extendErr("failed to read download requests:", ErrorConnection(err.Error())) } err = encoding.ReadObject(conn, &paymentRevision, modules.NegotiateMaxFileContractRevisionSize) if err != nil { return extendErr("failed to read payment revision:", ErrorConnection(err.Error())) } // Verify that the request is acceptable, and then fetch all of the data // for the renter. existingRevision := so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0] var payload [][]byte err = func() error { // Check that the length of each file is in-bounds, and that the total // size being requested is acceptable. var totalSize uint64 for _, request := range requests { if request.Length > modules.SectorSize || request.Offset+request.Length > modules.SectorSize { return extendErr("download iteration request failed: ", errRequestOutOfBounds) } totalSize += request.Length } if totalSize > settings.MaxDownloadBatchSize { return extendErr("download iteration batch failed: ", errLargeDownloadBatch) } // Verify that the correct amount of money has been moved from the // renter's contract funds to the host's contract funds. expectedTransfer := settings.MinDownloadBandwidthPrice.Mul64(totalSize) err = verifyPaymentRevision(existingRevision, paymentRevision, blockHeight, expectedTransfer) if err != nil { return extendErr("payment verification failed: ", err) } // Load the sectors and build the data payload. for _, request := range requests { sectorData, err := h.ReadSector(request.MerkleRoot) if err != nil { return extendErr("failed to load sector: ", ErrorInternal(err.Error())) } payload = append(payload, sectorData[request.Offset:request.Offset+request.Length]) } return nil }() if err != nil { modules.WriteNegotiationRejection(conn, err) // Error not reported to preserve type in extendErr return extendErr("download request rejected: ", err) } // Revision is acceptable, write acceptance. err = modules.WriteNegotiationAcceptance(conn) if err != nil { return extendErr("failed to write acceptance for renter revision: ", ErrorConnection(err.Error())) } // Renter will send a transaction siganture for the file contract revision. var renterSignature types.TransactionSignature err = encoding.ReadObject(conn, &renterSignature, modules.NegotiateMaxTransactionSignatureSize) if err != nil { return extendErr("failed to read renter signature: ", ErrorConnection(err.Error())) } txn, err := createRevisionSignature(paymentRevision, renterSignature, secretKey, blockHeight) // Update the storage obligation. paymentTransfer := existingRevision.NewValidProofOutputs[0].Value.Sub(paymentRevision.NewValidProofOutputs[0].Value) so.PotentialDownloadRevenue = so.PotentialDownloadRevenue.Add(paymentTransfer) so.RevisionTransactionSet = []types.Transaction{{ FileContractRevisions: []types.FileContractRevision{paymentRevision}, TransactionSignatures: []types.TransactionSignature{renterSignature, txn.TransactionSignatures[1]}, }} err = h.modifyStorageObligation(*so, nil, nil, nil) if err != nil { return extendErr("failed to modify storage obligation: ", ErrorInternal(modules.WriteNegotiationRejection(conn, err).Error())) } // Write acceptance to the renter - the data request can be fulfilled by // the host, the payment is satisfactory, signature is correct. Then send // the host signature and all of the data. err = modules.WriteNegotiationAcceptance(conn) if err != nil { return extendErr("failed to write acceptance following obligation modification: ", ErrorConnection(err.Error())) } err = encoding.WriteObject(conn, txn.TransactionSignatures[1]) if err != nil { return extendErr("failed to write signature: ", ErrorConnection(err.Error())) } err = encoding.WriteObject(conn, payload) if err != nil { return extendErr("failed to write payload: ", ErrorConnection(err.Error())) } return nil }
// managedRPCRecentRevision sends the most recent known file contract // revision, including signatures, to the renter, for the file contract with // the id given by the renter. // // The storage obligation is returned under a storage obligation lock. func (h *Host) managedRPCRecentRevision(conn net.Conn) (types.FileContractID, storageObligation, error) { // Set the negotiation deadline. conn.SetDeadline(time.Now().Add(modules.NegotiateRecentRevisionTime)) // Receive the file contract id from the renter. var fcid types.FileContractID err := encoding.ReadObject(conn, &fcid, uint64(len(fcid))) if err != nil { return types.FileContractID{}, storageObligation{}, extendErr("could not read file contract id: ", ErrorConnection(err.Error())) } // Send a challenge to the renter to verify that the renter has write // access to the revision being opened. var challenge crypto.Hash _, err = rand.Read(challenge[:]) if err != nil { return types.FileContractID{}, storageObligation{}, ErrorInternal(err.Error()) } err = encoding.WriteObject(conn, challenge) if err != nil { return types.FileContractID{}, storageObligation{}, extendErr("cound not write challenge: ", ErrorConnection(err.Error())) } // Read the signed response from the renter. var challengeResponse crypto.Signature err = encoding.ReadObject(conn, &challengeResponse, uint64(len(challengeResponse))) if err != nil { return types.FileContractID{}, storageObligation{}, extendErr("could not read challenge response: ", ErrorConnection(err.Error())) } // Verify the response. In the process, fetch the related storage // obligation, file contract revision, and transaction signatures. so, recentRevision, revisionSigs, err := h.managedVerifyChallengeResponse(fcid, challenge, challengeResponse) if err != nil { modules.WriteNegotiationRejection(conn, err) // Error not reported to preserve error type in extendErr. return types.FileContractID{}, storageObligation{}, extendErr("challenge failed: ", err) } // Defer a call to unlock the storage obligation in the event of an error. defer func() { if err != nil { h.managedUnlockStorageObligation(fcid) } }() // Send the file contract revision and the corresponding signatures to the // renter. err = modules.WriteNegotiationAcceptance(conn) if err != nil { err = extendErr("failed to write challenge acceptance: ", ErrorConnection(err.Error())) return types.FileContractID{}, storageObligation{}, err } err = encoding.WriteObject(conn, recentRevision) if err != nil { err = extendErr("failed to write recent revision: ", ErrorConnection(err.Error())) return types.FileContractID{}, storageObligation{}, err } err = encoding.WriteObject(conn, revisionSigs) if err != nil { err = extendErr("failed to write recent revision singatures: ", ErrorConnection(err.Error())) return types.FileContractID{}, storageObligation{}, err } return fcid, so, nil }