Ejemplo n.º 1
0
// negotiateRevision sends a revision and actions to the host for approval,
// completing one iteration of the revision loop.
func negotiateRevision(conn net.Conn, rev types.FileContractRevision, secretKey crypto.SecretKey) (types.Transaction, error) {
	// create transaction containing the revision
	signedTxn := types.Transaction{
		FileContractRevisions: []types.FileContractRevision{rev},
		TransactionSignatures: []types.TransactionSignature{{
			ParentID:       crypto.Hash(rev.ParentID),
			CoveredFields:  types.CoveredFields{FileContractRevisions: []uint64{0}},
			PublicKeyIndex: 0, // renter key is always first -- see formContract
		}},
	}
	// sign the transaction
	encodedSig, _ := crypto.SignHash(signedTxn.SigHash(0), secretKey) // no error possible
	signedTxn.TransactionSignatures[0].Signature = encodedSig[:]

	// send the revision
	if err := encoding.WriteObject(conn, rev); err != nil {
		return types.Transaction{}, errors.New("couldn't send revision: " + err.Error())
	}
	// read acceptance
	if err := modules.ReadNegotiationAcceptance(conn); err != nil {
		return types.Transaction{}, errors.New("host did not accept revision: " + err.Error())
	}

	// send the new transaction signature
	if err := encoding.WriteObject(conn, signedTxn.TransactionSignatures[0]); err != nil {
		return types.Transaction{}, errors.New("couldn't send transaction signature: " + err.Error())
	}
	// read the host's acceptance and transaction signature
	// NOTE: if the host sends ErrStopResponse, we should continue processing
	// the revision, but return the error anyway.
	responseErr := modules.ReadNegotiationAcceptance(conn)
	if responseErr != nil && responseErr != modules.ErrStopResponse {
		return types.Transaction{}, errors.New("host did not accept transaction signature: " + responseErr.Error())
	}
	var hostSig types.TransactionSignature
	if err := encoding.ReadObject(conn, &hostSig, 16e3); err != nil {
		return types.Transaction{}, errors.New("couldn't read host's signature: " + err.Error())
	}

	// add the signature to the transaction and verify it
	// NOTE: we can fake the blockheight here because it doesn't affect
	// verification; it just needs to be above the fork height and below the
	// contract expiration (which was checked earlier).
	verificationHeight := rev.NewWindowStart - 1
	signedTxn.TransactionSignatures = append(signedTxn.TransactionSignatures, hostSig)
	if err := signedTxn.StandaloneValid(verificationHeight); err != nil {
		return types.Transaction{}, err
	}

	// if the host sent ErrStopResponse, return it
	return signedTxn, responseErr
}
Ejemplo n.º 2
0
// verifyRecentRevision confirms that the host and contractor agree upon the current
// state of the contract being revised.
func verifyRecentRevision(conn net.Conn, contract modules.RenterContract) error {
	// send contract ID
	if err := encoding.WriteObject(conn, contract.ID); err != nil {
		return errors.New("couldn't send contract ID: " + err.Error())
	}
	// read challenge
	var challenge crypto.Hash
	if err := encoding.ReadObject(conn, &challenge, 32); err != nil {
		return errors.New("couldn't read challenge: " + err.Error())
	}
	// sign and return
	sig, err := crypto.SignHash(challenge, contract.SecretKey)
	if err != nil {
		return err
	} else if err := encoding.WriteObject(conn, sig); err != nil {
		return errors.New("couldn't send challenge response: " + err.Error())
	}
	// read acceptance
	if err := modules.ReadNegotiationAcceptance(conn); err != nil {
		return errors.New("host did not accept revision request: " + err.Error())
	}
	// read last revision and signatures
	var lastRevision types.FileContractRevision
	var hostSignatures []types.TransactionSignature
	if err := encoding.ReadObject(conn, &lastRevision, 2048); err != nil {
		return errors.New("couldn't read last revision: " + err.Error())
	}
	if err := encoding.ReadObject(conn, &hostSignatures, 2048); err != nil {
		return errors.New("couldn't read host signatures: " + err.Error())
	}
	// Check that the unlock hashes match; if they do not, something is
	// seriously wrong. Otherwise, check that the revision numbers match.
	if lastRevision.UnlockConditions.UnlockHash() != contract.LastRevision.UnlockConditions.UnlockHash() {
		return errors.New("unlock conditions do not match")
	} else if lastRevision.NewRevisionNumber != contract.LastRevision.NewRevisionNumber {
		return &recentRevisionError{contract.LastRevision.NewRevisionNumber, lastRevision.NewRevisionNumber}
	}
	// NOTE: we can fake the blockheight here because it doesn't affect
	// verification; it just needs to be above the fork height and below the
	// contract expiration (which was checked earlier).
	return modules.VerifyFileContractRevisionTransactionSignatures(lastRevision, hostSignatures, contract.FileContract.WindowStart-1)
}
Ejemplo n.º 3
0
// 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])
}
Ejemplo n.º 4
0
// 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
}
Ejemplo n.º 5
0
// 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
}
Ejemplo n.º 6
0
// 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)
}
Ejemplo n.º 7
0
// 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
}
Ejemplo n.º 8
0
// 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
}
Ejemplo n.º 9
0
// 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
}