// newRevision revises the current revision to incorporate new data. func newRevision(rev types.FileContractRevision, pieceLen uint64, merkleRoot crypto.Hash, piecePrice types.Currency) types.FileContractRevision { // prevent a negative currency panic if piecePrice.Cmp(rev.NewValidProofOutputs[0].Value) > 0 { // probably not enough money, but the host might accept it anyway piecePrice = rev.NewValidProofOutputs[0].Value } return types.FileContractRevision{ ParentID: rev.ParentID, UnlockConditions: rev.UnlockConditions, NewRevisionNumber: rev.NewRevisionNumber + 1, NewFileSize: rev.NewFileSize + pieceLen, NewFileMerkleRoot: merkleRoot, NewWindowStart: rev.NewWindowStart, NewWindowEnd: rev.NewWindowEnd, NewValidProofOutputs: []types.SiacoinOutput{ // less returned to renter {Value: rev.NewValidProofOutputs[0].Value.Sub(piecePrice), UnlockHash: rev.NewValidProofOutputs[0].UnlockHash}, // more given to host {Value: rev.NewValidProofOutputs[1].Value.Add(piecePrice), UnlockHash: rev.NewValidProofOutputs[1].UnlockHash}, }, NewMissedProofOutputs: []types.SiacoinOutput{ // less returned to renter {Value: rev.NewMissedProofOutputs[0].Value.Sub(piecePrice), UnlockHash: rev.NewMissedProofOutputs[0].UnlockHash}, // more given to void {Value: rev.NewMissedProofOutputs[1].Value.Add(piecePrice), UnlockHash: rev.NewMissedProofOutputs[1].UnlockHash}, }, NewUnlockHash: rev.NewUnlockHash, } }
// 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 / 2 hosts := r.hostDB.RandomHosts(sampleSize) for _, host := range hosts { totalPrice = totalPrice.Add(host.Price) } if len(hosts) == 0 { return } averagePrice := totalPrice.Mul(types.NewCurrency64(2)).Div(types.NewCurrency64(3)) // 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(4)).Div(types.NewCurrency64(3)) ri.Price = bufferedCost // Report the number of known hosts. ri.KnownHosts = len(r.hostDB.ActiveHosts()) return }
// validSiacoins checks that the siacoin inputs and outputs are valid in the // context of the current consensus set. func validSiacoins(tx *bolt.Tx, t types.Transaction) error { scoBucket := tx.Bucket(SiacoinOutputs) var inputSum types.Currency for _, sci := range t.SiacoinInputs { // Check that the input spends an existing output. scoBytes := scoBucket.Get(sci.ParentID[:]) if scoBytes == nil { return errMissingSiacoinOutput } // Check that the unlock conditions match the required unlock hash. var sco types.SiacoinOutput err := encoding.Unmarshal(scoBytes, &sco) if build.DEBUG && err != nil { panic(err) } if sci.UnlockConditions.UnlockHash() != sco.UnlockHash { return errWrongUnlockConditions } inputSum = inputSum.Add(sco.Value) } if inputSum.Cmp(t.SiacoinOutputSum()) != 0 { return errSiacoinInputOutputMismatch } return nil }
// 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 }
// findOutputs returns a set of spendable outputs that add up to at least // `amount` of coins, returning an error if it cannot. It also returns the // `total`, which is the sum of all the outputs that were found, since it's // unlikely that it will equal amount exaclty. func (w *Wallet) findOutputs(amount types.Currency) (knownOutputs []*knownOutput, total types.Currency, err error) { if amount.IsZero() { err = errors.New("cannot fund amount <= 0") return } // Iterate through all outputs until enough coins have been assembled. for _, key := range w.keys { if !key.spendable { continue } for _, knownOutput := range key.outputs { if !knownOutput.spendable { continue } if knownOutput.age > w.age-AgeDelay { continue } total = total.Add(knownOutput.output.Value) knownOutputs = append(knownOutputs, knownOutput) if total.Cmp(amount) >= 0 { return } } } // This code will only be reached if total < amount, meaning insufficient // funds. err = modules.LowBalanceErr return }
// SendSiacoins creates a transaction sending 'amount' to 'dest'. The transaction // is submitted to the transaction pool and is also returned. func (w *Wallet) SendSiacoins(amount types.Currency, dest types.UnlockHash) ([]types.Transaction, error) { if err := w.tg.Add(); err != nil { return nil, err } defer w.tg.Done() tpoolFee := types.SiacoinPrecision.Mul64(10) // TODO: better fee algo. output := types.SiacoinOutput{ Value: amount, UnlockHash: dest, } txnBuilder := w.StartTransaction() err := txnBuilder.FundSiacoins(amount.Add(tpoolFee)) if err != nil { return nil, err } txnBuilder.AddMinerFee(tpoolFee) txnBuilder.AddSiacoinOutput(output) txnSet, err := txnBuilder.Sign(true) if err != nil { return nil, err } err = w.tpool.AcceptTransactionSet(txnSet) if err != nil { return nil, err } return txnSet, nil }
// 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 }
// maxSectors is the estimated maximum number of sectors that the allowance // can support. func maxSectors(a modules.Allowance, hdb hostDB) (uint64, error) { if a.Hosts == 0 || a.Period == 0 { return 0, errors.New("invalid allowance") } // Sample at least 10 hosts. nRandomHosts := int(a.Hosts) if nRandomHosts < 10 { nRandomHosts = 10 } hosts := hdb.RandomHosts(nRandomHosts, nil) if len(hosts) < int(a.Hosts) { return 0, errors.New("not enough hosts") } // Calculate cost of storing 1 sector per host for the allowance period. var sum types.Currency for _, h := range hosts { sum = sum.Add(h.StoragePrice) } averagePrice := sum.Div64(uint64(len(hosts))) costPerSector := averagePrice.Mul64(a.Hosts).Mul64(modules.SectorSize).Mul64(uint64(a.Period)) // Divide total funds by cost per sector. numSectors, err := a.Funds.Div(costPerSector).Uint64() if err != nil { // if there was an overflow, something is definitely wrong return 0, errors.New("allowance can fund suspiciously large number of sectors") } return numSectors, nil }
// 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 }
// TestRPCUPload attempts to upload a file to the host, adding coverage to the // upload function. func TestRPCUpload(t *testing.T) { if testing.Short() { t.SkipNow() } ht, err := newHostTester("TestRPCUpload") if err != nil { t.Fatal(err) } ht.host.mu.RLock() baselineAnticipatedRevenue := ht.host.anticipatedRevenue baselineSpace := ht.host.spaceRemaining ht.host.mu.RUnlock() _, err = ht.uploadFile("TestRPCUpload - 1", renewDisabled) if err != nil { t.Fatal(err) } var expectedRevenue types.Currency func() { ht.host.mu.RLock() defer ht.host.mu.RUnlock() if ht.host.anticipatedRevenue.Cmp(baselineAnticipatedRevenue) <= 0 { t.Error("Anticipated revenue did not increase after a file was uploaded") } if baselineSpace <= ht.host.spaceRemaining { t.Error("space remaining on the host does not seem to have decreased") } expectedRevenue = ht.host.anticipatedRevenue }() // Mine until the storage proof goes through, and the obligation gets // cleared. for i := types.BlockHeight(0); i <= testUploadDuration+confirmationRequirement+defaultWindowSize; i++ { _, err := ht.miner.AddBlock() if err != nil { t.Fatal(err) } } // Check that the storage proof has succeeded. ht.host.mu.Lock() defer ht.host.mu.Unlock() if len(ht.host.obligationsByID) != 0 { t.Error("host still has obligation, when it should have completed the obligation and submitted a storage proof.") } if !ht.host.anticipatedRevenue.IsZero() { t.Error("host anticipated revenue was not set back to zero") } if ht.host.spaceRemaining != baselineSpace { t.Error("host does not seem to have reclaimed the space after a successful obligation") } if expectedRevenue.Cmp(ht.host.revenue) != 0 { t.Error("host's revenue was not moved from anticipated to expected") } }
// CalculateFee returns the fee-per-byte of a transaction set. func CalculateFee(ts []types.Transaction) types.Currency { var sum types.Currency for _, t := range ts { for _, fee := range t.MinerFees { sum = sum.Add(fee) } } size := len(encoding.Marshal(ts)) return sum.Div64(uint64(size)) }
// checkMinerPayouts compares a block's miner payouts to the block's subsidy and // returns true if they are equal. func checkMinerPayouts(b types.Block, height types.BlockHeight) bool { // Add up the payouts and check that all values are legal. var payoutSum types.Currency for _, payout := range b.MinerPayouts { if payout.Value.IsZero() { return false } payoutSum = payoutSum.Add(payout.Value) } return b.CalculateSubsidy(height).Cmp(payoutSum) == 0 }
// AveragePrice returns the average price of a host. func (hdb *HostDB) AveragePrice() types.Currency { // maybe a more sophisticated way of doing this var totalPrice types.Currency sampleSize := 18 hosts := hdb.randomHosts(sampleSize, nil) if len(hosts) == 0 { return totalPrice } for _, host := range hosts { totalPrice = totalPrice.Add(host.Price) } return totalPrice.Div(types.NewCurrency64(uint64(len(hosts)))) }
// currencyUnits converts a types.Currency to a string with human-readable // units. The unit used will be the largest unit that results in a value // greater than 1. The value is rounded to 4 significant digits. func currencyUnits(c types.Currency) string { pico := types.SiacoinPrecision.Div64(1e12) if c.Cmp(pico) < 0 { return c.String() + " H" } // iterate until we find a unit greater than c mag := pico unit := "" for _, unit = range []string{"pS", "nS", "uS", "mS", "SC", "KS", "MS", "GS", "TS"} { if c.Cmp(mag.Mul64(1e3)) < 0 { break } else if unit != "TS" { // don't want to perform this multiply on the last iter; that // would give us 1.235 TS instead of 1235 TS mag = mag.Mul64(1e3) } } num := new(big.Rat).SetInt(c.Big()) denom := new(big.Rat).SetInt(mag.Big()) res, _ := new(big.Rat).Mul(num, denom.Inv(denom)).Float64() return fmt.Sprintf("%.4g %s", res, unit) }
// validUnconfirmedSiafunds checks that all siafund inputs and outputs are // valid within the context of the unconfirmed consensus set. func (tp *TransactionPool) validUnconfirmedSiafunds(t types.Transaction) (err error) { var inputSum types.Currency for _, sfi := range t.SiafundInputs { // Check that the corresponding siafund output being spent exists. sfo, exists := tp.siafundOutputs[sfi.ParentID] if !exists { return errors.New("transaction spends unrecognized siafund output") } // Check that the unlock conditions match the unlock hash of the // corresponding output. if sfi.UnlockConditions.UnlockHash() != sfo.UnlockHash { return errors.New("transaction contains invalid unlock conditions (hash mismatch)") } // Add this input's value to the inputSum. inputSum = inputSum.Add(sfo.Value) } // Check that the value of the outputs equal the value of the inputs. var outputSum types.Currency for _, sfo := range t.SiafundOutputs { outputSum = outputSum.Add(sfo.Value) } if outputSum.Cmp(inputSum) != 0 { return errors.New("siafund inputs do not equal siafund outputs") } return }
// validSiafunds checks that the siafund portions of the transaction are valid // in the context of the consensus set. func (cs *ConsensusSet) validSiafunds(t types.Transaction) (err error) { // Compare the number of input siafunds to the output siafunds. var siafundInputSum types.Currency var siafundOutputSum types.Currency for _, sfi := range t.SiafundInputs { exists := cs.db.inSiafundOutputs(sfi.ParentID) if !exists { return ErrMissingSiafundOutput } sfo := cs.db.getSiafundOutputs(sfi.ParentID) // Check the unlock conditions match the unlock hash. if sfi.UnlockConditions.UnlockHash() != sfo.UnlockHash { return ErrWrongUnlockConditions } siafundInputSum = siafundInputSum.Add(sfo.Value) } for _, sfo := range t.SiafundOutputs { siafundOutputSum = siafundOutputSum.Add(sfo.Value) } if siafundOutputSum.Cmp(siafundInputSum) != 0 { return ErrSiafundInputOutputMismatch } return }
// nodeAtWeight grabs an element in the tree that appears at the given weight. // Though the tree has an arbitrary sorting, a sufficiently random weight will // pull a random element. The tree is searched through in a post-ordered way. func (hn *hostNode) nodeAtWeight(weight types.Currency) (*hostNode, error) { // Sanity check - weight must be less than the total weight of the tree. if weight.Cmp(hn.weight) > 0 { return nil, errOverweight } // Check if the left or right child should be returned. if hn.left != nil { if weight.Cmp(hn.left.weight) < 0 { return hn.left.nodeAtWeight(weight) } weight = weight.Sub(hn.left.weight) // Search from 0th index of right side. } if hn.right != nil && weight.Cmp(hn.right.weight) < 0 { return hn.right.nodeAtWeight(weight) } // Sanity check if build.DEBUG && !hn.taken { build.Critical("nodeAtWeight should not be returning a nil entry") } // Return the root entry. return hn, nil }
// validSiafunds checks that the siafund portions of the transaction are valid // in the context of the consensus set. func validSiafunds(tx *bolt.Tx, t types.Transaction) (err error) { // Compare the number of input siafunds to the output siafunds. var siafundInputSum types.Currency var siafundOutputSum types.Currency for _, sfi := range t.SiafundInputs { sfo, err := getSiafundOutput(tx, sfi.ParentID) if err != nil { return err } // Check the unlock conditions match the unlock hash. if sfi.UnlockConditions.UnlockHash() != sfo.UnlockHash { return errWrongUnlockConditions } siafundInputSum = siafundInputSum.Add(sfo.Value) } for _, sfo := range t.SiafundOutputs { siafundOutputSum = siafundOutputSum.Add(sfo.Value) } if siafundOutputSum.Cmp(siafundInputSum) != 0 { return errSiafundInputOutputMismatch } return }
// StoragePriceToHuman converts a consensus storage price, having the unit // 'Hastings Per Block Per Byte', to a human storage price, having the unit // 'Siacoins Per Month Per Terabyte'. An error is returned if the result would // overflow a uint64. If the result is between 0 and 1, the value is rounded to // the nearest value. func StoragePriceToHuman(hastingsBlockByte types.Currency) (siacoinsMonthTB uint64, err error) { // Perform multiplication first to preserve precision. hastingsMonthByte := hastingsBlockByte.Mul(types.NewCurrency64(4320)) hastingsMonthTB := hastingsMonthByte.Mul(types.NewCurrency64(1e12)) if hastingsMonthTB.Cmp(types.SiacoinPrecision.Div(types.NewCurrency64(2))) < 0 { // The result of the final division is going to be less than 0.5, // therefore 0 should be returned. return 0, nil } if hastingsMonthTB.Cmp(types.SiacoinPrecision) < 0 { // The result of the final division is going to be greater than or // equal to 0.5, but less than 1, therefore 1 should be returned. return 1, nil } return hastingsMonthTB.Div(types.SiacoinPrecision).Uint64() }
// validFileContractRevision checks that each file contract revision is valid // in the context of the current consensus set. func (cs *ConsensusSet) validFileContractRevisions(t types.Transaction) (err error) { for _, fcr := range t.FileContractRevisions { // Check that the revision revises an existing contract. exists := cs.db.inFileContracts(fcr.ParentID) if !exists { return ErrUnrecognizedFileContractID } fc := cs.db.getFileContracts(fcr.ParentID) // Check that the height is less than fc.WindowStart - revisions are // not allowed to be submitted once the storage proof window has // opened. This reduces complexity for unconfirmed transactions. if cs.height() > fc.WindowStart { return ErrLateRevision } // Check that the revision number of the revision is greater than the // revision number of the existing file contract. if fc.RevisionNumber >= fcr.NewRevisionNumber { return ErrLowRevisionNumber } // Check that the unlock conditions match the unlock hash. if fcr.UnlockConditions.UnlockHash() != fc.UnlockHash { return ErrWrongUnlockConditions } // Check that the payout of the revision matches the payout of the // original, and that the payouts match eachother. var validPayout, missedPayout types.Currency for _, output := range fcr.NewValidProofOutputs { validPayout = validPayout.Add(output.Value) } for _, output := range fcr.NewMissedProofOutputs { missedPayout = missedPayout.Add(output.Value) } if validPayout.Cmp(fc.Payout.Sub(fc.Tax())) != 0 { return ErrAlteredRevisionPayouts } if missedPayout.Cmp(fc.Payout.Sub(fc.Tax())) != 0 { return ErrAlteredRevisionPayouts } } return }
// 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 }
// checkSiafundCount checks that the number of siafunds countable within the // consensus set equal the expected number of siafunds for the block height. func checkSiafundCount(tx *bolt.Tx) { var total types.Currency err := tx.Bucket(SiafundOutputs).ForEach(func(_, siafundOutputBytes []byte) error { var sfo types.SiafundOutput err := encoding.Unmarshal(siafundOutputBytes, &sfo) if err != nil { manageErr(tx, err) } total = total.Add(sfo.Value) return nil }) if err != nil { manageErr(tx, err) } if total.Cmp(types.SiafundCount) != 0 { manageErr(tx, errors.New("wrong number if siafunds in the consensus set")) } }
// validUnconfirmedFileContractRevisions checks that all file contract // revisions are valid within the context of the unconfirmed consensus set. func (tp *TransactionPool) validUnconfirmedFileContractRevisions(t types.Transaction) (err error) { for _, fcr := range t.FileContractRevisions { // Check for the corresponding file contract in the unconfirmed set. fc, exists := tp.fileContracts[fcr.ParentID] if !exists { return errors.New("revision given for unrecognized file contract") } // Check that the revision was submitted before the storage proof // window opened. if tp.consensusSetHeight > fc.WindowStart { return errors.New("revision submitted too late") } // Check that the revision number is increasing as a result of the // revision. if fc.RevisionNumber >= fcr.NewRevisionNumber { return errors.New("contract revision is outdated") } // Check that the unlock conditions match the unlock hash of the // corresponding file contract. if fcr.UnlockConditions.UnlockHash() != fc.UnlockHash { return errors.New("unlock conditions do not meet required unlock hash") } // Check that the payouts in the revision add up to the payout of the // contract. var payout types.Currency for _, output := range fcr.NewMissedProofOutputs { payout = payout.Add(output.Value) } if payout.Cmp(fc.Payout) != 0 { return errors.New("contract revision has incorrect payouts") } } return }
// managedFormAllowanceContracts handles the special case where no contracts // need to be renewed when setting the allowance. func (c *Contractor) managedFormAllowanceContracts(n int, numSectors uint64, a modules.Allowance) error { if n <= 0 { return nil } // if we're forming contracts but not renewing, the new contracts should // have the same endHeight as the existing ones. Otherwise, the endHeight // should be a full period. c.mu.RLock() endHeight := c.blockHeight + a.Period if len(c.contracts) > 0 { endHeight = c.contractEndHeight() } c.mu.RUnlock() // form the contracts formed, err := c.managedFormContracts(n, numSectors, endHeight) if err != nil { return err } // Set the allowance and replace the contract set c.mu.Lock() c.allowance = a for _, contract := range formed { c.contracts[contract.ID] = contract } // update metrics var spending types.Currency for _, contract := range c.contracts { spending = spending.Add(contract.RenterFunds()) } c.financialMetrics.ContractSpending = spending err = c.saveSync() c.mu.Unlock() return err }
// validUnconfirmedSiacoins checks that all siacoin inputs and outputs are // valid in the context of the unconfirmed consensus set. func (tp *TransactionPool) validUnconfirmedSiacoins(t types.Transaction) (err error) { var inputSum types.Currency for _, sci := range t.SiacoinInputs { // All inputs must have corresponding outputs in the unconfirmed set. sco, exists := tp.siacoinOutputs[sci.ParentID] if !exists { return ErrUnrecognizedSiacoinInput } // The unlock conditions provided must match the unlock hash of the // corresponding output. if sci.UnlockConditions.UnlockHash() != sco.UnlockHash { return ErrBadUnlockConditions } inputSum = inputSum.Add(sco.Value) } // The sum of all inputs must equal the sum of all outputs. if inputSum.Cmp(t.SiacoinOutputSum()) != 0 { return ErrSiacoinOverspend } return }
// SendCoins creates a transaction sending 'amount' to 'dest'. The transaction // is submitted to the transaction pool and is also returned. func (w *Wallet) SendCoins(amount types.Currency, dest types.UnlockHash) (t types.Transaction, err error) { // Add a transaction fee of 10 siacoins. tpoolFee := types.NewCurrency64(10).Mul(types.SiacoinPrecision) // Create and send the transaction. output := types.SiacoinOutput{ Value: amount, UnlockHash: dest, } id, err := w.RegisterTransaction(t) if err != nil { return } _, err = w.FundTransaction(id, amount.Add(tpoolFee)) if err != nil { return } _, _, err = w.AddMinerFee(id, tpoolFee) if err != nil { return } _, _, err = w.AddSiacoinOutput(id, output) if err != nil { return } t, err = w.SignTransaction(id, true) if err != nil { return } err = w.tpool.AcceptTransaction(t) if err != nil { return } return }
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 }
// managedRevisionIteration handles one iteration of the revision loop. As a // performance optimization, multiple iterations of revisions are allowed to be // made over the same connection. func (h *Host) managedRevisionIteration(conn net.Conn, so *storageObligation, finalIter bool) error { // Send the settings to the renter. The host will keep going even if it is // not accepting contracts, because in this case the contract already // exists. err := h.managedRPCSettings(conn) if err != nil { return err } // Set the negotiation deadline. conn.SetDeadline(time.Now().Add(modules.NegotiateFileContractRevisionTime)) // The renter will either accept or reject the settings + revision // transaction. It may also return a stop response to indicate that it // wishes to terminate the revision loop. err = modules.ReadNegotiationAcceptance(conn) if err != nil { return err } // Read some variables from the host for use later in the function. h.mu.RLock() settings := h.settings secretKey := h.secretKey blockHeight := h.blockHeight h.mu.RUnlock() // The renter is going to send its intended modifications, followed by the // file contract revision that pays for them. var modifications []modules.RevisionAction var revision types.FileContractRevision err = encoding.ReadObject(conn, &modifications, settings.MaxReviseBatchSize) if err != nil { return err } err = encoding.ReadObject(conn, &revision, modules.NegotiateMaxFileContractRevisionSize) if err != nil { return err } // First read all of the modifications. Then make the modifications, but // with the ability to reverse them. Then verify the file contract revision // correctly accounts for the changes. var bandwidthRevenue types.Currency // Upload bandwidth. var storageRevenue types.Currency var newCollateral types.Currency var sectorsRemoved []crypto.Hash var sectorsGained []crypto.Hash var gainedSectorData [][]byte err = func() error { for _, modification := range modifications { // Check that the index points to an existing sector root. If the type // is ActionInsert, we permit inserting at the end. if modification.Type == modules.ActionInsert { if modification.SectorIndex > uint64(len(so.SectorRoots)) { return errBadModificationIndex } } else if modification.SectorIndex >= uint64(len(so.SectorRoots)) { return errBadModificationIndex } // Check that the data sent for the sector is not too large. if uint64(len(modification.Data)) > modules.SectorSize { return errLargeSector } switch modification.Type { case modules.ActionDelete: // There is no financial information to change, it is enough to // remove the sector. sectorsRemoved = append(sectorsRemoved, so.SectorRoots[modification.SectorIndex]) so.SectorRoots = append(so.SectorRoots[0:modification.SectorIndex], so.SectorRoots[modification.SectorIndex+1:]...) case modules.ActionInsert: // Check that the sector size is correct. if uint64(len(modification.Data)) != modules.SectorSize { return errBadSectorSize } // Update finances. blocksRemaining := so.proofDeadline() - blockHeight blockBytesCurrency := types.NewCurrency64(uint64(blocksRemaining)).Mul64(modules.SectorSize) bandwidthRevenue = bandwidthRevenue.Add(settings.MinUploadBandwidthPrice.Mul64(modules.SectorSize)) storageRevenue = storageRevenue.Add(settings.MinStoragePrice.Mul(blockBytesCurrency)) newCollateral = newCollateral.Add(settings.Collateral.Mul(blockBytesCurrency)) // Insert the sector into the root list. newRoot := crypto.MerkleRoot(modification.Data) sectorsGained = append(sectorsGained, newRoot) gainedSectorData = append(gainedSectorData, modification.Data) so.SectorRoots = append(so.SectorRoots[:modification.SectorIndex], append([]crypto.Hash{newRoot}, so.SectorRoots[modification.SectorIndex:]...)...) case modules.ActionModify: // Check that the offset and length are okay. Length is already // known to be appropriately small, but the offset needs to be // checked for being appropriately small as well otherwise there is // a risk of overflow. if modification.Offset > modules.SectorSize || modification.Offset+uint64(len(modification.Data)) > modules.SectorSize { return errIllegalOffsetAndLength } // Get the data for the new sector. sector, err := h.ReadSector(so.SectorRoots[modification.SectorIndex]) if err != nil { return err } copy(sector[modification.Offset:], modification.Data) // Update finances. bandwidthRevenue = bandwidthRevenue.Add(settings.MinUploadBandwidthPrice.Mul64(uint64(len(modification.Data)))) // Update the sectors removed and gained to indicate that the old // sector has been replaced with a new sector. newRoot := crypto.MerkleRoot(sector) sectorsRemoved = append(sectorsRemoved, so.SectorRoots[modification.SectorIndex]) sectorsGained = append(sectorsGained, newRoot) gainedSectorData = append(gainedSectorData, sector) so.SectorRoots[modification.SectorIndex] = newRoot default: return errUnknownModification } } newRevenue := storageRevenue.Add(bandwidthRevenue) return verifyRevision(so, revision, blockHeight, newRevenue, newCollateral) }() if err != nil { return modules.WriteNegotiationRejection(conn, err) } // Revision is acceptable, write an acceptance string. err = modules.WriteNegotiationAcceptance(conn) if err != nil { return err } // Renter will send a transaction signature for the file contract revision. var renterSig types.TransactionSignature err = encoding.ReadObject(conn, &renterSig, modules.NegotiateMaxTransactionSignatureSize) if err != nil { return err } // Verify that the signature is valid and get the host's signature. txn, err := createRevisionSignature(revision, renterSig, secretKey, blockHeight) if err != nil { return modules.WriteNegotiationRejection(conn, err) } so.PotentialStorageRevenue = so.PotentialStorageRevenue.Add(storageRevenue) so.RiskedCollateral = so.RiskedCollateral.Add(newCollateral) so.PotentialUploadRevenue = so.PotentialUploadRevenue.Add(bandwidthRevenue) so.RevisionTransactionSet = []types.Transaction{txn} err = h.modifyStorageObligation(so, sectorsRemoved, sectorsGained, gainedSectorData) if err != nil { return modules.WriteNegotiationRejection(conn, err) } // Host will now send acceptance and its signature to the renter. This // iteration is complete. If the finalIter flag is set, StopResponse will // be sent instead. This indicates to the renter that the host wishes to // terminate the revision loop. if finalIter { err = modules.WriteNegotiationStop(conn) } else { err = modules.WriteNegotiationAcceptance(conn) } if err != nil { return err } return encoding.WriteObject(conn, txn.TransactionSignatures[1]) }
// validFileContractRevision checks that each file contract revision is valid // in the context of the current consensus set. func validFileContractRevisions(tx *bolt.Tx, t types.Transaction) error { for _, fcr := range t.FileContractRevisions { fc, err := getFileContract(tx, fcr.ParentID) if err != nil { return err } // Check that the height is less than fc.WindowStart - revisions are // not allowed to be submitted once the storage proof window has // opened. This reduces complexity for unconfirmed transactions. if blockHeight(tx) > fc.WindowStart { return errLateRevision } // Check that the revision number of the revision is greater than the // revision number of the existing file contract. if fc.RevisionNumber >= fcr.NewRevisionNumber { return errLowRevisionNumber } // Check that the unlock conditions match the unlock hash. if fcr.UnlockConditions.UnlockHash() != fc.UnlockHash { return errWrongUnlockConditions } // Check that the payout of the revision matches the payout of the // original, and that the payouts match each other. var validPayout, missedPayout, oldPayout types.Currency for _, output := range fcr.NewValidProofOutputs { validPayout = validPayout.Add(output.Value) } for _, output := range fcr.NewMissedProofOutputs { missedPayout = missedPayout.Add(output.Value) } for _, output := range fc.ValidProofOutputs { oldPayout = oldPayout.Add(output.Value) } if validPayout.Cmp(oldPayout) != 0 { return errAlteredRevisionPayouts } if missedPayout.Cmp(oldPayout) != 0 { return errAlteredRevisionPayouts } } return nil }
// FundSiacoins will add a siacoin input of exaclty 'amount' to the // transaction. A parent transaction may be needed to achieve an input with the // correct value. The siacoin input will not be signed until 'Sign' is called // on the transaction builder. func (tb *transactionBuilder) FundSiacoins(amount types.Currency) error { tb.wallet.mu.Lock() defer tb.wallet.mu.Unlock() // Collect a value-sorted set of siacoin outputs. var so sortedOutputs for scoid, sco := range tb.wallet.siacoinOutputs { so.ids = append(so.ids, scoid) so.outputs = append(so.outputs, sco) } // Add all of the unconfirmed outputs as well. for _, upt := range tb.wallet.unconfirmedProcessedTransactions { for i, sco := range upt.Transaction.SiacoinOutputs { // Determine if the output belongs to the wallet. _, exists := tb.wallet.keys[sco.UnlockHash] if !exists { continue } so.ids = append(so.ids, upt.Transaction.SiacoinOutputID(uint64(i))) so.outputs = append(so.outputs, sco) } } sort.Sort(sort.Reverse(so)) // Create and fund a parent transaction that will add the correct amount of // siacoins to the transaction. var fund types.Currency // potentialFund tracks the balance of the wallet including outputs that // have been spent in other unconfirmed transactions recently. This is to // provide the user with a more useful error message in the event that they // are overspending. var potentialFund types.Currency parentTxn := types.Transaction{} var spentScoids []types.SiacoinOutputID for i := range so.ids { scoid := so.ids[i] sco := so.outputs[i] // Check that this output has not recently been spent by the wallet. spendHeight := tb.wallet.spentOutputs[types.OutputID(scoid)] // Prevent an underflow error. allowedHeight := tb.wallet.consensusSetHeight - RespendTimeout if tb.wallet.consensusSetHeight < RespendTimeout { allowedHeight = 0 } if spendHeight > allowedHeight { potentialFund = potentialFund.Add(sco.Value) continue } outputUnlockConditions := tb.wallet.keys[sco.UnlockHash].UnlockConditions if tb.wallet.consensusSetHeight < outputUnlockConditions.Timelock { continue } // Add a siacoin input for this output. sci := types.SiacoinInput{ ParentID: scoid, UnlockConditions: outputUnlockConditions, } parentTxn.SiacoinInputs = append(parentTxn.SiacoinInputs, sci) spentScoids = append(spentScoids, scoid) // Add the output to the total fund fund = fund.Add(sco.Value) potentialFund = potentialFund.Add(sco.Value) if fund.Cmp(amount) >= 0 { break } } if potentialFund.Cmp(amount) >= 0 && fund.Cmp(amount) < 0 { return modules.ErrPotentialDoubleSpend } if fund.Cmp(amount) < 0 { return modules.ErrLowBalance } // Create and add the output that will be used to fund the standard // transaction. parentUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } exactOutput := types.SiacoinOutput{ Value: amount, UnlockHash: parentUnlockConditions.UnlockHash(), } parentTxn.SiacoinOutputs = append(parentTxn.SiacoinOutputs, exactOutput) // Create a refund output if needed. if amount.Cmp(fund) != 0 { refundUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } refundOutput := types.SiacoinOutput{ Value: fund.Sub(amount), UnlockHash: refundUnlockConditions.UnlockHash(), } parentTxn.SiacoinOutputs = append(parentTxn.SiacoinOutputs, refundOutput) } // Sign all of the inputs to the parent trancstion. for _, sci := range parentTxn.SiacoinInputs { _, err := addSignatures(&parentTxn, types.FullCoveredFields, sci.UnlockConditions, crypto.Hash(sci.ParentID), tb.wallet.keys[sci.UnlockConditions.UnlockHash()]) if err != nil { return err } } // Mark the parent output as spent. Must be done after the transaction is // finished because otherwise the txid and output id will change. tb.wallet.spentOutputs[types.OutputID(parentTxn.SiacoinOutputID(0))] = tb.wallet.consensusSetHeight // Add the exact output. newInput := types.SiacoinInput{ ParentID: parentTxn.SiacoinOutputID(0), UnlockConditions: parentUnlockConditions, } tb.newParents = append(tb.newParents, len(tb.parents)) tb.parents = append(tb.parents, parentTxn) tb.siacoinInputs = append(tb.siacoinInputs, len(tb.transaction.SiacoinInputs)) tb.transaction.SiacoinInputs = append(tb.transaction.SiacoinInputs, newInput) // Mark all outputs that were spent as spent. for _, scoid := range spentScoids { tb.wallet.spentOutputs[types.OutputID(scoid)] = tb.wallet.consensusSetHeight } return nil }