negotiate.go 12.1 KB
Newer Older
1 2 3
package host

import (
4 5
	"time"

6 7 8 9
	"gitlab.com/NebulousLabs/Sia/build"
	"gitlab.com/NebulousLabs/Sia/crypto"
	"gitlab.com/NebulousLabs/Sia/modules"
	"gitlab.com/NebulousLabs/Sia/types"
10 11
)

12
var (
13 14 15 16 17 18 19 20 21 22
	// errBadContractOutputCounts is returned if the presented file contract
	// revision has the wrong number of outputs for either the valid or the
	// missed proof outputs.
	errBadContractOutputCounts = ErrorCommunication("rejected for having an unexpected number of outputs")

	// errBadContractParent is returned when a file contract revision is
	// presented which has a parent id that doesn't match the file contract
	// which is supposed to be getting revised.
	errBadContractParent = ErrorCommunication("could not find contract's parent")

23 24 25 26 27 28 29 30
	// errBadFileMerkleRoot is returned if the renter incorrectly updates the
	// file merkle root during a file contract revision.
	errBadFileMerkleRoot = ErrorCommunication("rejected for bad file merkle root")

	// errBadFileSize is returned if the renter incorrectly download and
	// changes the file size during a file contract revision.
	errBadFileSize = ErrorCommunication("rejected for bad file size")

31 32 33
	// errBadModificationIndex is returned if the renter requests a change on a
	// sector root that is not in the file contract.
	errBadModificationIndex = ErrorCommunication("renter has made a modification that points to a nonexistent sector")
34 35 36 37 38 39 40 41 42

	// errBadParentID is returned if the renter incorrectly download and
	// provides the wrong parent id during a file contract revision.
	errBadParentID = ErrorCommunication("rejected for bad parent id")

	// errBadPayoutUnlockHashes is returned if the renter incorrectly sets the
	// payout unlock hashes during contract formation.
	errBadPayoutUnlockHashes = ErrorCommunication("rejected for bad unlock hashes in the payout")

43 44 45 46 47 48 49 50 51
	// errBadRevisionNumber number is returned if the renter incorrectly
	// download and does not increase the revision number during a file
	// contract revision.
	errBadRevisionNumber = ErrorCommunication("rejected for bad revision number")

	// errBadSectorSize is returned if the renter provides a sector to be
	// inserted that is the wrong size.
	errBadSectorSize = ErrorCommunication("renter has provided an incorrectly sized sector")

52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
	// errBadUnlockConditions is returned if the renter incorrectly download
	// and does not provide the right unlock conditions in the payment
	// revision.
	errBadUnlockConditions = ErrorCommunication("rejected for bad unlock conditions")

	// errBadUnlockHash is returned if the renter incorrectly updates the
	// unlock hash during a file contract revision.
	errBadUnlockHash = ErrorCommunication("rejected for bad new unlock hash")

	// errBadWindowEnd is returned if the renter incorrectly download and
	// changes the window end during a file contract revision.
	errBadWindowEnd = ErrorCommunication("rejected for bad new window end")

	// errBadWindowStart is returned if the renter incorrectly updates the
	// window start during a file contract revision.
	errBadWindowStart = ErrorCommunication("rejected for bad new window start")

	// errEarlyWindow is returned if the file contract provided by the renter
	// has a storage proof window that is starting too near in the future.
	errEarlyWindow = ErrorCommunication("rejected for a window that starts too soon")

	// errEmptyObject is returned if the renter sends an empty or nil object
	// unexpectedly.
	errEmptyObject = ErrorCommunication("renter has unexpectedly send an empty/nil object")

	// errHighRenterMissedOutput is returned if the renter incorrectly download
	// and deducts an insufficient amount from the renter missed outputs during
	// a file contract revision.
	errHighRenterMissedOutput = ErrorCommunication("rejected for high paying renter missed output")

	// errHighRenterValidOutput is returned if the renter incorrectly download
	// and deducts an insufficient amount from the renter valid outputs during
	// a file contract revision.
	errHighRenterValidOutput = ErrorCommunication("rejected for high paying renter valid output")

87 88 89 90 91 92 93 94
	// errIllegalOffsetAndLength is returned if the renter tries perform a
	// modify operation that uses a troublesome combination of offset and
	// length.
	errIllegalOffsetAndLength = ErrorCommunication("renter is trying to do a modify with an illegal offset and length")

	// errLargeSector is returned if the renter sends a RevisionAction that has
	// data which creates a sector that is larger than what the host uses.
	errLargeSector = ErrorCommunication("renter has sent a sector that exceeds the host's sector size")
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114

	// errLateRevision is returned if the renter is attempting to revise a
	// revision after the revision deadline. The host needs time to submit the
	// final revision to the blockchain to guarantee payment, and therefore
	// will not accept revisions once the window start is too close.
	errLateRevision = ErrorCommunication("renter is requesting revision after the revision deadline")

	// errLongDuration is returned if the renter proposes a file contract with
	// an experation that is too far into the future according to the host's
	// settings.
	errLongDuration = ErrorCommunication("renter proposed a file contract with a too-long duration")

	// errLowHostMissedOutput is returned if the renter incorrectly updates the
	// host missed proof output during a file contract revision.
	errLowHostMissedOutput = ErrorCommunication("rejected for low paying host missed output")

	// errLowHostValidOutput is returned if the renter incorrectly updates the
	// host valid proof output during a file contract revision.
	errLowHostValidOutput = ErrorCommunication("rejected for low paying host valid output")

115 116 117 118
	// errLowTransactionFees is returned if the renter provides a transaction
	// that the host does not feel is able to make it onto the blockchain.
	errLowTransactionFees = ErrorCommunication("rejected for including too few transaction fees")

119 120 121 122
	// errLowVoidOutput is returned if the renter has not allocated enough
	// funds to the void output.
	errLowVoidOutput = ErrorCommunication("rejected for low value void output")

123 124 125 126 127 128 129 130
	// errMismatchedHostPayouts is returned if the renter incorrectly sets the
	// host valid and missed payouts to different values during contract
	// formation.
	errMismatchedHostPayouts = ErrorCommunication("rejected because host valid and missed payouts are not the same value")

	// errSmallWindow is returned if the renter suggests a storage proof window
	// that is too small.
	errSmallWindow = ErrorCommunication("rejected for small window size")
131 132 133 134

	// errUnknownModification is returned if the host receives a modification
	// action from the renter that it does not understand.
	errUnknownModification = ErrorCommunication("renter is attempting an action that the host does not understand")
135 136
)

