/
contractcreate.go
434 lines (378 loc) · 16.6 KB
/
contractcreate.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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
// Copyright 2019 DxChain, All rights reserved.
// Use of this source code is governed by an Apache
// License 2.0 that can be found in the LICENSE file
package contractmanager
import (
"errors"
"fmt"
"github.com/DxChainNetwork/godx/accounts"
"github.com/DxChainNetwork/godx/common"
"github.com/DxChainNetwork/godx/core/types"
"github.com/DxChainNetwork/godx/crypto"
"github.com/DxChainNetwork/godx/log"
"github.com/DxChainNetwork/godx/p2p/enode"
"github.com/DxChainNetwork/godx/rlp"
"github.com/DxChainNetwork/godx/storage"
"github.com/DxChainNetwork/godx/storage/storageclient/contractset"
"github.com/DxChainNetwork/godx/storage/storageclient/storagehostmanager"
"github.com/DxChainNetwork/godx/storage/storagehost"
)
// prepareCreateContract refers that client will sign some contracts with hosts, which satisfies the upload/download demand
func (cm *ContractManager) prepareCreateContract(neededContracts int, clientRemainingFund common.BigInt, rentPayment storage.RentPayment) (terminated bool, err error) {
// get some random hosts for contract formation
randomHosts, err := cm.randomHostsForContractForm(neededContracts)
if err != nil {
return
}
cm.lock.RLock()
contractFund := rentPayment.Fund.DivUint64(rentPayment.StorageHosts).DivUint64(3)
contractEndHeight := cm.currentPeriod + rentPayment.Period + storage.RenewWindow
cm.lock.RUnlock()
// loop through each host and try to form contract with them
for _, host := range randomHosts {
// check if the client has enough fund for forming contract
if contractFund.Cmp(clientRemainingFund) > 0 {
err = fmt.Errorf("the contract fund %v is larger than client remaining fund %v. Impossible to create contract",
contractFund, clientRemainingFund)
return
}
// start to form contract
formCost, contract, errFormContract := cm.createContract(host, contractFund, contractEndHeight, rentPayment)
// if contract formation failed, the error do not need to be returned, just try to form the
// contract with another storage host
if errFormContract != nil {
cm.log.Warn("failed to create the contract", "err", errFormContract.Error())
continue
}
// update the client remaining fund, and try to change the newly formed contract's status
clientRemainingFund = clientRemainingFund.Sub(formCost)
if err = cm.markNewlyFormedContractStats(contract.ID); err != nil {
return
}
// save persistently
if failedSave := cm.saveSettings(); failedSave != nil {
cm.log.Warn("after created the contract, failed to save the contract manager settings")
}
// update the number of needed contracts
neededContracts--
if neededContracts <= 0 {
break
}
// check if the maintenance termination signal was sent
if terminated = cm.checkMaintenanceTermination(); terminated {
break
}
}
return
}
// createContract will try to create the contract with the host that caller passed in:
// 1. storage host validation
// 2. form the contract create parameters
// 3. start to create the contract
// 4. update the contract manager fields
func (cm *ContractManager) createContract(host storage.HostInfo, contractFund common.BigInt, contractEndHeight uint64, rentPayment storage.RentPayment) (formCost common.BigInt, newlyCreatedContract storage.ContractMetaData, err error) {
// 1. storage host validation
// validate the storage price
if host.StoragePrice.Cmp(maxHostStoragePrice) > 0 {
formCost = common.BigInt0
err = fmt.Errorf("failed to create the contract with host: %v, the storage price is too high", host.EnodeID)
return
}
// validate the storage host max deposit
if host.MaxDeposit.Cmp(maxHostDeposit) > 0 {
host.MaxDeposit = maxHostDeposit
}
// validate the storage host max duration
if host.MaxDuration < rentPayment.Period {
formCost = common.BigInt0
err = fmt.Errorf("failed to create the contract with host: %v, the max duration is smaller than period", host.EnodeID)
return
}
// 2. form the contract create parameters
// The reason to get the newest blockHeight here is that during the checking time period
// many blocks may be generated already, which is unfair to the storage client.
cm.lock.RLock()
startHeight := cm.blockHeight
cm.lock.RUnlock()
// try to get the clientPaymentAddress. If failed, return error directly and set the contract creation cost
// to be zero
var clientPaymentAddress common.Address
if clientPaymentAddress, err = cm.b.GetPaymentAddress(); err != nil {
formCost = common.BigInt0
err = fmt.Errorf("failed to create the contract with host: %v, failed to get the clientPayment address: %s", host.EnodeID, err.Error())
return
}
// form the contract create parameters
params := storage.ContractParams{
RentPayment: rentPayment,
HostEnodeURL: host.EnodeURL,
Funding: contractFund,
StartHeight: startHeight,
EndHeight: contractEndHeight,
ClientPaymentAddress: clientPaymentAddress,
Host: host,
}
// 3. create the contract
if newlyCreatedContract, err = cm.ContractCreate(params); err != nil {
formCost = common.BigInt0
err = fmt.Errorf("failed to create the contract: %s", err.Error())
return
}
// 4. update the contract manager fields
cm.lock.Lock()
// check if the storage client have created another contract with the same storage host
if _, exists := cm.hostToContract[newlyCreatedContract.EnodeID]; exists {
cm.lock.Unlock()
formCost = contractFund
err = fmt.Errorf("client already formed a contract with the same storage host %v", newlyCreatedContract.EnodeID)
return
}
// if not exists, update the host to contract mapping
cm.hostToContract[newlyCreatedContract.EnodeID] = newlyCreatedContract.ID
cm.lock.Unlock()
formCost = contractFund
return
}
// randomHostsForContractForm will randomly retrieve some storage hosts from the storage host pool
func (cm *ContractManager) randomHostsForContractForm(neededContracts int) (randomHosts []storage.HostInfo, err error) {
// for all active contracts, the storage host will be added to be blacklist
// for all active contracts which are not canceled, good for uploading, and renewing
// the storage host will be added to the addressBlackList
var blackList []enode.ID
var addressBlackList []enode.ID
activeContracts := cm.activeContracts.RetrieveAllContractsMetaData()
cm.lock.RLock()
for _, contract := range activeContracts {
blackList = append(blackList, contract.EnodeID)
// update the addressBlackList
if contract.Status.UploadAbility && contract.Status.RenewAbility && !contract.Status.Canceled {
addressBlackList = append(addressBlackList, contract.EnodeID)
}
}
cm.lock.RUnlock()
// randomly retrieve some hosts
return cm.hostManager.RetrieveRandomHosts(neededContracts*randomStorageHostsFactor+randomStorageHostsBackup, blackList, addressBlackList)
}
// ContractCreate will try to create the contract with the storage host manager provided
// by the caller
func (cm *ContractManager) ContractCreate(params storage.ContractParams) (md storage.ContractMetaData, err error) {
rentPayment, funding, clientPaymentAddress, startHeight, endHeight, host := params.RentPayment, params.Funding, params.ClientPaymentAddress, params.StartHeight, params.EndHeight, params.Host
// Calculate the payouts for the client, host, and whole contract
period := endHeight - startHeight
expectedStorage := rentPayment.ExpectedStorage / rentPayment.StorageHosts
clientPayout, hostPayout, _, err := ClientPayouts(host, funding, common.BigInt0, common.BigInt0, period, expectedStorage)
if err != nil {
err = fmt.Errorf("failed to calculate the client payouts: %s", err.Error())
return storage.ContractMetaData{}, err
}
uc := types.UnlockConditions{
PaymentAddresses: []common.Address{
clientPaymentAddress,
host.PaymentAddress,
},
SignaturesRequired: 2,
}
// Create storage contract
storageContract := types.StorageContract{
FileSize: 0,
FileMerkleRoot: common.Hash{}, // no proof possible without data
WindowStart: endHeight,
WindowEnd: endHeight + host.WindowSize,
ClientCollateral: types.DxcoinCollateral{DxcoinCharge: types.DxcoinCharge{Value: clientPayout.BigIntPtr(), Address: clientPaymentAddress}},
HostCollateral: types.DxcoinCollateral{DxcoinCharge: types.DxcoinCharge{Value: hostPayout.BigIntPtr(), Address: host.PaymentAddress}},
UnlockHash: uc.UnlockHash(),
RevisionNumber: 0,
ValidProofOutputs: []types.DxcoinCharge{
// Deposit is returned to client
{Value: clientPayout.BigIntPtr(), Address: clientPaymentAddress},
// Deposit is returned to host
{Value: hostPayout.BigIntPtr(), Address: host.PaymentAddress},
},
MissedProofOutputs: []types.DxcoinCharge{
{Value: clientPayout.BigIntPtr(), Address: clientPaymentAddress},
{Value: hostPayout.BigIntPtr(), Address: host.PaymentAddress},
},
}
//Find the wallet based on the account address
account := accounts.Account{Address: clientPaymentAddress}
wallet, err := cm.b.AccountManager().Find(account)
if err != nil {
return storage.ContractMetaData{}, storagehost.ExtendErr("find client account error", err)
}
// set up the connection with the storage host and remove the operation once done
sp, err := cm.b.SetupConnection(host.EnodeURL)
if err != nil {
cm.log.Error("contract create failed, failed to set up connection", "err", err)
return storage.ContractMetaData{}, storagehost.ExtendErr("setup connection failed while creating the contract", err)
}
// Increase Successful/Failed interactions accordingly
// Ignore the send negotiate network error, we expect that client will wait for host
// that prevents client from opening another negotiate stage prematurely but receives host busy signal
var clientNegotiateErr, hostNegotiateErr, hostCommitErr error
defer func() {
if clientNegotiateErr != nil {
_ = sp.SendClientNegotiateErrorMsg()
if msg, err := sp.ClientWaitContractResp(); err != nil || msg.Code != storage.HostAckMsg {
cm.log.Error("Client receive host ack msg failed or msg.code is not host ack", "err", err)
}
}
// we will delete static flag when host negotiate or commit error
// when host occurs error, we increase failed interactions
if hostCommitErr != nil || hostNegotiateErr != nil {
cm.hostManager.IncrementFailedInteractions(host.EnodeID, storagehostmanager.InteractionCreateContract)
cm.b.CheckAndUpdateConnection(sp.PeerNode())
}
if err == nil {
cm.hostManager.IncrementSuccessfulInteractions(host.EnodeID, storagehostmanager.InteractionCreateContract)
}
}()
//Sign the hash of the storage contract
clientContractSign, err := wallet.SignHash(account, storageContract.RLPHash().Bytes())
if err != nil {
return storage.ContractMetaData{}, storagehost.ExtendErr("contract sign by client failed", err)
}
// Send the ContractCreate request
req := storage.ContractCreateRequest{
StorageContract: storageContract,
Sign: clientContractSign,
Renew: false,
}
if err := sp.RequestContractCreation(req); err != nil {
err = fmt.Errorf("failed to send the contract creation request: %s", err.Error())
log.Error("contract create failed", "err", err)
return storage.ContractMetaData{}, err
}
var hostSign []byte
msg, err := sp.ClientWaitContractResp()
if err != nil {
err = fmt.Errorf("contract create read message error: %s", err.Error())
return storage.ContractMetaData{}, err
}
// meaning request was sent too frequently, the host's evaluation
// will not be degraded
if msg.Code == storage.HostBusyHandleReqMsg {
return storage.ContractMetaData{}, storage.ErrHostBusyHandleReq
}
// if host send some negotiation error, client should handler it
if msg.Code == storage.HostNegotiateErrorMsg {
hostNegotiateErr = storage.ErrHostNegotiate
return storage.ContractMetaData{}, hostNegotiateErr
}
if err := msg.Decode(&hostSign); err != nil {
hostNegotiateErr = fmt.Errorf("failed to decode host signature: %s", err.Error())
return storage.ContractMetaData{}, hostNegotiateErr
}
storageContract.Signatures = [][]byte{clientContractSign, hostSign}
// Assemble init revision and sign it
storageContractRevision := types.StorageContractRevision{
ParentID: storageContract.RLPHash(),
UnlockConditions: uc,
NewRevisionNumber: 1,
NewFileSize: storageContract.FileSize,
NewFileMerkleRoot: storageContract.FileMerkleRoot,
NewWindowStart: storageContract.WindowStart,
NewWindowEnd: storageContract.WindowEnd,
NewValidProofOutputs: storageContract.ValidProofOutputs,
NewMissedProofOutputs: storageContract.MissedProofOutputs,
NewUnlockHash: storageContract.UnlockHash,
}
clientRevisionSign, err := wallet.SignHash(account, storageContractRevision.RLPHash().Bytes())
if err != nil {
clientNegotiateErr = storagehost.ExtendErr("client sign revision error", err)
return storage.ContractMetaData{}, clientNegotiateErr
}
storageContractRevision.Signatures = [][]byte{clientRevisionSign}
if err := sp.SendContractCreateClientRevisionSign(clientRevisionSign); err != nil {
clientNegotiateErr = storagehost.ExtendErr("send revision sign by client error", err)
return storage.ContractMetaData{}, clientNegotiateErr
}
// wait until response was sent by storage host
var hostRevisionSign []byte
msg, err = sp.ClientWaitContractResp()
if err != nil {
err = fmt.Errorf("failed to read message after sned revision sign: %s", err.Error())
log.Error("contract create failed", "err", err.Error())
return storage.ContractMetaData{}, err
}
// if host send some negotiation error, client should handler it
if msg.Code == storage.HostNegotiateErrorMsg {
hostNegotiateErr = storage.ErrHostNegotiate
return storage.ContractMetaData{}, hostNegotiateErr
}
if err := msg.Decode(&hostRevisionSign); err != nil {
hostNegotiateErr = fmt.Errorf("failed to decode the hostRevisionSign: %s", err.Error())
return storage.ContractMetaData{}, hostNegotiateErr
}
scBytes, err := rlp.EncodeToBytes(storageContract)
if err != nil {
clientNegotiateErr = fmt.Errorf("failed to enocde storageContract: %s", err.Error())
return storage.ContractMetaData{}, clientNegotiateErr
}
if _, err := cm.b.SendStorageContractCreateTx(clientPaymentAddress, scBytes); err != nil {
clientNegotiateErr = storagehost.ExtendErr("Send storage contract creation transaction error", err)
return storage.ContractMetaData{}, clientNegotiateErr
}
pubKey, err := crypto.UnmarshalPubkey(host.NodePubKey)
if err != nil {
clientNegotiateErr = storagehost.ExtendErr("Failed to convert the NodePubKey", err)
return storage.ContractMetaData{}, clientNegotiateErr
}
// wrap some information about this contract
storageContractRevision.Signatures = append(storageContractRevision.Signatures, hostRevisionSign)
header := contractset.ContractHeader{
ID: storage.ContractID(storageContract.ID()),
EnodeID: PubkeyToEnodeID(pubKey),
StartHeight: startHeight,
TotalCost: funding,
ContractFee: host.ContractPrice,
LatestContractRevision: storageContractRevision,
Status: storage.ContractStatus{
UploadAbility: true,
RenewAbility: true,
},
}
// store this contract info to client local
meta, err := cm.GetStorageContractSet().InsertContract(header, nil)
if err != nil {
// ignore the send message error the same as negotiate error
_ = sp.SendClientCommitFailedMsg()
// wait for host ack msg
msg, err = sp.ClientWaitContractResp()
if err == nil && msg.Code == storage.HostAckMsg {
err = errors.New("failed to insert the contract after announce host")
} else if err != nil {
err = fmt.Errorf("failed to insert the contract after announce host, but cann't receive host ack msg: %s", err.Error())
}
return storage.ContractMetaData{}, err
}
// send the commit success msg if insert contract occurs no error
// we ignore any error and then wait the host ack msg
_ = sp.SendClientCommitSuccessMsg()
// wait for HostAckMsg until timeout
msg, err = sp.ClientWaitContractResp()
if err != nil {
log.Error("contract create failed when wait for host ACK msg", "err", err)
_ = rollbackContractSet(cm.GetStorageContractSet(), header.ID)
return storage.ContractMetaData{}, err
}
switch msg.Code {
case storage.HostAckMsg:
return meta, nil
default:
hostCommitErr = storage.ErrHostCommit
_ = rollbackContractSet(cm.GetStorageContractSet(), header.ID)
_ = sp.SendClientAckMsg()
// client wait for host last ack msg. if timeout or not ack,
// client still throw host error. so we ignore any msg content and the return error
_, _ = sp.ClientWaitContractResp()
return storage.ContractMetaData{}, hostCommitErr
}
}
func rollbackContractSet(contractSet *contractset.StorageContractSet, id storage.ContractID) error {
if c, exist := contractSet.Acquire(id); exist {
if err := contractSet.Delete(c); err != nil {
return err
}
}
return nil
}