// TestApplySiacoinOutputs probes the applySiacoinOutput method of the // consensus set. func TestApplySiacoinOutputs(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestApplySiacoinOutputs") if err != nil { t.Fatal(err) } // Create a block node to use with application. bn := new(blockNode) // Apply a transaction with a single siacoin output. txn := types.Transaction{ SiacoinOutputs: []types.SiacoinOutput{{}}, } cst.cs.applySiacoinOutputs(bn, txn) scoid := txn.SiacoinOutputID(0) _, exists := cst.cs.siacoinOutputs[scoid] if !exists { t.Error("Failed to create siacoin output") } if len(cst.cs.siacoinOutputs) != 3 { // 3 because createConsensusSetTester has 2 initially. t.Error("siacoin outputs not correctly updated") } if len(bn.siacoinOutputDiffs) != 1 { t.Error("block node was not updated for single element transaction") } if bn.siacoinOutputDiffs[0].Direction != modules.DiffApply { t.Error("wrong diff direction applied when creating a siacoin output") } if bn.siacoinOutputDiffs[0].ID != scoid { t.Error("wrong id used when creating a siacoin output") } // Apply a transaction with 2 siacoin outputs. txn = types.Transaction{ SiacoinOutputs: []types.SiacoinOutput{ {Value: types.NewCurrency64(1)}, {Value: types.NewCurrency64(2)}, }, } cst.cs.applySiacoinOutputs(bn, txn) scoid0 := txn.SiacoinOutputID(0) scoid1 := txn.SiacoinOutputID(1) _, exists = cst.cs.siacoinOutputs[scoid0] if !exists { t.Error("Failed to create siacoin output") } _, exists = cst.cs.siacoinOutputs[scoid1] if !exists { t.Error("Failed to create siacoin output") } if len(cst.cs.siacoinOutputs) != 5 { // 5 because createConsensusSetTester has 2 initially. t.Error("siacoin outputs not correctly updated") } if len(bn.siacoinOutputDiffs) != 3 { t.Error("block node was not updated correctly") } }
// applySiacoinOutputs takes all of the siacoin outputs in a transaction and // applies them to the state, updating the diffs in the processed block. func applySiacoinOutputs(tx *bolt.Tx, pb *processedBlock, t types.Transaction) { // Add all siacoin outputs to the unspent siacoin outputs list. for i, sco := range t.SiacoinOutputs { scoid := t.SiacoinOutputID(uint64(i)) scod := modules.SiacoinOutputDiff{ Direction: modules.DiffApply, ID: scoid, SiacoinOutput: sco, } pb.SiacoinOutputDiffs = append(pb.SiacoinOutputDiffs, scod) commitSiacoinOutputDiff(tx, scod, modules.DiffApply) } }
// removeSiacoinOutputs removes all of the siacoin outputs of a transaction // from the unconfirmed consensus set. func (tp *TransactionPool) removeSiacoinOutputs(t types.Transaction) { for i, _ := range t.SiacoinOutputs { scoid := t.SiacoinOutputID(i) // Sanity check - the output should exist in the unconfirmed set as // there should be no transaction dependents who have spent the output. if build.DEBUG { _, exists := tp.siacoinOutputs[scoid] if !exists { panic("trying to delete missing siacoin output") } } delete(tp.siacoinOutputs, scoid) } }
// applySiacoinOutputs takes all of the siacoin outputs in a transaction and // applies them to the state, updating the diffs in the processed block. func (cs *ConsensusSet) applySiacoinOutputs(tx *bolt.Tx, pb *processedBlock, t types.Transaction) error { // Add all siacoin outputs to the unspent siacoin outputs list. scoBucket := tx.Bucket(SiacoinOutputs) for i, sco := range t.SiacoinOutputs { scoid := t.SiacoinOutputID(i) scod := modules.SiacoinOutputDiff{ Direction: modules.DiffApply, ID: scoid, SiacoinOutput: sco, } pb.SiacoinOutputDiffs = append(pb.SiacoinOutputDiffs, scod) err := cs.commitBucketSiacoinOutputDiff(scoBucket, scod, modules.DiffApply) if err != nil { return err } } return nil }
// applySiacoinOutputs takes all of the siacoin outputs in a transaction and // applies them to the state, updating the diffs in the block node. func (cs *State) applySiacoinOutputs(bn *blockNode, t types.Transaction) { // Add all siacoin outputs to the unspent siacoin outputs list. for i, sco := range t.SiacoinOutputs { // Sanity check - the output should not exist within the state. scoid := t.SiacoinOutputID(i) if build.DEBUG { _, exists := cs.siacoinOutputs[scoid] if exists { panic(ErrMisuseApplySiacoinOutput) } } scod := modules.SiacoinOutputDiff{ Direction: modules.DiffApply, ID: scoid, SiacoinOutput: sco, } bn.siacoinOutputDiffs = append(bn.siacoinOutputDiffs, scod) cs.commitSiacoinOutputDiff(scod, modules.DiffApply) } }
// TestPartialConfirmationWeave checks that the transaction pool correctly // accepts a transaction set which has parents that have been accepted by the // consensus set but not the whole set has been accepted by the consensus set, // this time weaving the dependencies, such that the first transaction is not // in the consensus set, the second is, and the third has both as dependencies. func TestPartialConfirmationWeave(t *testing.T) { if testing.Short() { t.SkipNow() } tpt, err := createTpoolTester("TestPartialConfirmation") if err != nil { t.Fatal(err) } defer tpt.Close() // Step 1: create an output to the empty address in a tx. // Step 2: create a second output to the empty address in another tx. // Step 3: create a transaction using both those outputs. // Step 4: mine the txn set in step 2 // Step 5: Submit the complete set. // Create a transaction with a single output to a fully controlled address. emptyUH := types.UnlockConditions{}.UnlockHash() builder1 := tpt.wallet.StartTransaction() funding1 := types.NewCurrency64(1e9) err = builder1.FundSiacoins(funding1) if err != nil { t.Fatal(err) } scOutput1 := types.SiacoinOutput{ Value: funding1, UnlockHash: emptyUH, } i1 := builder1.AddSiacoinOutput(scOutput1) tSet1, err := builder1.Sign(true) if err != nil { t.Fatal(err) } // Submit to the transaction pool and mine the block, to minimize // complexity. err = tpt.tpool.AcceptTransactionSet(tSet1) if err != nil { t.Fatal(err) } _, err = tpt.miner.AddBlock() if err != nil { t.Fatal(err) } // Create a second output to the fully controlled address, to fund the // second transaction in the weave. builder2 := tpt.wallet.StartTransaction() funding2 := types.NewCurrency64(2e9) err = builder2.FundSiacoins(funding2) if err != nil { t.Fatal(err) } scOutput2 := types.SiacoinOutput{ Value: funding2, UnlockHash: emptyUH, } i2 := builder2.AddSiacoinOutput(scOutput2) tSet2, err := builder2.Sign(true) if err != nil { t.Fatal(err) } // Submit to the transaction pool and mine the block, to minimize // complexity. err = tpt.tpool.AcceptTransactionSet(tSet2) if err != nil { t.Fatal(err) } _, err = tpt.miner.AddBlock() if err != nil { t.Fatal(err) } // Create a passthrough transaction for output1 and output2, so that they // can be used as unconfirmed dependencies. txn1 := types.Transaction{ SiacoinInputs: []types.SiacoinInput{{ ParentID: tSet1[len(tSet1)-1].SiacoinOutputID(i1), }}, SiacoinOutputs: []types.SiacoinOutput{{ Value: funding1, UnlockHash: emptyUH, }}, } txn2 := types.Transaction{ SiacoinInputs: []types.SiacoinInput{{ ParentID: tSet2[len(tSet2)-1].SiacoinOutputID(i2), }}, SiacoinOutputs: []types.SiacoinOutput{{ Value: funding2, UnlockHash: emptyUH, }}, } // Create a child transaction that depends on inputs from both txn1 and // txn2. child := types.Transaction{ SiacoinInputs: []types.SiacoinInput{ { ParentID: txn1.SiacoinOutputID(0), }, { ParentID: txn2.SiacoinOutputID(0), }, }, SiacoinOutputs: []types.SiacoinOutput{{ Value: funding1.Add(funding2), }}, } // Get txn2 accepted into the consensus set. err = tpt.tpool.AcceptTransactionSet([]types.Transaction{txn2}) if err != nil { t.Fatal(err) } _, err = tpt.miner.AddBlock() if err != nil { t.Fatal(err) } // Try to get the set of txn1, txn2, and child accepted into the // transaction pool. err = tpt.tpool.AcceptTransactionSet([]types.Transaction{txn1, txn2, child}) if err != nil { t.Fatal(err) } }
// 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 }
// buildExplorerTransaction takes a transaction and the height + id of the // block it appears in an uses that to build an explorer transaction. func (srv *Server) buildExplorerTransaction(height types.BlockHeight, parent types.BlockID, txn types.Transaction) (et ExplorerTransaction) { // Get the header information for the transaction. et.ID = txn.ID() et.Height = height et.Parent = parent et.RawTransaction = txn // Add the siacoin outputs that correspond with each siacoin input. for _, sci := range txn.SiacoinInputs { sco, exists := srv.explorer.SiacoinOutput(sci.ParentID) if build.DEBUG && !exists { panic("could not find corresponding siacoin output") } et.SiacoinInputOutputs = append(et.SiacoinInputOutputs, sco) } for i := range txn.SiacoinOutputs { et.SiacoinOutputIDs = append(et.SiacoinOutputIDs, txn.SiacoinOutputID(uint64(i))) } // Add all of the valid and missed proof ids as extra data to the file // contracts. for i, fc := range txn.FileContracts { fcid := txn.FileContractID(uint64(i)) var fcvpoids []types.SiacoinOutputID var fcmpoids []types.SiacoinOutputID for j := range fc.ValidProofOutputs { fcvpoids = append(fcvpoids, fcid.StorageProofOutputID(types.ProofValid, uint64(j))) } for j := range fc.MissedProofOutputs { fcmpoids = append(fcmpoids, fcid.StorageProofOutputID(types.ProofMissed, uint64(j))) } et.FileContractIDs = append(et.FileContractIDs, fcid) et.FileContractValidProofOutputIDs = append(et.FileContractValidProofOutputIDs, fcvpoids) et.FileContractMissedProofOutputIDs = append(et.FileContractMissedProofOutputIDs, fcmpoids) } // Add all of the valid and missed proof ids as extra data to the file // contract revisions. for _, fcr := range txn.FileContractRevisions { var fcrvpoids []types.SiacoinOutputID var fcrmpoids []types.SiacoinOutputID for j := range fcr.NewValidProofOutputs { fcrvpoids = append(fcrvpoids, fcr.ParentID.StorageProofOutputID(types.ProofValid, uint64(j))) } for j := range fcr.NewMissedProofOutputs { fcrmpoids = append(fcrmpoids, fcr.ParentID.StorageProofOutputID(types.ProofMissed, uint64(j))) } et.FileContractValidProofOutputIDs = append(et.FileContractValidProofOutputIDs, fcrvpoids) et.FileContractMissedProofOutputIDs = append(et.FileContractMissedProofOutputIDs, fcrmpoids) } // Add all of the output ids and outputs corresponding with each storage // proof. for _, sp := range txn.StorageProofs { fileContract, fileContractRevisions, fileContractExists, _ := srv.explorer.FileContractHistory(sp.ParentID) if !fileContractExists && build.DEBUG { panic("could not find a file contract connected with a storage proof") } var storageProofOutputs []types.SiacoinOutput if len(fileContractRevisions) > 0 { storageProofOutputs = fileContractRevisions[len(fileContractRevisions)-1].NewValidProofOutputs } else { storageProofOutputs = fileContract.ValidProofOutputs } var storageProofOutputIDs []types.SiacoinOutputID for i := range storageProofOutputs { storageProofOutputIDs = append(storageProofOutputIDs, sp.ParentID.StorageProofOutputID(types.ProofValid, uint64(i))) } et.StorageProofOutputIDs = append(et.StorageProofOutputIDs, storageProofOutputIDs) et.StorageProofOutputs = append(et.StorageProofOutputs, storageProofOutputs) } // Add the siafund outputs that correspond to each siacoin input. for _, sci := range txn.SiafundInputs { sco, exists := srv.explorer.SiafundOutput(sci.ParentID) if build.DEBUG && !exists { panic("could not find corresponding siafund output") } et.SiafundInputOutputs = append(et.SiafundInputOutputs, sco) } for i := range txn.SiafundOutputs { et.SiafundOutputIDs = append(et.SiafundOutputIDs, txn.SiafundOutputID(uint64(i))) } for _, sfi := range txn.SiafundInputs { et.SiaClaimOutputIDs = append(et.SiaClaimOutputIDs, sfi.ParentID.SiaClaimOutputID()) } return et }
// addTransaction is called from addBlockDB, and delegates the adding // of information to the database to the functions defined above func (tx *boltTx) addTransaction(txn types.Transaction) { // Store this for quick lookup txid := txn.ID() // Append each input to the list of modifications for _, input := range txn.SiacoinInputs { tx.addAddress(input.UnlockConditions.UnlockHash(), txid) tx.addSiacoinInput(input.ParentID, txid) } // Handle all the transaction outputs for i, output := range txn.SiacoinOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewOutput(txn.SiacoinOutputID(uint64(i)), txid) } // Handle each file contract individually for i, contract := range txn.FileContracts { fcid := txn.FileContractID(uint64(i)) tx.addNewHash("FileContracts", hashFilecontract, crypto.Hash(fcid), fcInfo{ Contract: txid, }) for j, output := range contract.ValidProofOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewOutput(fcid.StorageProofOutputID(true, uint64(j)), txid) } for j, output := range contract.MissedProofOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewOutput(fcid.StorageProofOutputID(false, uint64(j)), txid) } tx.addAddress(contract.UnlockHash, txid) } // Update the list of revisions for _, revision := range txn.FileContractRevisions { tx.addFcRevision(revision.ParentID, txid) // Note the old outputs will still be there in the // database. This is to provide information to the // people who may just need it. for i, output := range revision.NewValidProofOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewOutput(revision.ParentID.StorageProofOutputID(true, uint64(i)), txid) } for i, output := range revision.NewMissedProofOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewOutput(revision.ParentID.StorageProofOutputID(false, uint64(i)), txid) } tx.addAddress(revision.NewUnlockHash, txid) } // Update the list of storage proofs for _, proof := range txn.StorageProofs { tx.addFcProof(proof.ParentID, txid) } // Append all the siafund inputs to the modification list for _, input := range txn.SiafundInputs { tx.addSiafundInput(input.ParentID, txid) } // Handle all the siafund outputs for i, output := range txn.SiafundOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewSFOutput(txn.SiafundOutputID(uint64(i)), txid) } tx.putObject("Hashes", txid, hashTransaction) }
// 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 }