137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
// createRevisionSignature creates a signature for a file contract revision
// that signs on the file contract revision. The renter should have already
// provided the signature. createRevisionSignature will check to make sure that
// the renter's signature is valid.
func createRevisionSignature(fcr types.FileContractRevision, renterSig types.TransactionSignature, secretKey crypto.SecretKey, blockHeight types.BlockHeight) (types.Transaction, error) {
	hostSig := types.TransactionSignature{
		ParentID:       crypto.Hash(fcr.ParentID),
		PublicKeyIndex: 1,
		CoveredFields: types.CoveredFields{
			FileContractRevisions: []uint64{0},
		},
	}
	txn := types.Transaction{
		FileContractRevisions: []types.FileContractRevision{fcr},
		TransactionSignatures: []types.TransactionSignature{renterSig, hostSig},
	}
Luke Champine's avatar
Luke Champine committed
153
	sigHash := txn.SigHash(1, blockHeight)
154
	encodedSig := crypto.SignHash(sigHash, secretKey)
155
	txn.TransactionSignatures[1].Signature = encodedSig[:]
156
	err := modules.VerifyFileContractRevisionTransactionSignatures(fcr, txn.TransactionSignatures, blockHeight)
157 158 159 160 161
	if err != nil {
		return types.Transaction{}, err
	}
	return txn, nil
}
162 163 164 165 166

