forked from NebulousLabs/Sia
-
Notifications
You must be signed in to change notification settings - Fork 0
/
formcontract.go
291 lines (261 loc) · 10.4 KB
/
formcontract.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
package proto
import (
"net"
"github.com/NebulousLabs/Sia/crypto"
"github.com/NebulousLabs/Sia/encoding"
"github.com/NebulousLabs/Sia/modules"
"github.com/NebulousLabs/Sia/types"
"github.com/NebulousLabs/errors"
)
const (
// estTxnSize is the estimated size of an encoded file contract
// transaction set.
estTxnSize = 2048
)
// FormContract forms a contract with a host and submits the contract
// transaction to tpool. The contract is added to the ContractSet and its
// metadata is returned.
func (cs *ContractSet) FormContract(params ContractParams, txnBuilder transactionBuilder, tpool transactionPool, hdb hostDB, cancel <-chan struct{}) (rc modules.RenterContract, err error) {
// Extract vars from params, for convenience.
host, funding, startHeight, endHeight, refundAddress := params.Host, params.Funding, params.StartHeight, params.EndHeight, params.RefundAddress
// Create our key.
ourSK, ourPK := crypto.GenerateKeyPair()
// Create unlock conditions.
uc := types.UnlockConditions{
PublicKeys: []types.SiaPublicKey{
types.Ed25519PublicKey(ourPK),
host.PublicKey,
},
SignaturesRequired: 2,
}
// Calculate the anticipated transaction fee.
_, maxFee := tpool.FeeEstimation()
txnFee := maxFee.Mul64(estTxnSize)
// Underflow check.
if funding.Cmp(host.ContractPrice.Add(txnFee)) <= 0 {
return modules.RenterContract{}, errors.New("insufficient funds to cover contract fee and transaction fee during contract formation")
}
// Divide by zero check.
if host.StoragePrice.IsZero() {
host.StoragePrice = types.NewCurrency64(1)
}
// Calculate the payouts for the renter, host, and whole contract.
renterPayout := funding.Sub(host.ContractPrice).Sub(txnFee) // renter payout is pre-tax
maxStorageSize := renterPayout.Div(host.StoragePrice)
hostCollateral := maxStorageSize.Mul(host.Collateral)
if hostCollateral.Cmp(host.MaxCollateral) > 0 {
hostCollateral = host.MaxCollateral
}
// Calculate the initial host payout.
hostPayout := hostCollateral.Add(host.ContractPrice)
totalPayout := renterPayout.Add(hostPayout)
// Check for negative currency.
if types.PostTax(startHeight, totalPayout).Cmp(hostPayout) < 0 {
return modules.RenterContract{}, errors.New("not enough money to pay both siafund fee and also 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: totalPayout,
UnlockHash: uc.UnlockHash(),
RevisionNumber: 0,
ValidProofOutputs: []types.SiacoinOutput{
// Outputs need to account for tax.
{Value: types.PostTax(startHeight, totalPayout).Sub(hostPayout), UnlockHash: refundAddress}, // This is the renter payout, but with tax applied.
// Collateral is returned to host.
{Value: hostPayout, UnlockHash: host.UnlockHash},
},
MissedProofOutputs: []types.SiacoinOutput{
// Same as above.
{Value: types.PostTax(startHeight, totalPayout).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{}},
},
}
// Build transaction containing fc, e.g. the File Contract.
err = txnBuilder.FundSiacoins(funding)
if err != nil {
return modules.RenterContract{}, err
}
txnBuilder.AddFileContract(fc)
// Add miner fee.
txnBuilder.AddMinerFee(txnFee)
// Create initial transaction set.
txn, parentTxns := txnBuilder.View()
unconfirmedParents, err := txnBuilder.UnconfirmedParents()
if err != nil {
return modules.RenterContract{}, err
}
txnSet := append(unconfirmedParents, append(parentTxns, txn)...)
// Increase Successful/Failed interactions accordingly
defer func() {
if err != nil {
hdb.IncrementFailedInteractions(host.PublicKey)
err = errors.Extend(err, modules.ErrHostFault)
} else {
hdb.IncrementSuccessfulInteractions(host.PublicKey)
}
}()
// Initiate connection.
dialer := &net.Dialer{
Cancel: cancel,
Timeout: connTimeout,
}
conn, err := dialer.Dial("tcp", string(host.NetAddress))
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 := crypto.SignHash(revisionTxn.SigHash(0), ourSK)
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
}
// Construct contract header.
header := contractHeader{
Transaction: revisionTxn,
SecretKey: ourSK,
StartHeight: startHeight,
TotalCost: funding,
ContractFee: host.ContractPrice,
TxnFee: txnFee,
SiafundFee: types.Tax(startHeight, fc.Payout),
Utility: modules.ContractUtility{
GoodForUpload: true,
GoodForRenew: true,
},
}
// Add contract to set.
meta, err := cs.managedInsertContract(header, nil) // no Merkle roots yet
if err != nil {
return modules.RenterContract{}, err
}
return meta, nil
}