// newTesterStorageObligation uses the wallet to create and fund a file // contract that will form the foundation of a storage obligation. func (ht *hostTester) newTesterStorageObligation() (*storageObligation, error) { // Create the file contract that will be used in the obligation. builder := ht.wallet.StartTransaction() // Fund the file contract with a payout. The payout needs to be big enough // that the expected revenue is larger than the fee that the host may end // up paying. payout := types.SiacoinPrecision.Mul64(1e3) err := builder.FundSiacoins(payout) if err != nil { return nil, err } // Add the file contract that consumes the funds. _ = builder.AddFileContract(types.FileContract{ // Because this file contract needs to be able to accept file contract // revisions, the expiration is put more than // 'revisionSubmissionBuffer' blocks into the future. WindowStart: ht.host.blockHeight + revisionSubmissionBuffer + 2, WindowEnd: ht.host.blockHeight + revisionSubmissionBuffer + defaultWindowSize + 2, Payout: payout, ValidProofOutputs: []types.SiacoinOutput{ { Value: types.PostTax(ht.host.blockHeight, payout), }, { Value: types.ZeroCurrency, }, }, MissedProofOutputs: []types.SiacoinOutput{ { Value: types.PostTax(ht.host.blockHeight, payout), }, { Value: types.ZeroCurrency, }, }, UnlockHash: (types.UnlockConditions{}).UnlockHash(), RevisionNumber: 0, }) // Sign the transaction. tSet, err := builder.Sign(true) if err != nil { return nil, err } // Assemble and return the storage obligation. so := &storageObligation{ OriginTransactionSet: tSet, // TODO: There are no tracking values, because no fees were added. } return so, nil }
// TestAcceptFCAndConflictingRevision checks that the transaction pool // correctly accepts a file contract in a transaction set followed by a correct // revision to that file contract in the a following transaction set, with no // block separating them. func TestAcceptFCAndConflictingRevision(t *testing.T) { if testing.Short() { t.SkipNow() } tpt, err := createTpoolTester("TestAcceptFCAndConflictingRevision") if err != nil { t.Fatal(err) } defer tpt.Close() // Create and fund a valid file contract. builder := tpt.wallet.StartTransaction() payout := types.NewCurrency64(1e9) err = builder.FundSiacoins(payout) if err != nil { t.Fatal(err) } builder.AddFileContract(types.FileContract{ WindowStart: tpt.cs.Height() + 2, WindowEnd: tpt.cs.Height() + 5, Payout: payout, ValidProofOutputs: []types.SiacoinOutput{{Value: types.PostTax(tpt.cs.Height(), payout)}}, MissedProofOutputs: []types.SiacoinOutput{{Value: types.PostTax(tpt.cs.Height(), payout)}}, UnlockHash: types.UnlockConditions{}.UnlockHash(), }) tSet, err := builder.Sign(true) if err != nil { t.Fatal(err) } err = tpt.tpool.AcceptTransactionSet(tSet) if err != nil { t.Fatal(err) } fcid := tSet[len(tSet)-1].FileContractID(0) // Create a file contract revision and submit it. rSet := []types.Transaction{{ FileContractRevisions: []types.FileContractRevision{{ ParentID: fcid, NewRevisionNumber: 2, NewWindowStart: tpt.cs.Height() + 2, NewWindowEnd: tpt.cs.Height() + 5, NewValidProofOutputs: []types.SiacoinOutput{{Value: types.PostTax(tpt.cs.Height(), payout)}}, NewMissedProofOutputs: []types.SiacoinOutput{{Value: types.PostTax(tpt.cs.Height(), payout)}}, }}, }} err = tpt.tpool.AcceptTransactionSet(rSet) if err != nil { t.Fatal(err) } }
func TestNegotiateContract(t *testing.T) { if testing.Short() { t.SkipNow() } t.Parallel() ct, err := newContractorTester("TestNegotiateContract") if err != nil { t.Fatal(err) } payout := types.NewCurrency64(1e16) fc := types.FileContract{ FileSize: 0, FileMerkleRoot: crypto.Hash{}, // no proof possible without data WindowStart: 100, WindowEnd: 1000, Payout: payout, ValidProofOutputs: []types.SiacoinOutput{ {Value: types.PostTax(ct.contractor.blockHeight, payout), UnlockHash: types.UnlockHash{}}, {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, }, MissedProofOutputs: []types.SiacoinOutput{ // same as above {Value: types.PostTax(ct.contractor.blockHeight, payout), UnlockHash: types.UnlockHash{}}, // goes to the void, not the hostdb {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, }, UnlockHash: types.UnlockHash{}, RevisionNumber: 0, } txnBuilder := ct.wallet.StartTransaction() err = txnBuilder.FundSiacoins(fc.Payout) if err != nil { t.Fatal(err) } txnBuilder.AddFileContract(fc) signedTxnSet, err := txnBuilder.Sign(true) if err != nil { t.Fatal(err) } err = ct.tpool.AcceptTransactionSet(signedTxnSet) if err != nil { t.Fatal(err) } }
// considerRevision checks that the provided file contract revision is still // acceptable to the host. func (h *Host) considerRevision(txn types.Transaction, obligation *contractObligation) error { // Check that there is only one revision. if len(txn.FileContractRevisions) != 1 { return errors.New("transaction should have only one revision") } // calculate minimum expected output value rev := txn.FileContractRevisions[0] duration := types.NewCurrency64(uint64(obligation.windowStart() - h.blockHeight)) minHostPrice := types.NewCurrency64(rev.NewFileSize).Mul(duration).Mul(h.settings.Price) expectedPayout := types.PostTax(h.blockHeight, obligation.payout()) switch { // these fields should never change case rev.ParentID != obligation.ID: return errors.New("bad revision parent ID") case rev.NewWindowStart != obligation.windowStart(): return errors.New("bad revision window start") case rev.NewWindowEnd != obligation.windowEnd(): return errors.New("bad revision window end") case rev.NewUnlockHash != obligation.unlockHash(): return errors.New("bad revision unlock hash") case rev.UnlockConditions.UnlockHash() != obligation.unlockHash(): return errors.New("bad revision unlock conditions") case len(rev.NewValidProofOutputs) != 2: return errors.New("bad revision valid proof outputs") case len(rev.NewMissedProofOutputs) != 2: return errors.New("bad revision missed proof outputs") case rev.NewValidProofOutputs[1].UnlockHash != obligation.validProofUnlockHash(), rev.NewMissedProofOutputs[1].UnlockHash != obligation.missedProofUnlockHash(): return errors.New("bad revision proof outputs") case rev.NewRevisionNumber <= obligation.revisionNumber(): return errors.New("revision must have higher revision number") case rev.NewFileSize > uint64(h.spaceRemaining): return errors.New("revision file size is too large") case rev.NewFileSize <= obligation.fileSize(): return errors.New("revision must add data") case rev.NewFileSize-obligation.fileSize() > maxRevisionSize: return errors.New("revision adds too much data") case rev.NewValidProofOutputs[0].Value.Add(rev.NewValidProofOutputs[1].Value).Cmp(expectedPayout) != 0, // valid and missing outputs should still sum to payout rev.NewMissedProofOutputs[0].Value.Add(rev.NewMissedProofOutputs[1].Value).Cmp(expectedPayout) != 0: return errors.New("revision outputs do not sum to original payout") case rev.NewValidProofOutputs[1].Value.Cmp(minHostPrice) < 0: // outputs should have been adjusted proportional to the new filesize return errors.New("revision price is too small") case rev.NewMissedProofOutputs[0].Value.Cmp(rev.NewValidProofOutputs[0].Value) != 0: return errors.New("revision missed renter payout does not match valid payout") } return nil }
// considerRevision checks that the provided file contract revision is still // acceptable to the host. // TODO: should take a txn and check that is only contains the single revision func (h *Host) considerRevision(txn types.Transaction, obligation contractObligation) error { // Check that there is only one revision. // TODO: check that the txn is empty except for the revision? if len(txn.FileContractRevisions) != 1 { return errors.New("transaction should have only one revision") } // calculate minimum expected output value rev := txn.FileContractRevisions[0] fc := obligation.FileContract duration := types.NewCurrency64(uint64(fc.WindowStart - h.blockHeight)) minHostPrice := types.NewCurrency64(rev.NewFileSize).Mul(duration).Mul(h.Price) expectedPayout := types.PostTax(h.blockHeight, fc.Payout) switch { // these fields should never change case rev.ParentID != obligation.ID: return errors.New("bad revision parent ID") case rev.NewWindowStart != fc.WindowStart: return errors.New("bad revision window start") case rev.NewWindowEnd != fc.WindowEnd: return errors.New("bad revision window end") case rev.NewUnlockHash != fc.UnlockHash: return errors.New("bad revision unlock hash") case rev.UnlockConditions.UnlockHash() != fc.UnlockHash: return errors.New("bad revision unlock conditions") case len(rev.NewValidProofOutputs) != 2: return errors.New("bad revision valid proof outputs") case len(rev.NewMissedProofOutputs) != 2: return errors.New("bad revision missed proof outputs") case rev.NewValidProofOutputs[1].UnlockHash != fc.ValidProofOutputs[1].UnlockHash, rev.NewMissedProofOutputs[1].UnlockHash != fc.MissedProofOutputs[1].UnlockHash: return errors.New("bad revision proof outputs") case rev.NewRevisionNumber <= fc.RevisionNumber: return errors.New("revision must have higher revision number") case rev.NewFileSize > uint64(h.spaceRemaining) || rev.NewFileSize > h.MaxFilesize: return errors.New("revision file size is too large") // valid and missing outputs should still sum to payout case rev.NewValidProofOutputs[0].Value.Add(rev.NewValidProofOutputs[1].Value).Cmp(expectedPayout) != 0, rev.NewMissedProofOutputs[0].Value.Add(rev.NewMissedProofOutputs[1].Value).Cmp(expectedPayout) != 0: return errors.New("revision outputs do not sum to original payout") // outputs should have been adjusted proportional to the new filesize case rev.NewValidProofOutputs[1].Value.Cmp(minHostPrice) <= 0: return errors.New("revision price is too small") case rev.NewMissedProofOutputs[0].Value.Cmp(rev.NewValidProofOutputs[0].Value) != 0: return errors.New("revision missed renter payout does not match valid payout") } return nil }
func (h *Host) Info() modules.HostInfo { h.mu.RLock() defer h.mu.RUnlock() h.HostSettings.IPAddress = h.myAddr // needs to be updated manually info := modules.HostInfo{ HostSettings: h.HostSettings, StorageRemaining: h.spaceRemaining, NumContracts: len(h.obligationsByID), Profit: h.profit, } // sum up the current obligations to calculate PotentialProfit for _, obligation := range h.obligationsByID { fc := obligation.FileContract info.PotentialProfit = info.PotentialProfit.Add(types.PostTax(h.blockHeight, fc.Payout)) } return info }
func (h *Host) Info() modules.HostInfo { lockID := h.mu.RLock() defer h.mu.RUnlock(lockID) info := modules.HostInfo{ HostSettings: h.HostSettings, StorageRemaining: h.spaceRemaining, NumContracts: len(h.obligationsByID), Profit: h.profit, } // sum up the current obligations to calculate PotentialProfit for _, obligation := range h.obligationsByID { fc := obligation.FileContract info.PotentialProfit = info.PotentialProfit.Add(types.PostTax(h.blockHeight, fc.Payout)) } // Calculate estimated competition (reported in per GB per month). Price // calculated by taking the average of hosts 8-15. var averagePrice types.Currency hosts := h.hostdb.RandomHosts(15) for i, host := range hosts { if i < 8 { continue } averagePrice = averagePrice.Add(host.Price) } if len(hosts) == 0 { return info } averagePrice = averagePrice.Div(types.NewCurrency64(uint64(len(hosts)))) // HACK: 4320 is one month, and 1024^3 is a GB. Price is reported as per GB // per month. estimatedCost := averagePrice.Mul(types.NewCurrency64(4320)).Mul(types.NewCurrency64(1024 * 1024 * 1024)) info.Competition = estimatedCost return info }
// testValidStorageProofBlocks adds a block with a file contract, and then // submits a storage proof for that file contract. func (cst *consensusSetTester) testValidStorageProofBlocks() { // COMPATv0.4.0 - Step the block height up past the hardfork amount. This // code stops nondeterministic failures when producing storage proofs that // is related to buggy old code. for cst.cs.dbBlockHeight() <= 10 { _, err := cst.miner.AddBlock() if err != nil { panic(err) } } // Create a file (as a bytes.Buffer) that will be used for the file // contract. filesize := uint64(4e3) file := randFile(filesize) merkleRoot, err := crypto.ReaderMerkleRoot(file) if err != nil { panic(err) } file.Seek(0, 0) // Create a file contract that will be successful. validProofDest := randAddress() payout := types.NewCurrency64(400e6) fc := types.FileContract{ FileSize: filesize, FileMerkleRoot: merkleRoot, WindowStart: cst.cs.dbBlockHeight() + 1, WindowEnd: cst.cs.dbBlockHeight() + 2, Payout: payout, ValidProofOutputs: []types.SiacoinOutput{{ UnlockHash: validProofDest, Value: types.PostTax(cst.cs.dbBlockHeight(), payout), }}, MissedProofOutputs: []types.SiacoinOutput{{ UnlockHash: types.UnlockHash{}, Value: types.PostTax(cst.cs.dbBlockHeight(), payout), }}, } // Submit a transaction with the file contract. oldSiafundPool := cst.cs.dbGetSiafundPool() txnBuilder := cst.wallet.StartTransaction() err = txnBuilder.FundSiacoins(payout) if err != nil { panic(err) } fcIndex := txnBuilder.AddFileContract(fc) txnSet, err := txnBuilder.Sign(true) if err != nil { panic(err) } err = cst.tpool.AcceptTransactionSet(txnSet) if err != nil { panic(err) } _, err = cst.miner.AddBlock() if err != nil { panic(err) } // Check that the siafund pool was increased by the tax on the payout. siafundPool := cst.cs.dbGetSiafundPool() if siafundPool.Cmp(oldSiafundPool.Add(types.Tax(cst.cs.dbBlockHeight()-1, payout))) != 0 { panic("siafund pool was not increased correctly") } // Check that the file contract made it into the database. ti := len(txnSet) - 1 fcid := txnSet[ti].FileContractID(fcIndex) _, err = cst.cs.dbGetFileContract(fcid) if err != nil { panic(err) } // Create and submit a storage proof for the file contract. segmentIndex, err := cst.cs.StorageProofSegment(fcid) if err != nil { panic(err) } segment, hashSet, err := crypto.BuildReaderProof(file, segmentIndex) if err != nil { panic(err) } sp := types.StorageProof{ ParentID: fcid, HashSet: hashSet, } copy(sp.Segment[:], segment) txnBuilder = cst.wallet.StartTransaction() txnBuilder.AddStorageProof(sp) txnSet, err = txnBuilder.Sign(true) if err != nil { panic(err) } err = cst.tpool.AcceptTransactionSet(txnSet) if err != nil { panic(err) } _, err = cst.miner.AddBlock() if err != nil { panic(err) } // Check that the file contract has been removed. _, err = cst.cs.dbGetFileContract(fcid) if err != errNilItem { panic("file contract should not exist in the database") } // Check that the siafund pool has not changed. postProofPool := cst.cs.dbGetSiafundPool() if postProofPool.Cmp(siafundPool) != 0 { panic("siafund pool should not change after submitting a storage proof") } // Check that a delayed output was created for the valid proof. spoid := fcid.StorageProofOutputID(types.ProofValid, 0) dsco, err := cst.cs.dbGetDSCO(cst.cs.dbBlockHeight()+types.MaturityDelay, spoid) if err != nil { panic(err) } if dsco.UnlockHash != fc.ValidProofOutputs[0].UnlockHash { panic("wrong unlock hash in dsco") } if dsco.Value.Cmp(fc.ValidProofOutputs[0].Value) != 0 { panic("wrong sco value in dsco") } }
// COMPATv0.4.0 // // This test checks that the hardfork scheduled for block 21,000 rolls through // smoothly. func TestTaxHardfork(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestTaxHardfork") if err != nil { t.Fatal(err) } defer cst.closeCst() // Create a file contract with a payout that is put into the blockchain // before the hardfork block but expires after the hardfork block. payout := types.NewCurrency64(400e6) outputSize := types.PostTax(cst.cs.dbBlockHeight(), payout) fc := types.FileContract{ WindowStart: cst.cs.dbBlockHeight() + 12, WindowEnd: cst.cs.dbBlockHeight() + 14, Payout: payout, ValidProofOutputs: []types.SiacoinOutput{{Value: outputSize}}, MissedProofOutputs: []types.SiacoinOutput{{Value: outputSize}}, UnlockHash: types.UnlockConditions{}.UnlockHash(), // The empty UC is anyone-can-spend } // Create and fund a transaction with a file contract. txnBuilder := cst.wallet.StartTransaction() err = txnBuilder.FundSiacoins(payout) if err != nil { t.Fatal(err) } fcIndex := txnBuilder.AddFileContract(fc) txnSet, err := txnBuilder.Sign(true) if err != nil { t.Fatal(err) } err = cst.tpool.AcceptTransactionSet(txnSet) if err != nil { t.Fatal(err) } _, err = cst.miner.AddBlock() if err != nil { t.Fatal(err) } // Check that the siafund pool was increased by the faulty float amount. siafundPool := cst.cs.dbGetSiafundPool() if siafundPool.Cmp(types.NewCurrency64(15590e3)) != 0 { t.Fatal("siafund pool was not increased correctly") } // Mine blocks until the hardfork is reached. for i := 0; i < 10; i++ { _, err = cst.miner.AddBlock() if err != nil { t.Fatal(err) } } // Submit a file contract revision and check that the payouts are able to // be the same. fcid := txnSet[len(txnSet)-1].FileContractID(fcIndex) fcr := types.FileContractRevision{ ParentID: fcid, UnlockConditions: types.UnlockConditions{}, NewRevisionNumber: 1, NewFileSize: 1, NewWindowStart: cst.cs.dbBlockHeight() + 2, NewWindowEnd: cst.cs.dbBlockHeight() + 4, NewValidProofOutputs: fc.ValidProofOutputs, NewMissedProofOutputs: fc.MissedProofOutputs, } txnBuilder = cst.wallet.StartTransaction() txnBuilder.AddFileContractRevision(fcr) txnSet, err = txnBuilder.Sign(true) if err != nil { t.Fatal(err) } err = cst.tpool.AcceptTransactionSet(txnSet) if err != nil { t.Fatal(err) } _, err = cst.miner.AddBlock() if err != nil { t.Fatal(err) } // Mine blocks until the revision goes through, such that the sanity checks // can be run. for i := 0; i < 6; i++ { _, err = cst.miner.AddBlock() if err != nil { t.Fatal(err) } } // Check that the siafund pool did not change after the submitted revision. siafundPool = cst.cs.dbGetSiafundPool() if siafundPool.Cmp(types.NewCurrency64(15590e3)) != 0 { t.Fatal("siafund pool was not increased correctly") } }
func TestReviseContract(t *testing.T) { if testing.Short() { t.SkipNow() } t.Parallel() ct, err := newContractorTester("TestReviseContract") if err != nil { t.Fatal(err) } // get an address ourAddr, err := ct.wallet.NextAddress() if err != nil { t.Fatal(err) } // generate keys sk, pk, err := crypto.GenerateKeyPair() if err != nil { t.Fatal(err) } renterPubKey := types.SiaPublicKey{ Algorithm: types.SignatureEd25519, Key: pk[:], } uc := types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{renterPubKey, renterPubKey}, SignaturesRequired: 1, } // create file contract payout := types.NewCurrency64(1e16) fc := types.FileContract{ FileSize: 0, FileMerkleRoot: crypto.Hash{}, // no proof possible without data WindowStart: 100, WindowEnd: 1000, Payout: payout, UnlockHash: uc.UnlockHash(), RevisionNumber: 0, } // outputs need account for tax fc.ValidProofOutputs = []types.SiacoinOutput{ {Value: types.PostTax(ct.contractor.blockHeight, payout), UnlockHash: ourAddr.UnlockHash()}, {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, // no collateral } fc.MissedProofOutputs = []types.SiacoinOutput{ // same as above fc.ValidProofOutputs[0], // goes to the void, not the hostdb {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, } txnBuilder := ct.wallet.StartTransaction() err = txnBuilder.FundSiacoins(fc.Payout) if err != nil { t.Fatal(err) } txnBuilder.AddFileContract(fc) signedTxnSet, err := txnBuilder.Sign(true) if err != nil { t.Fatal(err) } // submit contract err = ct.tpool.AcceptTransactionSet(signedTxnSet) if err != nil { t.Fatal(err) } // create revision fcid := signedTxnSet[len(signedTxnSet)-1].FileContractID(0) rev := types.FileContractRevision{ ParentID: fcid, UnlockConditions: uc, NewFileSize: 10, NewWindowStart: 100, NewWindowEnd: 1000, NewRevisionNumber: 1, NewValidProofOutputs: fc.ValidProofOutputs, NewMissedProofOutputs: fc.MissedProofOutputs, } // create transaction containing the revision signedTxn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{rev}, TransactionSignatures: []types.TransactionSignature{{ ParentID: crypto.Hash(fcid), CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, PublicKeyIndex: 0, // hostdb key is always first -- see negotiateContract }}, } // sign the transaction encodedSig, err := crypto.SignHash(signedTxn.SigHash(0), sk) if err != nil { t.Fatal(err) } signedTxn.TransactionSignatures[0].Signature = encodedSig[:] err = signedTxn.StandaloneValid(ct.contractor.blockHeight) if err != nil { t.Fatal(err) } // submit revision err = ct.tpool.AcceptTransactionSet([]types.Transaction{signedTxn}) if err != nil { t.Fatal(err) } }
// 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 }
// testFileContractRevision creates and revises a file contract on the // blockchain. func (cst *consensusSetTester) testFileContractRevision() { // COMPATv0.4.0 - Step the block height up past the hardfork amount. This // code stops nondeterministic failures when producing storage proofs that // is related to buggy old code. for cst.cs.dbBlockHeight() <= 10 { _, err := cst.miner.AddBlock() if err != nil { panic(err) } } // Create a file (as a bytes.Buffer) that will be used for the file // contract. filesize := uint64(4e3) file := randFile(filesize) merkleRoot, err := crypto.ReaderMerkleRoot(file) if err != nil { panic(err) } file.Seek(0, 0) // Create a spendable unlock hash for the file contract. sk, pk, err := crypto.GenerateKeyPair() if err != nil { panic(err) } uc := types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{{ Algorithm: types.SignatureEd25519, Key: pk[:], }}, SignaturesRequired: 1, } // Create a file contract that will be revised. validProofDest := randAddress() payout := types.NewCurrency64(400e6) fc := types.FileContract{ FileSize: filesize, FileMerkleRoot: crypto.Hash{}, WindowStart: cst.cs.dbBlockHeight() + 2, WindowEnd: cst.cs.dbBlockHeight() + 3, Payout: payout, ValidProofOutputs: []types.SiacoinOutput{{ UnlockHash: validProofDest, Value: types.PostTax(cst.cs.dbBlockHeight(), payout), }}, MissedProofOutputs: []types.SiacoinOutput{{ UnlockHash: types.UnlockHash{}, Value: types.PostTax(cst.cs.dbBlockHeight(), payout), }}, UnlockHash: uc.UnlockHash(), } // Submit a transaction with the file contract. txnBuilder := cst.wallet.StartTransaction() err = txnBuilder.FundSiacoins(payout) if err != nil { panic(err) } fcIndex := txnBuilder.AddFileContract(fc) txnSet, err := txnBuilder.Sign(true) if err != nil { panic(err) } err = cst.tpool.AcceptTransactionSet(txnSet) if err != nil { panic(err) } _, err = cst.miner.AddBlock() if err != nil { panic(err) } // Submit a revision for the file contract. ti := len(txnSet) - 1 fcid := txnSet[ti].FileContractID(fcIndex) fcr := types.FileContractRevision{ ParentID: fcid, UnlockConditions: uc, NewRevisionNumber: 69292, NewFileSize: filesize, NewFileMerkleRoot: merkleRoot, NewWindowStart: cst.cs.dbBlockHeight() + 1, NewWindowEnd: cst.cs.dbBlockHeight() + 2, NewValidProofOutputs: fc.ValidProofOutputs, NewMissedProofOutputs: fc.MissedProofOutputs, NewUnlockHash: uc.UnlockHash(), } ts := types.TransactionSignature{ ParentID: crypto.Hash(fcid), CoveredFields: types.CoveredFields{WholeTransaction: true}, PublicKeyIndex: 0, } txn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{fcr}, TransactionSignatures: []types.TransactionSignature{ts}, } encodedSig, err := crypto.SignHash(txn.SigHash(0), sk) if err != nil { panic(err) } txn.TransactionSignatures[0].Signature = encodedSig[:] err = cst.tpool.AcceptTransactionSet([]types.Transaction{txn}) if err != nil { panic(err) } _, err = cst.miner.AddBlock() if err != nil { panic(err) } // Create and submit a storage proof for the file contract. segmentIndex, err := cst.cs.StorageProofSegment(fcid) if err != nil { panic(err) } segment, hashSet, err := crypto.BuildReaderProof(file, segmentIndex) if err != nil { panic(err) } sp := types.StorageProof{ ParentID: fcid, HashSet: hashSet, } copy(sp.Segment[:], segment) txnBuilder = cst.wallet.StartTransaction() txnBuilder.AddStorageProof(sp) txnSet, err = txnBuilder.Sign(true) if err != nil { panic(err) } err = cst.tpool.AcceptTransactionSet(txnSet) if err != nil { panic(err) } _, err = cst.miner.AddBlock() if err != nil { panic(err) } // Check that the file contract has been removed. _, err = cst.cs.dbGetFileContract(fcid) if err != errNilItem { panic("file contract should not exist in the database") } }
// 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 }
// considerRevision checks that the provided file contract revision is still // acceptable to the host. func (h *Host) considerRevision(txn types.Transaction, obligation *contractObligation) error { // Check that there is only one revision. if len(txn.FileContractRevisions) != 1 { return errors.New("transaction should have only one revision") } // calculate minimum expected output value rev := txn.FileContractRevisions[0] duration := types.NewCurrency64(uint64(obligation.windowStart() - h.blockHeight)) sizeDiff := rev.NewFileSize - obligation.fileSize() priceAdd := types.NewCurrency64(sizeDiff).Mul(duration).Mul(h.settings.Price) minPayment := obligation.value().Add(priceAdd) expectedPayout := types.PostTax(h.blockHeight, obligation.payout()) switch { // Check that the revision matches the previous file contract. case rev.ParentID != obligation.ID: return errors.New("bad revision parent ID") case rev.NewRevisionNumber <= obligation.revisionNumber(): return errors.New("revision must have higher revision number") case rev.NewUnlockHash != obligation.unlockHash(): return errors.New("bad revision unlock hash") case rev.UnlockConditions.UnlockHash() != obligation.unlockHash(): return errors.New("bad revision unlock conditions") // Check that the window is unchanged. case rev.NewWindowStart != obligation.windowStart(): return errors.New("bad revision window start") case rev.NewWindowEnd != obligation.windowEnd(): return errors.New("bad revision window end") // Check that the change in filesize is legal. // // TODO: Revisions should leave enough headroom so that renewals always // have some space. case rev.NewFileSize <= obligation.fileSize(): return errors.New("revision must add data") case rev.NewFileSize-obligation.fileSize() > uint64(h.spaceRemaining): return ErrHostCapacity case rev.NewFileSize-obligation.fileSize() > maxRevisionSize: return errors.New("revision adds too much data") // Check that the payout information is correct. case len(rev.NewValidProofOutputs) != 2: return errors.New("bad revision valid proof outputs") case len(rev.NewMissedProofOutputs) != 2: return errors.New("bad revision missed proof outputs") case rev.NewValidProofOutputs[1].UnlockHash != obligation.validProofUnlockHash(), rev.NewMissedProofOutputs[1].UnlockHash != obligation.missedProofUnlockHash(): return errors.New("bad revision proof outputs") case rev.NewValidProofOutputs[0].Value.Add(rev.NewValidProofOutputs[1].Value).Cmp(expectedPayout) != 0, rev.NewMissedProofOutputs[0].Value.Add(rev.NewMissedProofOutputs[1].Value).Cmp(expectedPayout) != 0: return errors.New("revision outputs do not sum to original payout") case rev.NewValidProofOutputs[1].Value.Cmp(minPayment) < 0: return ErrLowPayment case rev.NewMissedProofOutputs[0].Value.Cmp(rev.NewValidProofOutputs[0].Value) != 0: return errors.New("revision missed renter payout does not match valid payout") } return nil }
// considerContract checks that the provided transaction matches the host's // terms, and doesn't contain any flagrant errors. func (h *Host) considerContract(txn types.Transaction, renterKey types.SiaPublicKey, filesize uint64, merkleRoot crypto.Hash) error { // Check that there is only one file contract. if len(txn.FileContracts) != 1 { return errors.New("transaction should have only one file contract") } // convenience variables fc := txn.FileContracts[0] duration := fc.WindowStart - h.blockHeight minPayment := types.NewCurrency64(filesize).Mul(types.NewCurrency64(uint64(duration))).Mul(h.settings.Price) expectedOutputSum := types.PostTax(h.blockHeight, fc.Payout) // check contract fields for sanity and acceptability switch { // Check for legal filesize and content. case fc.FileSize != filesize: return errors.New("bad initial file size") case fc.FileSize >= uint64(h.spaceRemaining): return ErrHostCapacity case fc.FileMerkleRoot != merkleRoot: return errors.New("bad file contract Merkle root") // Check for legal duration and proof window. case fc.WindowStart <= h.blockHeight: return errors.New("window start cannot be in the past") case duration < h.settings.MinDuration || duration > h.settings.MaxDuration: return errors.New("duration is out of bounds") case fc.WindowEnd <= fc.WindowStart: return errors.New("window cannot end before it starts") case fc.WindowEnd-fc.WindowStart < h.settings.WindowSize: return errors.New("challenge window is not large enough") // Check for legal payout. case fc.Payout.IsZero(): return errors.New("bad file contract payout") case len(fc.ValidProofOutputs) != 2: return errors.New("bad file contract valid proof outputs") case len(fc.MissedProofOutputs) != 2: return errors.New("bad file contract missed proof outputs") case fc.ValidProofOutputs[0].Value.Add(fc.ValidProofOutputs[1].Value).Cmp(expectedOutputSum) != 0, fc.MissedProofOutputs[0].Value.Add(fc.MissedProofOutputs[1].Value).Cmp(expectedOutputSum) != 0: return errors.New("file contract outputs do not sum to original payout") case fc.ValidProofOutputs[1].UnlockHash != h.settings.UnlockHash: return errors.New("file contract valid proof output not sent to host") case fc.ValidProofOutputs[1].Value.Cmp(minPayment) < 0: return ErrLowPayment case fc.MissedProofOutputs[0].Value.Cmp(fc.ValidProofOutputs[0].Value) != 0: return errors.New("file contract missed renter payout does not match valid payout") case fc.MissedProofOutputs[1].UnlockHash != (types.UnlockHash{}): return errors.New("file contract missed proof output not sent to void") } // check unlock hash uc := types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{renterKey, h.publicKey}, SignaturesRequired: 2, } if fc.UnlockHash != uc.UnlockHash() { return errors.New("bad file contract unlock hash") } return nil }
// TestUploadConstraints checks that file contract negotiation correctly // rejects contracts that don't meet required criteria. func TestUploadConstraints(t *testing.T) { if testing.Short() { t.SkipNow() } ht, err := newHostTester("TestUploadConstraints") if err != nil { t.Fatal(err) } h := ht.host settings := h.Settings() settings.TotalStorage = 10e3 err = h.SetSettings(settings) if err != nil { t.Fatal(err) } // Create a valid file contract transaction. filesize := uint64(5e3) merkleRoot := crypto.Hash{51, 23} windowStart := ht.cs.Height() + 1 + settings.MinDuration windowEnd := ht.cs.Height() + 1 + settings.MinDuration + 1 + settings.WindowSize currencyDuration := types.NewCurrency64(1 + uint64(settings.MinDuration)) payment := types.NewCurrency64(filesize).Mul(settings.Price).Mul(currencyDuration) payout := payment.Mul(types.NewCurrency64(20)) refund := types.PostTax(ht.cs.Height(), payout).Sub(payment) renterKey := types.SiaPublicKey{} txn := types.Transaction{ FileContracts: []types.FileContract{{ FileSize: filesize, FileMerkleRoot: merkleRoot, WindowStart: windowStart, WindowEnd: windowEnd, Payout: payout, ValidProofOutputs: []types.SiacoinOutput{ { Value: refund, UnlockHash: types.UnlockHash{}, }, { Value: payment, UnlockHash: settings.UnlockHash, }, }, MissedProofOutputs: []types.SiacoinOutput{ { Value: refund, UnlockHash: types.UnlockHash{}, }, { Value: payment, UnlockHash: types.UnlockHash{}, }, }, UnlockHash: types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{renterKey, h.publicKey}, SignaturesRequired: 2, }.UnlockHash(), RevisionNumber: 3, }}, } err = h.considerContract(txn, renterKey, filesize, merkleRoot) if err != nil { t.Fatal(err) } // Test that under-paid file contracts get rejected. underPayment := types.NewCurrency64(filesize * 5 / 6).Mul(settings.Price).Mul(currencyDuration) underRefund := types.PostTax(ht.cs.Height(), payout).Sub(underPayment) txn.FileContracts[0].ValidProofOutputs[0].Value = underRefund txn.FileContracts[0].ValidProofOutputs[1].Value = underPayment txn.FileContracts[0].MissedProofOutputs[0].Value = underRefund txn.FileContracts[0].MissedProofOutputs[1].Value = underPayment err = h.considerContract(txn, renterKey, filesize, merkleRoot) if err != ErrLowPayment { t.Fatal(err) } // Test that too-large files get rejected. largeFilesize := uint64(10001) largeFilePayment := types.NewCurrency64(largeFilesize).Mul(settings.Price).Mul(currencyDuration) largeFileRefund := types.PostTax(ht.cs.Height(), payout).Sub(largeFilePayment) txn.FileContracts[0].FileSize = largeFilesize txn.FileContracts[0].ValidProofOutputs[0].Value = largeFileRefund txn.FileContracts[0].ValidProofOutputs[1].Value = largeFilePayment txn.FileContracts[0].MissedProofOutputs[0].Value = largeFileRefund txn.FileContracts[0].MissedProofOutputs[1].Value = largeFilePayment err = h.considerContract(txn, renterKey, largeFilesize, merkleRoot) if err != ErrHostCapacity { t.Fatal(err) } // Reset the file contract to a working contract, and create an obligation // from the transaction. txn.FileContracts[0].FileSize = filesize txn.FileContracts[0].ValidProofOutputs[0].Value = refund txn.FileContracts[0].ValidProofOutputs[1].Value = payment txn.FileContracts[0].MissedProofOutputs[0].Value = refund txn.FileContracts[0].MissedProofOutputs[1].Value = payment obligation := &contractObligation{ ID: txn.FileContractID(0), OriginTransaction: txn, } // Create a legal revision transaction. newFileSize := filesize + uint64(4e3) revisedPayment := payment.Add(types.NewCurrency64(newFileSize - filesize).Mul(currencyDuration).Mul(settings.Price)) revisedRefund := types.PostTax(ht.cs.Height(), payout).Sub(revisedPayment) revisionTxn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{{ ParentID: txn.FileContractID(0), UnlockConditions: types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{renterKey, h.publicKey}, SignaturesRequired: 2, }, NewRevisionNumber: txn.FileContracts[0].RevisionNumber + 1, NewFileSize: newFileSize, NewFileMerkleRoot: merkleRoot, NewWindowStart: windowStart, NewWindowEnd: windowEnd, NewValidProofOutputs: []types.SiacoinOutput{ { Value: revisedRefund, UnlockHash: types.UnlockHash{}, }, { Value: revisedPayment, UnlockHash: settings.UnlockHash, }, }, NewMissedProofOutputs: []types.SiacoinOutput{ { Value: revisedRefund, UnlockHash: types.UnlockHash{}, }, { Value: revisedPayment, UnlockHash: types.UnlockHash{}, }, }, NewUnlockHash: txn.FileContracts[0].UnlockHash, }}, } err = ht.host.considerRevision(revisionTxn, obligation) if err != nil { t.Fatal(err) } // Test that too large revisions get rejected. settings.TotalStorage = 3e3 ht.host.SetSettings(settings) if ht.host.spaceRemaining != 3e3 { t.Fatal("host is not getting the correct space remaining") } err = ht.host.considerRevision(revisionTxn, obligation) if err != ErrHostCapacity { t.Fatal(err) } // Test that file revisions get accepted if the updated file size is too // large but just the added data is small enough (regression test). settings.TotalStorage = 8e3 ht.host.SetSettings(settings) err = ht.host.considerRevision(revisionTxn, obligation) if err != nil { t.Fatal(err) } // Test that underpaid revisions get rejected. revisedUnderPayment := payment.Add(types.NewCurrency64(newFileSize - filesize - 1e3).Mul(currencyDuration).Mul(settings.Price)) revisedUnderRefund := types.PostTax(ht.cs.Height(), payout).Sub(revisedUnderPayment) revisionTxn.FileContractRevisions[0].NewValidProofOutputs[0].Value = revisedUnderRefund revisionTxn.FileContractRevisions[0].NewValidProofOutputs[1].Value = revisedUnderPayment revisionTxn.FileContractRevisions[0].NewMissedProofOutputs[0].Value = revisedUnderRefund revisionTxn.FileContractRevisions[0].NewMissedProofOutputs[1].Value = revisedUnderPayment revisionTxn.FileContractRevisions[0].NewMissedProofOutputs[1].Value = revisedUnderPayment revisionTxn.FileContractRevisions[0].NewMissedProofOutputs[1].Value = revisedUnderPayment err = ht.host.considerRevision(revisionTxn, obligation) if err != ErrLowPayment { t.Fatal(err) } }
// Renew negotiates a new contract for data already stored with a host. It // returns the ID of the new contract. This is a blocking call that performs // network I/O. func (hdb *HostDB) Renew(fcid types.FileContractID, newEndHeight types.BlockHeight) (types.FileContractID, error) { hdb.mu.RLock() height := hdb.blockHeight hc, ok := hdb.contracts[fcid] host, eok := hdb.allHosts[hc.IP] hdb.mu.RUnlock() if !ok { return types.FileContractID{}, errors.New("no record of that contract") } else if !eok { return types.FileContractID{}, errors.New("no record of that host") } else if newEndHeight < height { return types.FileContractID{}, errors.New("cannot renew below current height") } else if host.Price.Cmp(maxPrice) > 0 { return types.FileContractID{}, errTooExpensive } // get an address to use for negotiation hdb.mu.Lock() if hdb.cachedAddress == (types.UnlockHash{}) { uc, err := hdb.wallet.NextAddress() if err != nil { hdb.mu.Unlock() return types.FileContractID{}, err } hdb.cachedAddress = uc.UnlockHash() } ourAddress := hdb.cachedAddress hdb.mu.Unlock() renterCost := host.Price.Mul(types.NewCurrency64(hc.LastRevision.NewFileSize)).Mul(types.NewCurrency64(uint64(newEndHeight - height))) renterCost = renterCost.MulFloat(1.05) // extra buffer to guarantee we won't run out of money during revision payout := renterCost // no collateral // create file contract fc := types.FileContract{ FileSize: hc.LastRevision.NewFileSize, FileMerkleRoot: hc.LastRevision.NewFileMerkleRoot, WindowStart: newEndHeight, WindowEnd: newEndHeight + host.WindowSize, Payout: payout, UnlockHash: types.UnlockHash{}, // to be filled in by negotiateContract RevisionNumber: 0, ValidProofOutputs: []types.SiacoinOutput{ // nothing returned to us; everything goes to the host {Value: types.ZeroCurrency, UnlockHash: ourAddress}, {Value: types.PostTax(height, renterCost), UnlockHash: host.UnlockHash}, }, MissedProofOutputs: []types.SiacoinOutput{ // nothing returned to us; everything goes to the void {Value: types.ZeroCurrency, UnlockHash: ourAddress}, {Value: types.PostTax(height, renterCost), UnlockHash: types.UnlockHash{}}, }, } // create transaction builder txnBuilder := hdb.wallet.StartTransaction() // initiate connection conn, err := hdb.dialer.DialTimeout(hc.IP, 15*time.Second) if err != nil { return types.FileContractID{}, err } defer conn.Close() if err := encoding.WriteObject(conn, modules.RPCRenew); err != nil { return types.FileContractID{}, errors.New("couldn't initiate RPC: " + err.Error()) } if err := encoding.WriteObject(conn, fcid); err != nil { return types.FileContractID{}, errors.New("couldn't send contract ID: " + err.Error()) } // execute negotiation protocol newContract, err := negotiateContract(conn, hc.IP, fc, txnBuilder, hdb.tpool) if err != nil { txnBuilder.Drop() // return unused outputs to wallet return types.FileContractID{}, err } // update host contract hdb.mu.Lock() hdb.contracts[newContract.ID] = newContract hdb.cachedAddress = types.UnlockHash{} // clear cachedAddress err = hdb.save() hdb.mu.Unlock() if err != nil { hdb.log.Println("WARN: failed to save the hostdb:", err) } return newContract.ID, nil }
// newContract negotiates an initial file contract with the specified host // and returns a hostContract. The contract is also saved by the HostDB. func (hdb *HostDB) newContract(host modules.HostSettings, filesize uint64, duration types.BlockHeight) (hostContract, error) { // reject hosts that are too expensive if host.Price.Cmp(maxPrice) > 0 { return hostContract{}, errTooExpensive } // get an address to use for negotiation hdb.mu.Lock() if hdb.cachedAddress == (types.UnlockHash{}) { uc, err := hdb.wallet.NextAddress() if err != nil { hdb.mu.Unlock() return hostContract{}, err } hdb.cachedAddress = uc.UnlockHash() } ourAddress := hdb.cachedAddress hdb.mu.Unlock() // create file contract renterCost := host.Price.Mul(types.NewCurrency64(filesize)).Mul(types.NewCurrency64(uint64(duration))) renterCost = renterCost.MulFloat(1.05) // extra buffer to guarantee we won't run out of money during revision payout := renterCost // no collateral hdb.mu.RLock() height := hdb.blockHeight hdb.mu.RUnlock() fc := types.FileContract{ FileSize: 0, FileMerkleRoot: crypto.Hash{}, // no proof possible without data WindowStart: height + duration, WindowEnd: height + duration + host.WindowSize, Payout: payout, UnlockHash: types.UnlockHash{}, // to be filled in by negotiateContract RevisionNumber: 0, ValidProofOutputs: []types.SiacoinOutput{ // outputs need to account for tax {Value: types.PostTax(height, renterCost), UnlockHash: ourAddress}, // no collateral {Value: types.ZeroCurrency, UnlockHash: host.UnlockHash}, }, MissedProofOutputs: []types.SiacoinOutput{ // same as above {Value: types.PostTax(height, renterCost), UnlockHash: ourAddress}, // goes to the void, not the renter {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, }, } // create transaction builder txnBuilder := hdb.wallet.StartTransaction() // initiate connection conn, err := hdb.dialer.DialTimeout(host.NetAddress, 15*time.Second) if err != nil { return hostContract{}, err } defer conn.Close() if err := encoding.WriteObject(conn, modules.RPCUpload); err != nil { return hostContract{}, err } // execute negotiation protocol contract, err := negotiateContract(conn, host.NetAddress, fc, txnBuilder, hdb.tpool) if err != nil { txnBuilder.Drop() // return unused outputs to wallet return hostContract{}, err } hdb.mu.Lock() hdb.contracts[contract.ID] = contract // clear the cached address hdb.cachedAddress = types.UnlockHash{} hdb.save() hdb.mu.Unlock() return contract, nil }
// TestPartialConfirmation checks that the transaction pool correctly accepts a // transaction set which has parents that have been accepted by the consensus // set but not the whole set has been accepted by the consensus set. func TestPartialConfirmation(t *testing.T) { if testing.Short() { t.SkipNow() } tpt, err := createTpoolTester("TestPartialConfirmation") if err != nil { t.Fatal(err) } defer tpt.Close() // Create and fund a valid file contract. builder := tpt.wallet.StartTransaction() payout := types.NewCurrency64(1e9) err = builder.FundSiacoins(payout) if err != nil { t.Fatal(err) } builder.AddFileContract(types.FileContract{ WindowStart: tpt.cs.Height() + 2, WindowEnd: tpt.cs.Height() + 5, Payout: payout, ValidProofOutputs: []types.SiacoinOutput{{Value: types.PostTax(tpt.cs.Height(), payout)}}, MissedProofOutputs: []types.SiacoinOutput{{Value: types.PostTax(tpt.cs.Height(), payout)}}, UnlockHash: types.UnlockConditions{}.UnlockHash(), }) tSet, err := builder.Sign(true) if err != nil { t.Fatal(err) } fcid := tSet[len(tSet)-1].FileContractID(0) // Create a file contract revision. rSet := []types.Transaction{{ FileContractRevisions: []types.FileContractRevision{{ ParentID: fcid, NewRevisionNumber: 2, NewWindowStart: tpt.cs.Height() + 2, NewWindowEnd: tpt.cs.Height() + 5, NewValidProofOutputs: []types.SiacoinOutput{{Value: types.PostTax(tpt.cs.Height(), payout)}}, NewMissedProofOutputs: []types.SiacoinOutput{{Value: types.PostTax(tpt.cs.Height(), payout)}}, }}, }} // Combine the contract and revision in to a single set. fullSet := append(tSet, rSet...) // Get the tSet onto the blockchain. unsolvedBlock, target, err := tpt.miner.BlockForWork() if err != nil { t.Fatal(err) } unsolvedBlock.Transactions = append(unsolvedBlock.Transactions, tSet...) solvedBlock, solved := tpt.miner.SolveBlock(unsolvedBlock, target) if !solved { t.Fatal("Failed to solve block") } err = tpt.cs.AcceptBlock(solvedBlock) if err != nil { t.Fatal(err) } // Try to get the full set into the transaction pool. The transaction pool // should recognize that the set is partially accepted, and be able to // accept on the the transactions that are new and are not yet on the // blockchain. err = tpt.tpool.AcceptTransactionSet(fullSet) if err != nil { t.Fatal(err) } }
// testMissedStorageProofBlocks adds a block with a file contract, and then // fails to submit a storage proof before expiration. func (cst *consensusSetTester) testMissedStorageProofBlocks() { // Create a file contract that will be successful. filesize := uint64(4e3) payout := types.NewCurrency64(400e6) missedProofDest := randAddress() fc := types.FileContract{ FileSize: filesize, FileMerkleRoot: crypto.Hash{}, WindowStart: cst.cs.dbBlockHeight() + 1, WindowEnd: cst.cs.dbBlockHeight() + 2, Payout: payout, ValidProofOutputs: []types.SiacoinOutput{{ UnlockHash: types.UnlockHash{}, Value: types.PostTax(cst.cs.dbBlockHeight(), payout), }}, MissedProofOutputs: []types.SiacoinOutput{{ UnlockHash: missedProofDest, Value: types.PostTax(cst.cs.dbBlockHeight(), payout), }}, } // Submit a transaction with the file contract. oldSiafundPool := cst.cs.dbGetSiafundPool() txnBuilder := cst.wallet.StartTransaction() err := txnBuilder.FundSiacoins(payout) if err != nil { panic(err) } fcIndex := txnBuilder.AddFileContract(fc) txnSet, err := txnBuilder.Sign(true) if err != nil { panic(err) } err = cst.tpool.AcceptTransactionSet(txnSet) if err != nil { panic(err) } _, err = cst.miner.AddBlock() if err != nil { panic(err) } // Check that the siafund pool was increased by the tax on the payout. siafundPool := cst.cs.dbGetSiafundPool() if siafundPool.Cmp(oldSiafundPool.Add(types.Tax(cst.cs.dbBlockHeight()-1, payout))) != 0 { panic("siafund pool was not increased correctly") } // Check that the file contract made it into the database. ti := len(txnSet) - 1 fcid := txnSet[ti].FileContractID(fcIndex) _, err = cst.cs.dbGetFileContract(fcid) if err != nil { panic(err) } // Mine a block to close the storage proof window. _, err = cst.miner.AddBlock() if err != nil { panic(err) } // Check that the file contract has been removed. _, err = cst.cs.dbGetFileContract(fcid) if err != errNilItem { panic("file contract should not exist in the database") } // Check that the siafund pool has not changed. postProofPool := cst.cs.dbGetSiafundPool() if postProofPool.Cmp(siafundPool) != 0 { panic("siafund pool should not change after submitting a storage proof") } // Check that a delayed output was created for the missed proof. spoid := fcid.StorageProofOutputID(types.ProofMissed, 0) dsco, err := cst.cs.dbGetDSCO(cst.cs.dbBlockHeight()+types.MaturityDelay, spoid) if err != nil { panic(err) } if dsco.UnlockHash != fc.MissedProofOutputs[0].UnlockHash { panic("wrong unlock hash in dsco") } if dsco.Value.Cmp(fc.MissedProofOutputs[0].Value) != 0 { panic("wrong sco value in dsco") } }