// managedFinalizeContract will take a file contract, add the host's
// collateral, and then try submitting the file contract to the transaction
// pool. If there is no error, the completed transaction set will be returned
// to the caller.
167
func (h *Host) managedFinalizeContract(builder modules.TransactionBuilder, renterPK crypto.PublicKey, renterSignatures []types.TransactionSignature, renterRevisionSignature types.TransactionSignature, initialSectorRoots []crypto.Hash, hostCollateral, hostInitialRevenue, hostInitialRisk types.Currency, settings modules.HostExternalSettings) ([]types.TransactionSignature, types.TransactionSignature, types.FileContractID, error) {
168 169 170 171 172 173
	for _, sig := range renterSignatures {
		builder.AddTransactionSignature(sig)
	}
	fullTxnSet, err := builder.Sign(true)
	if err != nil {
		builder.Drop()
174
		return nil, types.TransactionSignature{}, types.FileContractID{}, err
175 176 177
	}

	// Verify that the signature for the revision from the renter is correct.
178
	h.mu.RLock()
179 180 181
	blockHeight := h.blockHeight
	hostSPK := h.publicKey
	hostSK := h.secretKey
182
	h.mu.RUnlock()
183 184 185 186 187 188
	contractTxn := fullTxnSet[len(fullTxnSet)-1]
	fc := contractTxn.FileContracts[0]
	noOpRevision := types.FileContractRevision{
		ParentID: contractTxn.FileContractID(0),
		UnlockConditions: types.UnlockConditions{
			PublicKeys: []types.SiaPublicKey{
189
				types.Ed25519PublicKey(renterPK),
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
				hostSPK,
			},
			SignaturesRequired: 2,
		},
		NewRevisionNumber: fc.RevisionNumber + 1,

		NewFileSize:           fc.FileSize,
		NewFileMerkleRoot:     fc.FileMerkleRoot,
		NewWindowStart:        fc.WindowStart,
		NewWindowEnd:          fc.WindowEnd,
		NewValidProofOutputs:  fc.ValidProofOutputs,
		NewMissedProofOutputs: fc.MissedProofOutputs,
		NewUnlockHash:         fc.UnlockHash,
	}
	// createRevisionSignature will also perform validation on the result,
	// returning an error if the renter provided an incorrect signature.
	revisionTransaction, err := createRevisionSignature(noOpRevision, renterRevisionSignature, hostSK, blockHeight)
	if err != nil {
208
		return nil, types.TransactionSignature{}, types.FileContractID{}, err
209 210 211 212
	}

	// Create and add the storage obligation for this file contract.
	fullTxn, _ := builder.View()
213
	so := storageObligation{
214 215
		SectorRoots: initialSectorRoots,

216
		ContractCost:            settings.ContractPrice,
217 218 219 220
		LockedCollateral:        hostCollateral,
		PotentialStorageRevenue: hostInitialRevenue,
		RiskedCollateral:        hostInitialRisk,

221
		NegotiationHeight: blockHeight,
222

223 224 225 226 227
		OriginTransactionSet:   fullTxnSet,
		RevisionTransactionSet: []types.Transaction{revisionTransaction},
	}

	// Get a lock on the storage obligation.
David Vorick's avatar
David Vorick committed
228
	lockErr := h.managedTryLockStorageObligation(so.id())
229
	if lockErr != nil {
230 231
		build.Critical("failed to get a lock on a brand new storage obligation")
		return nil, types.TransactionSignature{}, types.FileContractID{}, lockErr
232
	}
233 234 235 236 237
	defer func() {
		if err != nil {
			h.managedUnlockStorageObligation(so.id())
		}
	}()
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253

	// addStorageObligation will submit the transaction to the transaction
	// pool, and will only do so if there was not some error in creating the
	// storage obligation. If the transaction pool returns a consensus
	// conflict, wait 30 seconds and try again.
	err = func() error {
		// Try adding the storage obligation. If there's an error, wait a few
		// seconds and try again. Eventually time out. It should be noted that
		// the storage obligation locking is both crappy and incomplete, and
		// that I'm not sure how this timeout plays with the overall host
		// timeouts.
		//
		// The storage obligation locks should occur at the highest level, not
		// just when the actual modification is happening.
		i := 0
		for {
254
			err = h.managedAddStorageObligation(so)
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
			if err == nil {
				return nil
			}
			if err != nil && i > 4 {
				h.log.Println(err)
				builder.Drop()
				return err
			}

			i++
			if build.Release == "standard" {
				time.Sleep(time.Second * 15)
			}
		}
	}()
	if err != nil {
271
		return nil, types.TransactionSignature{}, types.FileContractID{}, err
272 273 274 275 276 277 278 279
	}

	// Get the host's transaction signatures from the builder.
	var hostTxnSignatures []types.TransactionSignature
	_, _, _, txnSigIndices := builder.ViewAdded()
	for _, sigIndex := range txnSigIndices {
		hostTxnSignatures = append(hostTxnSignatures, fullTxn.TransactionSignatures[sigIndex])
	}
280
	return hostTxnSignatures, revisionTransaction.TransactionSignatures[1], so.id(), nil
281
}