// 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 }
// 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 }
// 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 }
// 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 }
// 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 }
// 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 }
// 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 }
// 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 }
// 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 }
// 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 }
// 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 }
// wallettransactionscmd lists all of the transactions related to the wallet, // providing a net flow of siacoins and siafunds for each. func wallettransactionscmd() { wtg := new(api.WalletTransactionsGET) err := getAPI("/wallet/transactions?startheight=0&endheight=10000000", wtg) if err != nil { fmt.Println("Could not fetch transaction history:", err) return } fmt.Println(" [height] [transaction id] [net siacoins] [net siafunds]") txns := append(wtg.ConfirmedTransactions, wtg.UnconfirmedTransactions...) for _, txn := range txns { // Determine the number of outgoing siacoins and siafunds. var outgoingSiacoins types.Currency var outgoingSiafunds types.Currency for _, input := range txn.Inputs { if input.FundType == types.SpecifierSiacoinInput && input.WalletAddress { outgoingSiacoins = outgoingSiacoins.Add(input.Value) } if input.FundType == types.SpecifierSiafundInput && input.WalletAddress { outgoingSiafunds = outgoingSiafunds.Add(input.Value) } } // Determine the number of incoming siacoins and siafunds. var incomingSiacoins types.Currency var incomingSiafunds types.Currency for _, output := range txn.Outputs { if output.FundType == types.SpecifierMinerPayout { incomingSiacoins = incomingSiacoins.Add(output.Value) } if output.FundType == types.SpecifierSiacoinOutput && output.WalletAddress { incomingSiacoins = incomingSiacoins.Add(output.Value) } if output.FundType == types.SpecifierSiafundOutput && output.WalletAddress { incomingSiafunds = incomingSiafunds.Add(output.Value) } } // Convert the siacoins to a float. incomingSiacoinsFloat, _ := new(big.Rat).SetFrac(incomingSiacoins.Big(), types.SiacoinPrecision.Big()).Float64() outgoingSiacoinsFloat, _ := new(big.Rat).SetFrac(outgoingSiacoins.Big(), types.SiacoinPrecision.Big()).Float64() // Print the results. if txn.ConfirmationHeight < 1e9 { fmt.Printf("%12v", txn.ConfirmationHeight) } else { fmt.Printf(" unconfirmed") } fmt.Printf("%67v%15.2f SC", txn.TransactionID, incomingSiacoinsFloat-outgoingSiacoinsFloat) // For siafunds, need to avoid having a negative types.Currency. if incomingSiafunds.Cmp(outgoingSiafunds) >= 0 { fmt.Printf("%14v SF\n", incomingSiafunds.Sub(outgoingSiafunds)) } else { fmt.Printf("-%14v SF\n", outgoingSiafunds.Sub(incomingSiafunds)) } } }
// 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)))) }
// 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 }
// 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")) } }
// 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 }
// 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 }
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 }
// 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 }
// 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 }
// FundSiafunds will add a siafund input of exaclty 'amount' to the // transaction. A parent transaction may be needed to achieve an input with the // correct value. The siafund input will not be signed until 'Sign' is called // on the transaction builder. func (tb *transactionBuilder) FundSiafunds(amount types.Currency) error { tb.wallet.mu.Lock() defer tb.wallet.mu.Unlock() // Create and fund a parent transaction that will add the correct amount of // siafunds to the transaction. var fund types.Currency var potentialFund types.Currency parentTxn := types.Transaction{} var spentSfoids []types.SiafundOutputID for sfoid, sfo := range tb.wallet.siafundOutputs { // Check that this output has not recently been spent by the wallet. spendHeight := tb.wallet.spentOutputs[types.OutputID(sfoid)] // Prevent an underflow error. allowedHeight := tb.wallet.consensusSetHeight - RespendTimeout if tb.wallet.consensusSetHeight < RespendTimeout { allowedHeight = 0 } if spendHeight > allowedHeight { potentialFund = potentialFund.Add(sfo.Value) continue } outputUnlockConditions := tb.wallet.keys[sfo.UnlockHash].UnlockConditions if tb.wallet.consensusSetHeight < outputUnlockConditions.Timelock { continue } // Add a siafund input for this output. parentClaimUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } sfi := types.SiafundInput{ ParentID: sfoid, UnlockConditions: outputUnlockConditions, ClaimUnlockHash: parentClaimUnlockConditions.UnlockHash(), } parentTxn.SiafundInputs = append(parentTxn.SiafundInputs, sfi) spentSfoids = append(spentSfoids, sfoid) // Add the output to the total fund fund = fund.Add(sfo.Value) potentialFund = potentialFund.Add(sfo.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.SiafundOutput{ Value: amount, UnlockHash: parentUnlockConditions.UnlockHash(), } parentTxn.SiafundOutputs = append(parentTxn.SiafundOutputs, exactOutput) // Create a refund output if needed. if amount.Cmp(fund) != 0 { refundUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } refundOutput := types.SiafundOutput{ Value: fund.Sub(amount), UnlockHash: refundUnlockConditions.UnlockHash(), } parentTxn.SiafundOutputs = append(parentTxn.SiafundOutputs, refundOutput) } // Sign all of the inputs to the parent trancstion. for _, sfi := range parentTxn.SiafundInputs { _, err := addSignatures(&parentTxn, types.FullCoveredFields, sfi.UnlockConditions, crypto.Hash(sfi.ParentID), tb.wallet.keys[sfi.UnlockConditions.UnlockHash()]) if err != nil { return err } } // Add the exact output. claimUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } newInput := types.SiafundInput{ ParentID: parentTxn.SiafundOutputID(0), UnlockConditions: parentUnlockConditions, ClaimUnlockHash: claimUnlockConditions.UnlockHash(), } tb.newParents = append(tb.newParents, len(tb.parents)) tb.parents = append(tb.parents, parentTxn) tb.siafundInputs = append(tb.siafundInputs, len(tb.transaction.SiafundInputs)) tb.transaction.SiafundInputs = append(tb.transaction.SiafundInputs, newInput) // Mark all outputs that were spent as spent. for _, sfoid := range spentSfoids { tb.wallet.spentOutputs[types.OutputID(sfoid)] = tb.wallet.consensusSetHeight } return nil }
// 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 }
// TestRenterHandlerContracts checks that contract formation between a host and // renter behaves as expected, and that contract spending is the right amount. func TestRenterHandlerContracts(t *testing.T) { if testing.Short() { t.SkipNow() } st, err := createServerTester("TestRenterHandlerContracts") if err != nil { t.Fatal(err) } defer st.server.Close() // Anounce the host and start accepting contracts. if err := st.announceHost(); err != nil { t.Fatal(err) } if err = st.acceptContracts(); err != nil { t.Fatal(err) } if err = st.setHostStorage(); err != nil { t.Fatal(err) } // The renter should not have any contracts yet. var contracts RenterContracts if err = st.getAPI("/renter/contracts", &contracts); err != nil { t.Fatal(err) } if len(contracts.Contracts) != 0 { t.Fatalf("expected renter to have 0 contracts; got %v", len(contracts.Contracts)) } // Set an allowance for the renter, allowing a contract to be formed. allowanceValues := url.Values{} allowanceValues.Set("funds", testFunds) allowanceValues.Set("period", testPeriod) if err = st.stdPostAPI("/renter", allowanceValues); err != nil { t.Fatal(err) } // The renter should now have 1 contract. if err = st.getAPI("/renter/contracts", &contracts); err != nil { t.Fatal(err) } if len(contracts.Contracts) != 1 { t.Fatalf("expected renter to have 1 contract; got %v", len(contracts.Contracts)) } // Check the renter's contract spending. var get RenterGET if err = st.getAPI("/renter", &get); err != nil { t.Fatal(err) } var expectedContractSpending types.Currency for _, contract := range contracts.Contracts { expectedContractSpending = expectedContractSpending.Add(contract.RenterFunds) } if got := get.FinancialMetrics.ContractSpending; got.Cmp(expectedContractSpending) != 0 { t.Fatalf("expected contract spending to be %v; got %v", expectedContractSpending, got) } }
// checkDSCOs scans the sets of delayed siacoin outputs and checks for // consistency. func checkDSCOs(tx *bolt.Tx) { // Create a map to track which delayed siacoin output maps exist, and // another map to track which ids have appeared in the dsco set. dscoTracker := make(map[types.BlockHeight]struct{}) idMap := make(map[types.SiacoinOutputID]struct{}) // Iterate through all the buckets looking for the delayed siacoin output // buckets, and check that they are for the correct heights. err := tx.ForEach(func(name []byte, b *bolt.Bucket) error { // If the bucket is not a delayed siacoin output bucket or a file // contract expiration bucket, skip. if !bytes.HasPrefix(name, prefixDSCO) { return nil } // Add the bucket to the dscoTracker. var height types.BlockHeight err := encoding.Unmarshal(name[len(prefixDSCO):], &height) if err != nil { manageErr(tx, err) } _, exists := dscoTracker[height] if exists { return errors.New("repeat dsco map") } dscoTracker[height] = struct{}{} var total types.Currency err = b.ForEach(func(idBytes, delayedOutput []byte) error { // Check that the output id has not appeared in another dsco. var id types.SiacoinOutputID copy(id[:], idBytes) _, exists := idMap[id] if exists { return errors.New("repeat delayed siacoin output") } idMap[id] = struct{}{} // Sum the funds in the bucket. var sco types.SiacoinOutput err := encoding.Unmarshal(delayedOutput, &sco) if err != nil { manageErr(tx, err) } total = total.Add(sco.Value) return nil }) if err != nil { return err } // Check that the minimum value has been achieved - the coinbase from // an earlier block is guaranteed to be in the bucket. minimumValue := types.CalculateCoinbase(height - types.MaturityDelay) if total.Cmp(minimumValue) < 0 { return errors.New("total number of coins in the delayed output bucket is incorrect") } return nil }) if err != nil { manageErr(tx, err) } // Check that all of the correct heights are represented. currentHeight := blockHeight(tx) expectedBuckets := 0 for i := currentHeight + 1; i <= currentHeight+types.MaturityDelay; i++ { if i < types.MaturityDelay { continue } _, exists := dscoTracker[i] if !exists { manageErr(tx, errors.New("missing a dsco bucket")) } expectedBuckets++ } if len(dscoTracker) != expectedBuckets { manageErr(tx, errors.New("too many dsco buckets")) } }
// 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]) }
// checkSiacoinCount checks that the number of siacoins countable within the // consensus set equal the expected number of siacoins for the block height. func checkSiacoinCount(tx *bolt.Tx) { // Iterate through all the buckets looking for the delayed siacoin output // buckets, and check that they are for the correct heights. var dscoSiacoins types.Currency err := tx.ForEach(func(name []byte, b *bolt.Bucket) error { // Check if the bucket is a delayed siacoin output bucket. if !bytes.HasPrefix(name, prefixDSCO) { return nil } // Sum up the delayed outputs in this bucket. err := b.ForEach(func(_, delayedOutput []byte) error { var sco types.SiacoinOutput err := encoding.Unmarshal(delayedOutput, &sco) if err != nil { manageErr(tx, err) } dscoSiacoins = dscoSiacoins.Add(sco.Value) return nil }) if err != nil { return err } return nil }) if err != nil { manageErr(tx, err) } // Add all of the siacoin outputs. var scoSiacoins types.Currency err = tx.Bucket(SiacoinOutputs).ForEach(func(_, scoBytes []byte) error { var sco types.SiacoinOutput err := encoding.Unmarshal(scoBytes, &sco) if err != nil { manageErr(tx, err) } scoSiacoins = scoSiacoins.Add(sco.Value) return nil }) if err != nil { manageErr(tx, err) } // Add all of the payouts from file contracts. var fcSiacoins types.Currency err = tx.Bucket(FileContracts).ForEach(func(_, fcBytes []byte) error { var fc types.FileContract err := encoding.Unmarshal(fcBytes, &fc) if err != nil { manageErr(tx, err) } var fcCoins types.Currency for _, output := range fc.ValidProofOutputs { fcCoins = fcCoins.Add(output.Value) } fcSiacoins = fcSiacoins.Add(fcCoins) return nil }) if err != nil { manageErr(tx, err) } // Add all of the siafund claims. var claimSiacoins types.Currency err = tx.Bucket(SiafundOutputs).ForEach(func(_, sfoBytes []byte) error { var sfo types.SiafundOutput err := encoding.Unmarshal(sfoBytes, &sfo) if err != nil { manageErr(tx, err) } coinsPerFund := getSiafundPool(tx).Sub(sfo.ClaimStart) claimCoins := coinsPerFund.Mul(sfo.Value).Div(types.SiafundCount) claimSiacoins = claimSiacoins.Add(claimCoins) return nil }) if err != nil { manageErr(tx, err) } // Count how many coins should exist deflationBlocks := types.BlockHeight(types.InitialCoinbase - types.MinimumCoinbase) expectedSiacoins := types.CalculateCoinbase(0).Add(types.CalculateCoinbase(blockHeight(tx))).Div(types.NewCurrency64(2)) if blockHeight(tx) < deflationBlocks { expectedSiacoins = expectedSiacoins.Mul(types.NewCurrency64(uint64(blockHeight(tx) + 1))) } else { expectedSiacoins = expectedSiacoins.Mul(types.NewCurrency64(uint64(deflationBlocks + 1))) trailingSiacoins := types.NewCurrency64(uint64(blockHeight(tx) - deflationBlocks)).Mul(types.CalculateCoinbase(blockHeight(tx))) expectedSiacoins = expectedSiacoins.Add(trailingSiacoins) } totalSiacoins := dscoSiacoins.Add(scoSiacoins).Add(fcSiacoins).Add(claimSiacoins) if totalSiacoins.Cmp(expectedSiacoins) != 0 { diagnostics := fmt.Sprintf("Wrong number of siacoins\nDsco: %v\nSco: %v\nFc: %v\nClaim: %v\n", dscoSiacoins, scoSiacoins, fcSiacoins, claimSiacoins) if totalSiacoins.Cmp(expectedSiacoins) < 0 { diagnostics += fmt.Sprintf("total: %v\nexpected: %v\n expected is bigger: %v", totalSiacoins, expectedSiacoins, expectedSiacoins.Sub(totalSiacoins)) } else { diagnostics += fmt.Sprintf("total: %v\nexpected: %v\n expected is bigger: %v", totalSiacoins, expectedSiacoins, totalSiacoins.Sub(expectedSiacoins)) } manageErr(tx, errors.New(diagnostics)) } }