// 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 }
// 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 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 }
// 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 }
// 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, } }
// 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 }
// 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)) } } }
// 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") } }
// 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 }
// 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 }
// 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")) } }
// 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 }
// 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 }
// 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 }
// SendSiagSiafunds sends siafunds to another address. The siacoins stored in // the siafunds are sent to an address in the wallet. func (w *Wallet) SendSiagSiafunds(amount types.Currency, dest types.UnlockHash, keyfiles []string) (types.Transaction, error) { if len(keyfiles) < 1 { return types.Transaction{}, ErrNoKeyfile } // Load the siafund keys and verify they are sufficient to sign the // transaction. skps := make([]SiagKeyPair, len(keyfiles)) for i, keyfile := range keyfiles { err := encoding.ReadFile(keyfile, &skps[i]) if err != nil { return types.Transaction{}, err } if skps[i].Header != SiagFileHeader { return types.Transaction{}, ErrUnknownHeader } if skps[i].Version != SiagFileVersion { return types.Transaction{}, ErrUnknownVersion } } // Check that all of the loaded files have the same address, and that there // are enough to create the transaction. baseUnlockHash := skps[0].UnlockConditions.UnlockHash() for _, skp := range skps { if skp.UnlockConditions.UnlockHash() != baseUnlockHash { return types.Transaction{}, ErrInconsistentKeys } } if uint64(len(skps)) < skps[0].UnlockConditions.SignaturesRequired { return types.Transaction{}, ErrInsufficientKeys } // Check that there are enough siafunds in the key to complete the spend. lockID := w.mu.RLock() var availableSiafunds types.Currency var sfoids []types.SiafundOutputID for sfoid, sfo := range w.siafundOutputs { if sfo.UnlockHash == baseUnlockHash { availableSiafunds = availableSiafunds.Add(sfo.Value) sfoids = append(sfoids, sfoid) } if availableSiafunds.Cmp(amount) >= 0 { break } } w.mu.RUnlock(lockID) if availableSiafunds.Cmp(amount) < 0 { return types.Transaction{}, ErrInsufficientSiafunds } // Truncate the keys to exactly the number needed. skps = skps[:skps[0].UnlockConditions.SignaturesRequired] // Assemble the base transction, including a 10 siacoin fee if possible. id, err := w.RegisterTransaction(types.Transaction{}) if err != nil { return types.Transaction{}, err } // Add a miner fee - if funding the transaction fails, we'll just send a // transaction with no fee. txn, err := w.FundTransaction(id, types.NewCurrency64(TransactionFee)) if err == nil { txn, _, err = w.AddMinerFee(id, types.NewCurrency64(TransactionFee)) if err != nil { return types.Transaction{}, err } } // Add the siafund inputs to the transcation. for _, sfoid := range sfoids { // Get an address for the siafund claims. lockID := w.mu.Lock() claimDest, _, err := w.coinAddress(false) w.mu.Unlock(lockID) if err != nil { return types.Transaction{}, err } // Assemble the SiafundInput to spend this output. sfi := types.SiafundInput{ ParentID: sfoid, UnlockConditions: skps[0].UnlockConditions, ClaimUnlockHash: claimDest, } txn, _, err = w.AddSiafundInput(id, sfi) if err != nil { return types.Transaction{}, err } } // Add the siafund output to the transaction. sfo := types.SiafundOutput{ Value: amount, UnlockHash: dest, } txn, _, err = w.AddSiafundOutput(id, sfo) if err != nil { return types.Transaction{}, err } // Add a refund siafund output if needed. if amount.Cmp(availableSiafunds) != 0 { refund := availableSiafunds.Sub(amount) sfo := types.SiafundOutput{ Value: refund, UnlockHash: baseUnlockHash, } txn, _, err = w.AddSiafundOutput(id, sfo) if err != nil { return types.Transaction{}, err } } // Add signatures for the siafund inputs. sigIndex := 0 for _, sfoid := range sfoids { for _, key := range skps { txnSig := types.TransactionSignature{ ParentID: crypto.Hash(sfoid), CoveredFields: types.CoveredFields{WholeTransaction: true}, PublicKeyIndex: uint64(key.Index), } txn.TransactionSignatures = append(txn.TransactionSignatures, txnSig) sigHash := txn.SigHash(sigIndex) encodedSig, err := crypto.SignHash(sigHash, key.SecretKey) if err != nil { return types.Transaction{}, err } txn.TransactionSignatures[sigIndex].Signature = encodedSig[:] txn, _, err = w.AddTransactionSignature(id, txn.TransactionSignatures[sigIndex]) if err != nil { return types.Transaction{}, err } sigIndex++ } } // Sign the transaction. txn, err = w.SignTransaction(id, true) if err != nil { return types.Transaction{}, err } err = w.tpool.AcceptTransaction(txn) if err != nil { return types.Transaction{}, err } return txn, 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 }
// 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")) } }
// FundTransaction adds siacoins to a transaction that the wallet knows how to // spend. The exact amount of coins are always added, and this is achieved by // creating two transactions. The first transaciton, the parent, spends a set // of outputs that add up to at least the desired amount, and then creates a // single output of the exact amount and a second refund output. func (w *Wallet) FundTransaction(id string, amount types.Currency) (t types.Transaction, err error) { counter := w.mu.Lock() defer w.mu.Unlock(counter) // Create a parent transaction and supply it with enough inputs to cover // 'amount'. parentTxn := types.Transaction{} fundingOutputs, fundingTotal, err := w.findOutputs(amount) if err != nil { return } for _, output := range fundingOutputs { output.age = w.age key := w.keys[output.output.UnlockHash] newInput := types.SiacoinInput{ ParentID: output.id, UnlockConditions: key.unlockConditions, } parentTxn.SiacoinInputs = append(parentTxn.SiacoinInputs, newInput) } // Create and add the output that will be used to fund the standard // transaction. parentDest, parentSpendConds, err := w.coinAddress(false) // false indicates that the address should not be visible to the user exactOutput := types.SiacoinOutput{ Value: amount, UnlockHash: parentDest, } parentTxn.SiacoinOutputs = append(parentTxn.SiacoinOutputs, exactOutput) // Create a refund output if needed. if amount.Cmp(fundingTotal) != 0 { var refundDest types.UnlockHash refundDest, _, err = w.coinAddress(false) // false indicates that the address should not be visible to the user if err != nil { return } refundOutput := types.SiacoinOutput{ Value: fundingTotal.Sub(amount), UnlockHash: refundDest, } parentTxn.SiacoinOutputs = append(parentTxn.SiacoinOutputs, refundOutput) } // Sign all of the inputs to the parent trancstion. coveredFields := types.CoveredFields{WholeTransaction: true} for _, input := range parentTxn.SiacoinInputs { sig := types.TransactionSignature{ ParentID: crypto.Hash(input.ParentID), CoveredFields: coveredFields, PublicKeyIndex: 0, } parentTxn.TransactionSignatures = append(parentTxn.TransactionSignatures, sig) // Hash the transaction according to the covered fields. coinAddress := input.UnlockConditions.UnlockHash() sigIndex := len(parentTxn.TransactionSignatures) - 1 secKey := w.keys[coinAddress].secretKey sigHash := parentTxn.SigHash(sigIndex) // Get the signature. var encodedSig crypto.Signature encodedSig, err = crypto.SignHash(sigHash, secKey) if err != nil { return } parentTxn.TransactionSignatures[sigIndex].Signature = types.Signature(encodedSig[:]) } // Add the exact output to the wallet's knowledgebase before releasing the // lock, to prevent the wallet from using the exact output elsewhere. key := w.keys[parentSpendConds.UnlockHash()] key.outputs[parentTxn.SiacoinOutputID(0)] = &knownOutput{ id: parentTxn.SiacoinOutputID(0), output: exactOutput, spendable: true, age: w.age, } // Send the transaction to the transaction pool. err = w.tpool.AcceptTransaction(parentTxn) if err != nil { return } // Get the transaction that was originally meant to be funded. openTxn, exists := w.transactions[id] if !exists { err = ErrInvalidID return } txn := openTxn.transaction // Add the exact output. newInput := types.SiacoinInput{ ParentID: parentTxn.SiacoinOutputID(0), UnlockConditions: parentSpendConds, } openTxn.inputs = append(openTxn.inputs, len(txn.SiacoinInputs)) txn.SiacoinInputs = append(txn.SiacoinInputs, newInput) t = *txn return }
// 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. // // TODO: The implementation of FundSiacoins is known to have quirks/bugs // (non-fatal), and has diverged from the implementation of FundSiacoins. The // implementations should be converged once again. func (tb *transactionBuilder) FundSiafunds(amount types.Currency) error { lockID := tb.wallet.mu.Lock() defer tb.wallet.mu.Unlock(lockID) // Create and fund a parent transaction that will add the correct amount of // siafunds to the transaction. var fund types.Currency parentTxn := types.Transaction{} for scoid, sco := range tb.wallet.siafundOutputs { // Check that this output has not recently been spent by the wallet. spendHeight := tb.wallet.spentOutputs[types.OutputID(scoid)] if spendHeight > tb.wallet.consensusSetHeight-RespendTimeout { continue } outputUnlockConditions := tb.wallet.keys[sco.UnlockHash].unlockConditions if tb.wallet.consensusSetHeight < outputUnlockConditions.Timelock { continue } // Mark the output as spent. tb.wallet.spentOutputs[types.OutputID(scoid)] = tb.wallet.consensusSetHeight // Add a siafund input for this output. parentClaimUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } sci := types.SiafundInput{ ParentID: scoid, UnlockConditions: outputUnlockConditions, ClaimUnlockHash: parentClaimUnlockConditions.UnlockHash(), } parentTxn.SiafundInputs = append(parentTxn.SiafundInputs, sci) // Add the output to the total fund fund = fund.Add(sco.Value) if fund.Cmp(amount) >= 0 { break } } // 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.parents = append(tb.parents, parentTxn) tb.siafundInputs = append(tb.siafundInputs, len(tb.transaction.SiafundInputs)) tb.transaction.SiafundInputs = append(tb.transaction.SiafundInputs, newInput) return nil }