// createContractTransaction takes contract terms and a merkle root and uses // them to build a transaction containing a file contract that satisfies the // terms, including providing an input balance. The transaction does not get // signed. func (r *Renter) createContractTransaction(terms modules.ContractTerms, merkleRoot crypto.Hash) (txn types.Transaction, txnBuilder modules.TransactionBuilder, err error) { // Get the payout as set by the missed proofs, and the client fund as determined by the terms. sizeCurrency := types.NewCurrency64(terms.FileSize) durationCurrency := types.NewCurrency64(uint64(terms.Duration)) clientCost := terms.Price.Mul(sizeCurrency).Mul(durationCurrency) hostCollateral := terms.Collateral.Mul(sizeCurrency).Mul(durationCurrency) payout := clientCost.Add(hostCollateral) // Fill out the contract. contract := types.FileContract{ FileMerkleRoot: merkleRoot, FileSize: terms.FileSize, WindowStart: terms.DurationStart + terms.Duration, WindowEnd: terms.DurationStart + terms.Duration + terms.WindowSize, Payout: payout, ValidProofOutputs: terms.ValidProofOutputs, MissedProofOutputs: terms.MissedProofOutputs, } // Create the transaction. txnBuilder = r.wallet.RegisterTransaction(txn, nil) err = txnBuilder.FundSiacoins(clientCost) if err != nil { return } txnBuilder.AddFileContract(contract) txn, _ = txnBuilder.View() return }
// StoragePriceToConsensus converts a human storage price, having the unit // 'Siacoins Per Month Per Terabyte', to a consensus storage price, having the // unit 'Hastings Per Block Per Byte'. func StoragePriceToConsensus(siacoinsMonthTB uint64) (hastingsBlockByte types.Currency) { // Perform multiplication first to preserve precision. hastingsMonthTB := types.NewCurrency64(siacoinsMonthTB).Mul(types.SiacoinPrecision) hastingsBlockTB := hastingsMonthTB.Div(types.NewCurrency64(4320)) hastingsBlockByte = hastingsBlockTB.Div(types.NewCurrency64(1e12)) return hastingsBlockByte }
// TestApplySiacoinOutputs probes the applySiacoinOutput method of the // consensus set. func TestApplySiacoinOutputs(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestApplySiacoinOutputs") if err != nil { t.Fatal(err) } // Create a block node to use with application. bn := new(blockNode) // Apply a transaction with a single siacoin output. txn := types.Transaction{ SiacoinOutputs: []types.SiacoinOutput{{}}, } cst.cs.applySiacoinOutputs(bn, txn) scoid := txn.SiacoinOutputID(0) _, exists := cst.cs.siacoinOutputs[scoid] if !exists { t.Error("Failed to create siacoin output") } if len(cst.cs.siacoinOutputs) != 3 { // 3 because createConsensusSetTester has 2 initially. t.Error("siacoin outputs not correctly updated") } if len(bn.siacoinOutputDiffs) != 1 { t.Error("block node was not updated for single element transaction") } if bn.siacoinOutputDiffs[0].Direction != modules.DiffApply { t.Error("wrong diff direction applied when creating a siacoin output") } if bn.siacoinOutputDiffs[0].ID != scoid { t.Error("wrong id used when creating a siacoin output") } // Apply a transaction with 2 siacoin outputs. txn = types.Transaction{ SiacoinOutputs: []types.SiacoinOutput{ {Value: types.NewCurrency64(1)}, {Value: types.NewCurrency64(2)}, }, } cst.cs.applySiacoinOutputs(bn, txn) scoid0 := txn.SiacoinOutputID(0) scoid1 := txn.SiacoinOutputID(1) _, exists = cst.cs.siacoinOutputs[scoid0] if !exists { t.Error("Failed to create siacoin output") } _, exists = cst.cs.siacoinOutputs[scoid1] if !exists { t.Error("Failed to create siacoin output") } if len(cst.cs.siacoinOutputs) != 5 { // 5 because createConsensusSetTester has 2 initially. t.Error("siacoin outputs not correctly updated") } if len(bn.siacoinOutputDiffs) != 3 { t.Error("block node was not updated correctly") } }
// TestWeightedList inserts and removes nodes in a semi-random manner and // verifies that the tree stays consistent through the adjustments. func TestWeightedList(t *testing.T) { // Create a hostdb and 3 equal entries to insert. hdbt := newHDBTester("TestWeightedList", t) // Create a bunch of host entries of equal weight. firstInsertions := 64 for i := 0; i < firstInsertions; i++ { entry := hostEntry{ HostSettings: modules.HostSettings{IPAddress: fakeAddr(uint8(i))}, weight: types.NewCurrency64(10), } hdbt.hostdb.insertNode(&entry) } err := hdbt.uniformTreeVerification(firstInsertions) if err != nil { t.Error(err) } // Remove a few hosts and check that the tree is still in order. removals := 12 // Keep a map of what we've removed so far. removedMap := make(map[uint8]struct{}) for i := 0; i < removals; i++ { // Try numbers until we roll a number that's not been removed yet. var randInt uint8 for { randBig, err := rand.Int(rand.Reader, big.NewInt(int64(firstInsertions))) if err != nil { t.Fatal(err) } randInt = uint8(randBig.Int64()) _, exists := removedMap[randInt] if !exists { break } } // Remove the entry and add it to the list of removed entries err := hdbt.hostdb.RemoveHost(fakeAddr(randInt)) if err != nil { t.Fatal(err) } removedMap[randInt] = struct{}{} } err = hdbt.uniformTreeVerification(firstInsertions - removals) if err != nil { t.Error(err) } // Do some more insertions. secondInsertions := 64 for i := firstInsertions; i < firstInsertions+secondInsertions; i++ { entry := hostEntry{ HostSettings: modules.HostSettings{IPAddress: fakeAddr(uint8(i))}, weight: types.NewCurrency64(10), } hdbt.hostdb.insertNode(&entry) } hdbt.uniformTreeVerification(firstInsertions - removals + secondInsertions) }
// TestActiveHosts tests the ActiveHosts method. func TestActiveHosts(t *testing.T) { hdb := bareHostDB() // empty if hosts := hdb.ActiveHosts(); len(hosts) != 0 { t.Errorf("wrong number of hosts: expected %v, got %v", 0, len(hosts)) } // with one host h1 := new(hostEntry) h1.NetAddress = "foo" h1.Weight = types.NewCurrency64(1) h1.AcceptingContracts = true hdb.insertNode(h1) if hosts := hdb.ActiveHosts(); len(hosts) != 1 { t.Errorf("wrong number of hosts: expected %v, got %v", 1, len(hosts)) } else if hosts[0].NetAddress != h1.NetAddress { t.Errorf("ActiveHosts returned wrong host: expected %v, got %v", h1.NetAddress, hosts[0].NetAddress) } // with multiple hosts h2 := new(hostEntry) h2.NetAddress = "bar" h2.Weight = types.NewCurrency64(1) h2.AcceptingContracts = true hdb.insertNode(h2) if hosts := hdb.ActiveHosts(); len(hosts) != 2 { t.Errorf("wrong number of hosts: expected %v, got %v", 2, len(hosts)) } else if hosts[0].NetAddress != h1.NetAddress && hosts[1].NetAddress != h1.NetAddress { t.Errorf("ActiveHosts did not contain an inserted host: %v (missing %v)", hosts, h1.NetAddress) } else if hosts[0].NetAddress != h2.NetAddress && hosts[1].NetAddress != h2.NetAddress { t.Errorf("ActiveHosts did not contain an inserted host: %v (missing %v)", hosts, h2.NetAddress) } }
// checkSiacoins counts the number of siacoins in the database and verifies // that it matches the sum of all the coinbases. func (cs *ConsensusSet) checkSiacoins() error { // Calculate the number of expected coins in constant time. deflationBlocks := types.InitialCoinbase - types.MinimumCoinbase expectedSiacoins := types.CalculateCoinbase(0).Add(types.CalculateCoinbase(cs.height())).Div(types.NewCurrency64(2)) if cs.height() < types.BlockHeight(deflationBlocks) { expectedSiacoins = expectedSiacoins.Mul(types.NewCurrency64(uint64(cs.height()) + 1)) } else { expectedSiacoins = expectedSiacoins.Mul(types.NewCurrency64(deflationBlocks + 1)) trailingSiacoins := types.NewCurrency64(uint64(cs.height()) - deflationBlocks).Mul(types.CalculateCoinbase(cs.height())) expectedSiacoins = expectedSiacoins.Add(trailingSiacoins) } totalSiacoins := types.ZeroCurrency cs.db.forEachSiacoinOutputs(func(scoid types.SiacoinOutputID, sco types.SiacoinOutput) { totalSiacoins = totalSiacoins.Add(sco.Value) }) cs.db.forEachFileContracts(func(fcid types.FileContractID, fc types.FileContract) { var payout types.Currency for _, output := range fc.ValidProofOutputs { payout = payout.Add(output.Value) } totalSiacoins = totalSiacoins.Add(payout) }) cs.db.forEachDelayedSiacoinOutputs(func(v types.SiacoinOutputID, dso types.SiacoinOutput) { totalSiacoins = totalSiacoins.Add(dso.Value) }) cs.db.forEachSiafundOutputs(func(sfoid types.SiafundOutputID, sfo types.SiafundOutput) { sfoSiacoins := cs.siafundPool.Sub(sfo.ClaimStart).Div(types.SiafundCount).Mul(sfo.Value) totalSiacoins = totalSiacoins.Add(sfoSiacoins) }) if expectedSiacoins.Cmp(totalSiacoins) != 0 { return errSiacoinMiscount } return nil }
// TestRepeatInsert inserts 2 hosts with the same address. func TestRepeatInsert(t *testing.T) { if testing.Short() { t.SkipNow() } hdb := &HostDB{ activeHosts: make(map[modules.NetAddress]*hostNode), allHosts: make(map[modules.NetAddress]*hostEntry), scanPool: make(chan *hostEntry, scanPoolSize), } var dbe modules.HostDBEntry dbe.NetAddress = fakeAddr(0) entry1 := hostEntry{ HostDBEntry: dbe, Weight: types.NewCurrency64(1), } entry2 := entry1 hdb.insertNode(&entry1) entry2.Weight = types.NewCurrency64(100) hdb.insertNode(&entry2) if len(hdb.activeHosts) != 1 { t.Error("insterting the same entry twice should result in only 1 entry in the hostdb") } }
// checkWalletBalance looks at an upload and determines if there is enough // money in the wallet to support such an upload. An error is returned if it is // determined that there is not enough money. func (r *Renter) checkWalletBalance(up modules.FileUploadParams) error { // Get the size of the file. fileInfo, err := os.Stat(up.Filename) if err != nil { return err } curSize := types.NewCurrency64(uint64(fileInfo.Size())) var averagePrice types.Currency sampleSize := up.ErasureCode.NumPieces() * 3 / 2 hosts := r.hostDB.RandomHosts(sampleSize) for _, host := range hosts { averagePrice = averagePrice.Add(host.Price) } if len(hosts) == 0 { return errors.New("no hosts!") } averagePrice = averagePrice.Div(types.NewCurrency64(uint64(len(hosts)))) estimatedCost := averagePrice.Mul(types.NewCurrency64(uint64(up.Duration))).Mul(curSize) bufferedCost := estimatedCost.Mul(types.NewCurrency64(2)) siacoinBalance, _, _ := r.wallet.ConfirmedBalance() if bufferedCost.Cmp(siacoinBalance) > 0 { return errors.New("insufficient balance for upload") } return nil }
// TestAveragePrice tests the AveragePrice method, which also depends on the // randomHosts method. func TestAveragePrice(t *testing.T) { hdb := bareHostDB() // empty if avg := hdb.AveragePrice(); !avg.IsZero() { t.Error("average of empty hostdb should be zero:", avg) } // with one host h1 := new(hostEntry) h1.NetAddress = "foo" h1.Price = types.NewCurrency64(100) h1.weight = baseWeight hdb.insertNode(h1) if avg := hdb.AveragePrice(); avg.Cmp(h1.Price) != 0 { t.Error("average of one host should be that host's price:", avg) } // with two hosts h2 := new(hostEntry) h2.NetAddress = "bar" h2.Price = types.NewCurrency64(300) h2.weight = baseWeight hdb.insertNode(h2) if len(hdb.activeHosts) != 2 { t.Error("host was not added:", hdb.activeHosts) } if avg := hdb.AveragePrice(); avg.Cmp(types.NewCurrency64(200)) != 0 { t.Error("average of two hosts should be their sum/2:", avg) } }
// TestRepeatInsert inserts 2 hosts with the same address. func TestRepeatInsert(t *testing.T) { if testing.Short() { t.SkipNow() } hdb := &HostDB{ activeHosts: make(map[modules.NetAddress]*hostNode), allHosts: make(map[modules.NetAddress]*hostEntry), scanPool: make(chan *hostEntry, scanPoolSize), mu: sync.New(modules.SafeMutexDelay, 1), } entry1 := hostEntry{ HostSettings: modules.HostSettings{IPAddress: fakeAddr(0)}, weight: types.NewCurrency64(1), } entry2 := entry1 hdb.insertNode(&entry1) entry2.weight = types.NewCurrency64(100) hdb.insertNode(&entry2) if len(hdb.activeHosts) != 1 { t.Error("insterting the same entry twice should result in only 1 entry in the hostdb") } }
// Info returns generic information about the renter and the files that are // being rented. func (r *Renter) Info() (ri modules.RentInfo) { lockID := r.mu.RLock() defer r.mu.RUnlock(lockID) // Include the list of files the renter knows about. for filename := range r.files { ri.Files = append(ri.Files, filename) } // Calculate the average cost of a file. var totalPrice types.Currency redundancy := 6 // reasonable estimate until we come up with an alternative sampleSize := redundancy * 3 hosts := r.hostDB.RandomHosts(sampleSize) for _, host := range hosts { totalPrice = totalPrice.Add(host.Price) } if len(hosts) == 0 { return } averagePrice := totalPrice.Div(types.NewCurrency64(uint64(len(hosts)))).Mul(types.NewCurrency64(uint64(redundancy))) // HACK: 6000 is the duration (set by the API), and 1024^3 is a GB. Price // is reported as per GB, no timeframe is given. estimatedCost := averagePrice.Mul(types.NewCurrency64(6000)).Mul(types.NewCurrency64(1024 * 1024 * 1024)) bufferedCost := estimatedCost.Mul(types.NewCurrency64(3)) // For some reason, this estimate can still be off by a large factor. ri.Price = bufferedCost // Report the number of known hosts. ri.KnownHosts = len(r.hostDB.ActiveHosts()) return }
// Info returns generic information about the renter and the files that are // being rented. func (r *Renter) Info() (ri modules.RentInfo) { lockID := r.mu.RLock() // Include the list of files the renter knows about. for filename := range r.files { ri.Files = append(ri.Files, filename) } r.mu.RUnlock(lockID) // Calculate the average cost of a file. var totalPrice types.Currency sampleSize := defaultParityPieces + defaultDataPieces hosts := r.hostDB.RandomHosts(sampleSize) for _, host := range hosts { totalPrice = totalPrice.Add(host.Price) } if len(hosts) == 0 { return } averagePrice := totalPrice.Div(types.NewCurrency64(uint64(len(hosts)))) estimatedCost := averagePrice.Mul(types.NewCurrency64(defaultDuration)).Mul(types.NewCurrency64(1e9)).Mul(types.NewCurrency64(defaultParityPieces + defaultDataPieces)) // this also accounts for the buffering in the contract negotiation bufferedCost := estimatedCost.Mul(types.NewCurrency64(5)).Div(types.NewCurrency64(2)) ri.Price = bufferedCost // Report the number of known hosts. ri.KnownHosts = len(r.hostDB.ActiveHosts()) return }
// TestDecrementReliability tests the decrementReliability method. func TestDecrementReliability(t *testing.T) { hdb := bareHostDB() // Decrementing a non-existent host should be a no-op. // NOTE: can't check any post-conditions here; only indication of correct // behavior is that the test doesn't panic. hdb.decrementReliability("foo", types.NewCurrency64(0)) // Add a host to allHosts and activeHosts. Decrementing it should remove it // from activeHosts. h := new(hostEntry) h.NetAddress = "foo" h.reliability = types.NewCurrency64(1) hdb.allHosts[h.NetAddress] = h hdb.activeHosts[h.NetAddress] = &hostNode{hostEntry: h} hdb.decrementReliability(h.NetAddress, types.NewCurrency64(0)) if len(hdb.ActiveHosts()) != 0 { t.Error("decrementing did not remove host from activeHosts") } // Decrement reliability to 0. This should remove the host from allHosts. hdb.decrementReliability(h.NetAddress, h.reliability) if len(hdb.AllHosts()) != 0 { t.Error("decrementing did not remove host from allHosts") } }
// Upload revises an existing file contract with a host, and then uploads a // piece to it. func (hu *hostUploader) Upload(data []byte) (uint64, error) { // offset is old filesize offset := hu.contract.LastRevision.NewFileSize // calculate price hu.hdb.mu.RLock() height := hu.hdb.blockHeight hu.hdb.mu.RUnlock() if height > hu.contract.FileContract.WindowStart { return 0, errors.New("contract has already ended") } piecePrice := types.NewCurrency64(uint64(len(data))).Mul(types.NewCurrency64(uint64(hu.contract.FileContract.WindowStart - height))).Mul(hu.price) piecePrice = piecePrice.MulFloat(1.02) // COMPATv0.4.8 -- hosts reject exact prices // calculate new merkle root (no error possible with bytes.Reader) _ = hu.tree.ReadSegments(bytes.NewReader(data)) merkleRoot := hu.tree.Root() // revise the file contract rev := newRevision(hu.contract.LastRevision, uint64(len(data)), merkleRoot, piecePrice) signedTxn, err := negotiateRevision(hu.conn, rev, data, hu.contract.SecretKey) if err != nil { return 0, err } // update host contract hu.contract.LastRevision = rev hu.contract.LastRevisionTxn = signedTxn hu.hdb.mu.Lock() hu.hdb.contracts[hu.contract.ID] = hu.contract hu.hdb.save() hu.hdb.mu.Unlock() return offset, nil }
// TestHostWeight probes the hostWeight function. func TestHostWeight(t *testing.T) { hdbt := newHDBTester("TestHostWeight", t) // Create two identical entries, except that one has a price that is 2x the // other. The weight returned by hostWeight should be 1/8 for the more // expensive host. entry1 := hostEntry{ HostSettings: modules.HostSettings{ Price: types.NewCurrency64(3), }, } entry2 := hostEntry{ HostSettings: modules.HostSettings{ Price: types.NewCurrency64(6), }, } weight1 := hdbt.hostdb.hostWeight(entry1) weight2 := hdbt.hostdb.hostWeight(entry2) expectedWeight := weight1.Div(types.NewCurrency64(8)) if weight2.Cmp(expectedWeight) != 0 { t.Error("Weight of expensive host is not the correct value.") } // Try a 0 price. entry3 := hostEntry{ HostSettings: modules.HostSettings{ Price: types.NewCurrency64(0), }, } weight3 := hdbt.hostdb.hostWeight(entry3) if weight3.Cmp(weight1) <= 0 { t.Error("Free host not weighing fairly") } }
// TestSendSiacoins probes the SendSiacoins method of the wallet. func TestSendSiacoins(t *testing.T) { if testing.Short() { t.SkipNow() } wt, err := createWalletTester("TestSendSiacoins") if err != nil { t.Fatal(err) } defer wt.closeWt() // Get the initial balance - should be 1 block. The unconfirmed balances // should be 0. confirmedBal, _, _ := wt.wallet.ConfirmedBalance() unconfirmedOut, unconfirmedIn := wt.wallet.UnconfirmedBalance() if confirmedBal.Cmp(types.CalculateCoinbase(1)) != 0 { t.Error("unexpected confirmed balance") } if unconfirmedOut.Cmp(types.ZeroCurrency) != 0 { t.Error("unconfirmed balance should be 0") } if unconfirmedIn.Cmp(types.ZeroCurrency) != 0 { t.Error("unconfirmed balance should be 0") } // Send 5000 hastings. The wallet will automatically add a fee. Outgoing // unconfirmed siacoins - incoming unconfirmed siacoins should equal 5000 + // fee. tpoolFee := types.NewCurrency64(10).Mul(types.SiacoinPrecision) _, err = wt.wallet.SendSiacoins(types.NewCurrency64(5000), types.UnlockHash{}) if err != nil { t.Fatal(err) } confirmedBal2, _, _ := wt.wallet.ConfirmedBalance() unconfirmedOut2, unconfirmedIn2 := wt.wallet.UnconfirmedBalance() if confirmedBal2.Cmp(confirmedBal) != 0 { t.Error("confirmed balance changed without introduction of blocks") } if unconfirmedOut2.Cmp(unconfirmedIn2.Add(types.NewCurrency64(5000)).Add(tpoolFee)) != 0 { t.Error("sending siacoins appears to be ineffective") } // Move the balance into the confirmed set. b, _ := wt.miner.FindBlock() err = wt.cs.AcceptBlock(b) if err != nil { t.Fatal(err) } confirmedBal3, _, _ := wt.wallet.ConfirmedBalance() unconfirmedOut3, unconfirmedIn3 := wt.wallet.UnconfirmedBalance() if confirmedBal3.Cmp(confirmedBal2.Add(types.CalculateCoinbase(2)).Sub(types.NewCurrency64(5000)).Sub(tpoolFee)) != 0 { t.Error("confirmed balance did not adjust to the expected value") } if unconfirmedOut3.Cmp(types.ZeroCurrency) != 0 { t.Error("unconfirmed balance should be 0") } if unconfirmedIn3.Cmp(types.ZeroCurrency) != 0 { t.Error("unconfirmed balance should be 0") } }
// TestCheckMinerPayouts probes the checkMinerPayouts function. func TestCheckMinerPayouts(t *testing.T) { // All tests are done at height = 0. coinbase := types.CalculateCoinbase(0) // Create a block with a single valid payout. b := types.Block{ MinerPayouts: []types.SiacoinOutput{ {Value: coinbase}, }, } if !checkMinerPayouts(b, 0) { t.Error("payouts evaluated incorrectly when there is only one payout.") } // Try a block with an incorrect payout. b = types.Block{ MinerPayouts: []types.SiacoinOutput{ {Value: coinbase.Sub(types.NewCurrency64(1))}, }, } if checkMinerPayouts(b, 0) { t.Error("payouts evaluated incorrectly when there is a too-small payout") } // Try a block with 2 payouts. b = types.Block{ MinerPayouts: []types.SiacoinOutput{ {Value: coinbase.Sub(types.NewCurrency64(1))}, {Value: types.NewCurrency64(1)}, }, } if !checkMinerPayouts(b, 0) { t.Error("payouts evaluated incorrectly when there are 2 payouts") } // Try a block with 2 payouts that are too large. b = types.Block{ MinerPayouts: []types.SiacoinOutput{ {Value: coinbase}, {Value: coinbase}, }, } if checkMinerPayouts(b, 0) { t.Error("payouts evaluated incorrectly when there are two large payouts") } // Create a block with an empty payout. b = types.Block{ MinerPayouts: []types.SiacoinOutput{ {Value: coinbase}, {}, }, } if checkMinerPayouts(b, 0) { t.Error("payouts evaluated incorrectly when there is only one payout.") } }
// 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 }
// hostWeight returns the weight of a host according to the settings of the // host database. Currently, only the price is considered. func (hdb *HostDB) hostWeight(entry hostEntry) (weight types.Currency) { // Prevent a divide by zero error by making sure the price is at least one. price := entry.Price if price.Cmp(types.NewCurrency64(0)) <= 0 { price = types.NewCurrency64(1) } // Divide the base weight by the cube of the price. return baseWeight.Div(price).Div(price).Div(price) }
// TestIntegrationSortedOutputsSorting checks that the outputs are being correctly sorted // by the currency value. func TestIntegrationSortedOutputsSorting(t *testing.T) { if testing.Short() { t.SkipNow() } so := sortedOutputs{ ids: []types.SiacoinOutputID{{0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}}, outputs: []types.SiacoinOutput{ {Value: types.NewCurrency64(2)}, {Value: types.NewCurrency64(3)}, {Value: types.NewCurrency64(4)}, {Value: types.NewCurrency64(7)}, {Value: types.NewCurrency64(6)}, {Value: types.NewCurrency64(0)}, {Value: types.NewCurrency64(1)}, {Value: types.NewCurrency64(5)}, }, } sort.Sort(so) expectedIDSorting := []types.SiacoinOutputID{{5}, {6}, {0}, {1}, {2}, {7}, {4}, {3}} for i := uint64(0); i < 8; i++ { if so.ids[i] != expectedIDSorting[i] { t.Error("an id is out of place: ", i) } if so.outputs[i].Value.Cmp(types.NewCurrency64(i)) != 0 { t.Error("a value is out of place: ", i) } } }
// testFundTransaction funds and completes a transaction using the // build-your-own transaction functions, checking that a no-refund transaction // is created that is valid. func (wt *walletTester) testFundTransaction() error { // Build a transaction that intentionally needs a refund. id, err := wt.wallet.RegisterTransaction(types.Transaction{}) fund := wt.wallet.Balance(false).Sub(types.NewCurrency64(1)) if err != nil { return err } _, err = wt.wallet.FundTransaction(id, fund) if err != nil { return err } wt.tpUpdateWait() _, _, err = wt.wallet.AddMinerFee(id, fund) if err != nil { return err } t, err := wt.wallet.SignTransaction(id, true) if err != nil { return err } err = wt.tpool.AcceptTransaction(t) if err != nil { return err } wt.tpUpdateWait() // Check that the length of the created transaction is 1 siacoin, and that // the unconfirmed balance of the wallet is 1. if len(t.SiacoinOutputs) != 0 { return errors.New("expecting 0 siacoin outputs, got non-zero result") } if wt.wallet.Balance(true).Cmp(types.NewCurrency64(1)) != 0 { return errors.New("incorrect balance being reported") } // Dump the transaction pool into a block and see that the balance still // registers correctly. b, _ := wt.miner.FindBlock() err = wt.cs.AcceptBlock(b) if err != nil { return err } wt.csUpdateWait() // Check that the length of the created transaction is 1 siacoin, and that // the unconfirmed balance of the wallet is 1 + BlockReward. if len(t.SiacoinOutputs) != 0 { return errors.New("wrong number of siacoin outputs - expecting 0") } expectedBalance := types.CalculateCoinbase(2).Add(types.NewCurrency64(1)) if bal := wt.wallet.Balance(true); bal.Cmp(expectedBalance) != 0 { return errors.New("did not arrive at the expected balance") } 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 := fc.Payout.Sub(fc.Tax()) 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 }
// TestApplyFileContractMaintenance probes the applyFileContractMaintenance // method of the consensus set. func TestApplyFileContractMaintenance(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestApplyMissedStorageProof") if err != nil { t.Fatal(err) } defer cst.closeCst() // Create a block node. pb := new(processedBlock) pb.Height = cst.cs.height() // Create a file contract that's expiring and has 1 missed proof output. expiringFC := types.FileContract{ Payout: types.NewCurrency64(300e3), WindowEnd: pb.Height, MissedProofOutputs: []types.SiacoinOutput{{Value: types.NewCurrency64(290e3)}}, } // Assign the contract a 0-id. cst.cs.db.addFileContracts(types.FileContractID{}, expiringFC) cst.cs.db.addFCExpirations(pb.Height) cst.cs.db.addFCExpirationsHeight(pb.Height, types.FileContractID{}) cst.cs.db.Update(func(tx *bolt.Tx) error { return cst.cs.applyFileContractMaintenance(tx, pb) }) if err != nil { t.Fatal(err) } exists := cst.cs.db.inFileContracts(types.FileContractID{}) if exists { t.Error("file contract was not consumed in missed storage proof") } spoid := types.FileContractID{}.StorageProofOutputID(types.ProofMissed, 0) exists = cst.cs.db.inDelayedSiacoinOutputsHeight(pb.Height+types.MaturityDelay, spoid) if !exists { t.Error("missed proof output was never created") } exists = cst.cs.db.inSiacoinOutputs(spoid) if exists { t.Error("storage proof output made it into the siacoin output set") } exists = cst.cs.db.inFileContracts(types.FileContractID{}) if exists { t.Error("file contract remains after expiration") } }
// TestVariedWeights runs broad statistical tests on selecting hosts with // multiple different weights. func TestVariedWeights(t *testing.T) { if testing.Short() { t.SkipNow() } hdb := &HostDB{ activeHosts: make(map[modules.NetAddress]*hostNode), allHosts: make(map[modules.NetAddress]*hostEntry), scanPool: make(chan *hostEntry, scanPoolSize), } // insert i hosts with the weights 0, 1, ..., i-1. 100e3 selections will be made // per weight added to the tree, the total number of selections necessary // will be tallied up as hosts are created. var dbe modules.HostDBEntry dbe.AcceptingContracts = true hostCount := 5 expectedPerWeight := int(10e3) selections := 0 for i := 0; i < hostCount; i++ { dbe.NetAddress = fakeAddr(uint8(i)) entry := hostEntry{ HostDBEntry: dbe, Weight: types.NewCurrency64(uint64(i)), } hdb.insertNode(&entry) selections += i * expectedPerWeight } // Perform many random selections, noting which host was selected each // time. selectionMap := make(map[string]int) for i := 0; i < selections; i++ { randEntry := hdb.RandomHosts(1, nil) if len(randEntry) == 0 { t.Fatal("no hosts!") } node, exists := hdb.activeHosts[randEntry[0].NetAddress] if !exists { t.Fatal("can't find randomly selected node in tree") } selectionMap[node.hostEntry.Weight.String()]++ } // Check that each host was selected an expected number of times. An error // will be reported if the host of 0 weight is ever selected. acceptableError := 0.2 for weight, timesSelected := range selectionMap { intWeight, err := strconv.Atoi(weight) if err != nil { t.Fatal(err) } expectedSelected := float64(intWeight * expectedPerWeight) if float64(expectedSelected)*acceptableError > float64(timesSelected) || float64(expectedSelected)/acceptableError < float64(timesSelected) { t.Error("weighted list not selecting in a uniform distribution based on weight") t.Error(expectedSelected) t.Error(timesSelected) } } }
// TestTryInvalidTransactionSet submits an invalid transaction set to the // TryTransaction method. func TestTryInvalidTransactionSet(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestValidTransaction") if err != nil { t.Fatal(err) } defer cst.Close() initialHash := cst.cs.dbConsensusChecksum() // Try a valid transaction followed by an invalid transaction. _, err = cst.wallet.SendSiacoins(types.NewCurrency64(1), types.UnlockHash{}) if err != nil { t.Fatal(err) } txns := cst.tpool.TransactionList() txn := types.Transaction{ SiacoinInputs: []types.SiacoinInput{{}}, } txns = append(txns, txn) cc, err := cst.cs.TryTransactionSet(txns) if err == nil { t.Error("bad transaction survived filter") } if cst.cs.dbConsensusChecksum() != initialHash { t.Error("TryTransactionSet did not restore order") } if len(cc.SiacoinOutputDiffs) != 0 { t.Error("consensus change was not empty despite an error being returned") } }
// TestTryValidTransactionSet submits a valid transaction set to the // TryTransactionSet method. func TestTryValidTransactionSet(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestValidTransaction") if err != nil { t.Fatal(err) } defer cst.Close() initialHash := cst.cs.dbConsensusChecksum() // Try a valid transaction. _, err = cst.wallet.SendSiacoins(types.NewCurrency64(1), types.UnlockHash{}) if err != nil { t.Fatal(err) } txns := cst.tpool.TransactionList() cc, err := cst.cs.TryTransactionSet(txns) if err != nil { t.Error(err) } if cst.cs.dbConsensusChecksum() != initialHash { t.Error("TryTransactionSet did not resotre order") } if len(cc.SiacoinOutputDiffs) == 0 { t.Error("consensus change is missing diffs after verifying a transction clump") } }
// TestInconsistencyCheck puts the consensus set in to an inconsistent state // and makes sure that the santiy checks are triggering panics. func TestInconsistentCheck(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestInconsistentCheck") if err != nil { t.Fatal(err) } defer cst.closeCst() // Corrupt the consensus set by adding a new siafund output. sfo := types.SiafundOutput{ Value: types.NewCurrency64(1), } cst.cs.dbAddSiafundOutput(types.SiafundOutputID{}, sfo) // Catch a panic that should be caused by the inconsistency check after a // block is mined. defer func() { r := recover() if r == nil { t.Fatalf("inconsistency panic not triggered by corrupted database") } }() cst.miner.AddBlock() }
// TestCommitDelayedSiacoinOutputDiffBadMaturity commits a delayed sicoin // output that has a bad maturity height and triggers a panic. func TestCommitDelayedSiacoinOutputDiffBadMaturity(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestCommitDelayedSiacoinOutputDiffBadMaturity") if err != nil { t.Fatal(err) } // Trigger an inconsistency check. defer func() { r := recover() if r == nil { t.Error("expecting error after corrupting database") } }() // Commit a delayed siacoin output with maturity height = cs.height()+1 maturityHeight := cst.cs.height() - 1 id := types.SiacoinOutputID{'1'} dsco := types.SiacoinOutput{Value: types.NewCurrency64(1)} dscod := modules.DelayedSiacoinOutputDiff{ Direction: modules.DiffApply, ID: id, SiacoinOutput: dsco, MaturityHeight: maturityHeight, } cst.cs.commitDelayedSiacoinOutputDiff(dscod, modules.DiffApply) }
// checkMinerFees checks that the total amount of transaction fees in the // transaction set is sufficient to earn a spot in the transaction pool. func (tp *TransactionPool) checkMinerFees(ts []types.Transaction) error { // Transactions cannot be added after the TransactionPoolSizeLimit has been // hit. if tp.transactionListSize > TransactionPoolSizeLimit { return errFullTransactionPool } // The first TransactionPoolSizeForFee transactions do not need fees. if tp.transactionListSize > TransactionPoolSizeForFee { // Currently required fees are set on a per-transaction basis. 2 coins // are required per transaction if the free-fee limit has been reached, // adding a larger fee is not useful. var feeSum types.Currency for i := range ts { for _, fee := range ts[i].MinerFees { feeSum = feeSum.Add(fee) } } feeRequired := TransactionMinFee.Mul(types.NewCurrency64(uint64(len(ts)))) if feeSum.Cmp(feeRequired) < 0 { return errLowMinerFees } } return nil }
// renewBasePrice returns the base cost of the storage in the file contract, // using the host external settings and the starting file contract. func renewBasePrice(so storageObligation, settings modules.HostExternalSettings, fc types.FileContract) types.Currency { if fc.WindowEnd <= so.proofDeadline() { return types.NewCurrency64(0) } timeExtension := fc.WindowEnd - so.proofDeadline() return settings.StoragePrice.Mul64(fc.FileSize).Mul64(uint64(timeExtension)) }