// 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 }
// TestSendSiacoins probes the SendSiacoins method of the wallet. func TestSendSiacoins(t *testing.T) { if testing.Short() { t.SkipNow() } wt, err := createWalletTester("TestSendSiacoins") if err != nil { t.Fatal(err) } defer wt.closeWt() // Get the initial balance - should be 1 block. The unconfirmed balances // should be 0. confirmedBal, _, _ := wt.wallet.ConfirmedBalance() unconfirmedOut, unconfirmedIn := wt.wallet.UnconfirmedBalance() if confirmedBal.Cmp(types.CalculateCoinbase(1)) != 0 { t.Error("unexpected confirmed balance") } if unconfirmedOut.Cmp(types.ZeroCurrency) != 0 { t.Error("unconfirmed balance should be 0") } if unconfirmedIn.Cmp(types.ZeroCurrency) != 0 { t.Error("unconfirmed balance should be 0") } // Send 5000 hastings. The wallet will automatically add a fee. Outgoing // unconfirmed siacoins - incoming unconfirmed siacoins should equal 5000 + // fee. tpoolFee := types.NewCurrency64(10).Mul(types.SiacoinPrecision) _, err = wt.wallet.SendSiacoins(types.NewCurrency64(5000), types.UnlockHash{}) if err != nil { t.Fatal(err) } confirmedBal2, _, _ := wt.wallet.ConfirmedBalance() unconfirmedOut2, unconfirmedIn2 := wt.wallet.UnconfirmedBalance() if confirmedBal2.Cmp(confirmedBal) != 0 { t.Error("confirmed balance changed without introduction of blocks") } if unconfirmedOut2.Cmp(unconfirmedIn2.Add(types.NewCurrency64(5000)).Add(tpoolFee)) != 0 { t.Error("sending siacoins appears to be ineffective") } // Move the balance into the confirmed set. b, _ := wt.miner.FindBlock() err = wt.cs.AcceptBlock(b) if err != nil { t.Fatal(err) } confirmedBal3, _, _ := wt.wallet.ConfirmedBalance() unconfirmedOut3, unconfirmedIn3 := wt.wallet.UnconfirmedBalance() if confirmedBal3.Cmp(confirmedBal2.Add(types.CalculateCoinbase(2)).Sub(types.NewCurrency64(5000)).Sub(tpoolFee)) != 0 { t.Error("confirmed balance did not adjust to the expected value") } if unconfirmedOut3.Cmp(types.ZeroCurrency) != 0 { t.Error("unconfirmed balance should be 0") } if unconfirmedIn3.Cmp(types.ZeroCurrency) != 0 { t.Error("unconfirmed balance should be 0") } }
// testDependentUpdates adds a parent transaction and a dependent transaction // to the unconfirmed set. Then the parent transaction is added to the // confirmed set but the dependent is not. A check is made to see that the // dependent is still in the unconfirmed set. func (tpt *tpoolTester) testDependentUpdates() { // Put two transactions, a parent and a dependent, into the transaction // pool. Then create a transaction that is in conflict with the parent. parent := tpt.emptyUnlockTransaction() dependent := types.Transaction{ SiacoinInputs: []types.SiacoinInput{ types.SiacoinInput{ ParentID: parent.SiacoinOutputID(0), }, }, MinerFees: []types.Currency{ parent.SiacoinOutputs[0].Value, }, } err := tpt.tpool.AcceptTransaction(parent) if err != nil { tpt.t.Fatal(err) } tpt.tpUpdateWait() err = tpt.tpool.AcceptTransaction(dependent) if err != nil { tpt.t.Fatal(err) } tpt.tpUpdateWait() // Mine a block to put the parent into the confirmed set. tset := tpt.tpool.TransactionSet() tset = tset[:len(tset)-1] // strip 'dependent' target, exists := tpt.cs.ChildTarget(tpt.cs.CurrentBlock().ID()) if !exists { tpt.t.Fatal("unable to recover child target") } block := types.Block{ ParentID: tpt.cs.CurrentBlock().ID(), Timestamp: types.Timestamp(time.Now().Unix()), MinerPayouts: []types.SiacoinOutput{ types.SiacoinOutput{Value: types.CalculateCoinbase(tpt.cs.Height() + 1)}, }, Transactions: tset, } for { var found bool block, found = tpt.miner.SolveBlock(block, target) if found { err = tpt.cs.AcceptBlock(block) if err != nil { tpt.t.Fatal(err) } break } } tpt.csUpdateWait() // Check that 'parent' and 'dependent' have been removed from the // transaction set, since conflict has made the confirmed set. if len(tpt.tpool.TransactionSet()) != 1 { tpt.t.Error("dependent transaction does not remain unconfirmed after parent has been confirmed:", len(tset)) } }
// TestBuriedBadFork creates a block with an invalid transaction that's not on // the longest fork. The consensus set will not validate that block. Then valid // blocks are added on top of it to make it the longest fork. When it becomes // the longest fork, all the blocks should be fully validated and thrown out // because a parent is invalid. func TestBuriedBadFork(t *testing.T) { if testing.Short() { t.SkipNow() } t.Parallel() cst, err := createConsensusSetTester("TestBuriedBadFork") if err != nil { t.Fatal(err) } defer cst.Close() pb := cst.cs.dbCurrentProcessedBlock() // Create a bad block that builds on a parent, so that it is part of not // the longest fork. badBlock := types.Block{ ParentID: pb.Block.ParentID, Timestamp: types.CurrentTimestamp(), MinerPayouts: []types.SiacoinOutput{{Value: types.CalculateCoinbase(pb.Height)}}, Transactions: []types.Transaction{{ SiacoinInputs: []types.SiacoinInput{{}}, // Will trigger an error on full verification but not partial verification. }}, } parent, err := cst.cs.dbGetBlockMap(pb.Block.ParentID) if err != nil { t.Fatal(err) } badBlock, _ = cst.miner.SolveBlock(badBlock, parent.ChildTarget) err = cst.cs.AcceptBlock(badBlock) if err != modules.ErrNonExtendingBlock { t.Fatal(err) } // Build another bock on top of the bad block that is fully valid, this // will cause a fork and full validation of the bad block, both the bad // block and this block should be thrown away. block := types.Block{ ParentID: badBlock.ID(), Timestamp: types.CurrentTimestamp(), MinerPayouts: []types.SiacoinOutput{{Value: types.CalculateCoinbase(pb.Height + 1)}}, } block, _ = cst.miner.SolveBlock(block, parent.ChildTarget) // okay because the target will not change err = cst.cs.AcceptBlock(block) if err == nil { t.Fatal("a bad block failed to cause an error") } }
// TestCheckMinerPayouts probes the checkMinerPayouts function. func TestCheckMinerPayouts(t *testing.T) { // All tests are done at height = 0. coinbase := types.CalculateCoinbase(0) // Create a block with a single valid payout. b := types.Block{ MinerPayouts: []types.SiacoinOutput{ {Value: coinbase}, }, } if !checkMinerPayouts(b, 0) { t.Error("payouts evaluated incorrectly when there is only one payout.") } // Try a block with an incorrect payout. b = types.Block{ MinerPayouts: []types.SiacoinOutput{ {Value: coinbase.Sub(types.NewCurrency64(1))}, }, } if checkMinerPayouts(b, 0) { t.Error("payouts evaluated incorrectly when there is a too-small payout") } // Try a block with 2 payouts. b = types.Block{ MinerPayouts: []types.SiacoinOutput{ {Value: coinbase.Sub(types.NewCurrency64(1))}, {Value: types.NewCurrency64(1)}, }, } if !checkMinerPayouts(b, 0) { t.Error("payouts evaluated incorrectly when there are 2 payouts") } // Try a block with 2 payouts that are too large. b = types.Block{ MinerPayouts: []types.SiacoinOutput{ {Value: coinbase}, {Value: coinbase}, }, } if checkMinerPayouts(b, 0) { t.Error("payouts evaluated incorrectly when there are two large payouts") } // Create a block with an empty payout. b = types.Block{ MinerPayouts: []types.SiacoinOutput{ {Value: coinbase}, {}, }, } if checkMinerPayouts(b, 0) { t.Error("payouts evaluated incorrectly when there is only one payout.") } }
// testFundTransaction funds and completes a transaction using the // build-your-own transaction functions, checking that a no-refund transaction // is created that is valid. func (wt *walletTester) testFundTransaction() error { // Build a transaction that intentionally needs a refund. id, err := wt.wallet.RegisterTransaction(types.Transaction{}) fund := wt.wallet.Balance(false).Sub(types.NewCurrency64(1)) if err != nil { return err } _, err = wt.wallet.FundTransaction(id, fund) if err != nil { return err } wt.tpUpdateWait() _, _, err = wt.wallet.AddMinerFee(id, fund) if err != nil { return err } t, err := wt.wallet.SignTransaction(id, true) if err != nil { return err } err = wt.tpool.AcceptTransaction(t) if err != nil { return err } wt.tpUpdateWait() // Check that the length of the created transaction is 1 siacoin, and that // the unconfirmed balance of the wallet is 1. if len(t.SiacoinOutputs) != 0 { return errors.New("expecting 0 siacoin outputs, got non-zero result") } if wt.wallet.Balance(true).Cmp(types.NewCurrency64(1)) != 0 { return errors.New("incorrect balance being reported") } // Dump the transaction pool into a block and see that the balance still // registers correctly. b, _ := wt.miner.FindBlock() err = wt.cs.AcceptBlock(b) if err != nil { return err } wt.csUpdateWait() // Check that the length of the created transaction is 1 siacoin, and that // the unconfirmed balance of the wallet is 1 + BlockReward. if len(t.SiacoinOutputs) != 0 { return errors.New("wrong number of siacoin outputs - expecting 0") } expectedBalance := types.CalculateCoinbase(2).Add(types.NewCurrency64(1)) if bal := wt.wallet.Balance(true); bal.Cmp(expectedBalance) != 0 { return errors.New("did not arrive at the expected balance") } return nil }
// createConsensusObjects initialzes the consensus portions of the database. func (cs *ConsensusSet) createConsensusDB(tx *bolt.Tx) error { // Enumerate and create the database buckets. buckets := [][]byte{ BlockHeight, BlockMap, BlockPath, Consistency, SiacoinOutputs, FileContracts, SiafundOutputs, SiafundPool, } for _, bucket := range buckets { _, err := tx.CreateBucket(bucket) if err != nil { return err } } // Set the block height to -1, so the genesis block is at height 0. blockHeight := tx.Bucket(BlockHeight) underflow := types.BlockHeight(0) err := blockHeight.Put(BlockHeight, encoding.Marshal(underflow-1)) if err != nil { return err } // Set the siafund pool to 0. setSiafundPool(tx, types.NewCurrency64(0)) // Update the siafund output diffs map for the genesis block on disk. This // needs to happen between the database being opened/initilized and the // consensus set hash being calculated for _, sfod := range cs.blockRoot.SiafundOutputDiffs { commitSiafundOutputDiff(tx, sfod, modules.DiffApply) } // Add the miner payout from the genesis block to the delayed siacoin // outputs - unspendable, as the unlock hash is blank. createDSCOBucket(tx, types.MaturityDelay) addDSCO(tx, types.MaturityDelay, cs.blockRoot.Block.MinerPayoutID(0), types.SiacoinOutput{ Value: types.CalculateCoinbase(0), UnlockHash: types.UnlockHash{}, }) // Add the genesis block to the block strucutres - checksum must be taken // after pushing the genesis block into the path. pushPath(tx, cs.blockRoot.Block.ID()) if build.DEBUG { cs.blockRoot.ConsensusChecksum = consensusChecksum(tx) } addBlockMap(tx, &cs.blockRoot) return nil }
// TestBuriedBadTransaction tries submitting a block with a bad transaction // that is buried under good transactions. func TestBuriedBadTransaction(t *testing.T) { if testing.Short() { t.SkipNow() } t.Parallel() cst, err := createConsensusSetTester("TestBuriedBadTransaction") if err != nil { t.Fatal(err) } defer cst.Close() pb := cst.cs.dbCurrentProcessedBlock() // Create a good transaction using the wallet. txnValue := types.NewCurrency64(1200) txnBuilder := cst.wallet.StartTransaction() err = txnBuilder.FundSiacoins(txnValue) if err != nil { t.Fatal(err) } txnBuilder.AddSiacoinOutput(types.SiacoinOutput{Value: txnValue}) txnSet, err := txnBuilder.Sign(true) if err != nil { t.Fatal(err) } err = cst.tpool.AcceptTransactionSet(txnSet) if err != nil { t.Fatal(err) } // Create a bad transaction badTxn := types.Transaction{ SiacoinInputs: []types.SiacoinInput{{}}, } txns := append(cst.tpool.TransactionList(), badTxn) // Create a block with a buried bad transaction. block := types.Block{ ParentID: pb.Block.ID(), Timestamp: types.CurrentTimestamp(), MinerPayouts: []types.SiacoinOutput{{Value: types.CalculateCoinbase(pb.Height + 1)}}, Transactions: txns, } block, _ = cst.miner.SolveBlock(block, pb.ChildTarget) err = cst.cs.AcceptBlock(block) if err == nil { t.Error("buried transaction didn't cause an error") } }
// initDatabase is run when the database. This has become the true // init function for consensus set func (cs *ConsensusSet) initSetDB() error { err := cs.db.startConsistencyGuard() if err != nil { return err } // add genesis block err = cs.db.addBlockMap(&cs.blockRoot) if err != nil { return err } err = cs.db.pushPath(cs.blockRoot.Block.ID()) if err != nil { return err } // Set the siafund pool to 0. err = cs.db.Update(func(tx *bolt.Tx) error { sfpBucket := tx.Bucket(SiafundPool) return sfpBucket.Put(SiafundPool, encoding.Marshal(types.NewCurrency64(0))) }) if err != nil { return err } // Update the siafundoutput diffs map for the genesis block on // disk. This needs to happen between the database being // opened/initilized and the consensus set hash being calculated for _, sfod := range cs.blockRoot.SiafundOutputDiffs { cs.commitSiafundOutputDiff(sfod, modules.DiffApply) } // Prevent the miner payout for the genesis block from being spent cs.db.addSiacoinOutputs(cs.blockRoot.Block.MinerPayoutID(0), types.SiacoinOutput{ Value: types.CalculateCoinbase(0), UnlockHash: types.UnlockHash{}, }) if build.DEBUG { cs.blockRoot.ConsensusSetHash = cs.consensusSetHash() cs.db.updateBlockMap(&cs.blockRoot) } cs.db.stopConsistencyGuard() return nil }
// Creates a block ready for nonce grinding, also returning the MerkleRoot of // the block. Getting the MerkleRoot of a block requires encoding and hashing // in a specific way, which are implementation details we didn't want to // require external miners to need to worry about. All blocks returned are // unique, which means all miners can safely start at the '0' nonce. func (m *Miner) blockForWork() (types.Block, types.Target) { // Determine the timestamp. blockTimestamp := types.CurrentTimestamp() if blockTimestamp < m.earliestTimestamp { blockTimestamp = m.earliestTimestamp } // Create the miner payouts. subsidy := types.CalculateCoinbase(m.height) for _, txn := range m.transactions { for _, fee := range txn.MinerFees { subsidy = subsidy.Add(fee) } } blockPayouts := []types.SiacoinOutput{types.SiacoinOutput{Value: subsidy, UnlockHash: m.address}} // Create the list of transacitons, including the randomized transaction. // The transactions are assembled by calling append(singleElem, // existingSlic) because doing it the reverse way has some side effects, // creating a race condition and ultimately changing the block hash for // other parts of the program. This is related to the fact that slices are // pointers, and not immutable objects. Use of the builtin `copy` function // when passing objects like blocks around may fix this problem. randBytes := make([]byte, types.SpecifierLen) rand.Read(randBytes) randTxn := types.Transaction{ ArbitraryData: [][]byte{append(modules.PrefixNonSia[:], randBytes...)}, } blockTransactions := append([]types.Transaction{randTxn}, m.transactions...) // Assemble the block b := types.Block{ ParentID: m.parent, Timestamp: blockTimestamp, MinerPayouts: blockPayouts, Transactions: blockTransactions, } return b, m.target }
// checkCurrency verifies that the amount of currency in the system matches the // amount of currency that is supposed to be in the system. func (cst *consensusSetTester) checkCurrency() error { lockID := cst.cs.mu.RLock() defer cst.cs.mu.RUnlock(lockID) // Check that there are 10k siafunds. totalSiafunds := types.NewCurrency64(0) for _, sfo := range cst.cs.siafundOutputs { totalSiafunds = totalSiafunds.Add(sfo.Value) } if totalSiafunds.Cmp(types.NewCurrency64(types.SiafundCount)) != 0 { return errors.New("incorrect number of siafunds in the consensus set") } // Check that there are the expected number of siacoins. expectedSiacoins := types.NewCurrency64(0) for i := types.BlockHeight(0); i <= cst.cs.height(); i++ { expectedSiacoins = expectedSiacoins.Add(types.CalculateCoinbase(i)) } totalSiacoins := cst.cs.siafundPool for _, sco := range cst.cs.siacoinOutputs { totalSiacoins = totalSiacoins.Add(sco.Value) } for _, fc := range cst.cs.fileContracts { totalSiacoins = totalSiacoins.Add(fc.Payout) } for height, dsoMap := range cst.cs.delayedSiacoinOutputs { if height+types.MaturityDelay > cst.cs.Height() { for _, dso := range dsoMap { totalSiacoins = totalSiacoins.Add(dso.Value) } } } if expectedSiacoins.Cmp(totalSiacoins) != 0 { return errors.New("incorrect number of siacoins in the consensus set") } return nil }
// Special handling for the genesis block. No other functions are called on it. func dbAddGenesisBlock(tx *bolt.Tx) { id := types.GenesisID dbAddBlockID(tx, id, 0) txid := types.GenesisBlock.Transactions[0].ID() dbAddTransactionID(tx, txid, 0) for i, sfo := range types.GenesisSiafundAllocation { sfoid := types.GenesisBlock.Transactions[0].SiafundOutputID(uint64(i)) dbAddSiafundOutputID(tx, sfoid, txid) dbAddUnlockHash(tx, sfo.UnlockHash, txid) dbAddSiafundOutput(tx, sfoid, sfo) } dbAddBlockFacts(tx, blockFacts{ BlockFacts: modules.BlockFacts{ BlockID: id, Height: 0, Difficulty: types.RootTarget.Difficulty(), Target: types.RootTarget, TotalCoins: types.CalculateCoinbase(0), TransactionCount: 1, SiafundOutputCount: uint64(len(types.GenesisSiafundAllocation)), }, Timestamp: types.GenesisBlock.Timestamp, }) }
// 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)) } }
// testBlockConflicts adds a transaction to the unconfirmed set, and then adds // a conflicting transaction to the confirmed set, checking that the conflict // is properly handled by the pool. func (tpt *tpoolTester) testBlockConflicts() { // Put two transactions, a parent and a dependent, into the transaction // pool. Then create a transaction that is in conflict with the parent. parent := tpt.emptyUnlockTransaction() dependent := types.Transaction{ SiacoinInputs: []types.SiacoinInput{ types.SiacoinInput{ ParentID: parent.SiacoinOutputID(0), }, }, MinerFees: []types.Currency{ parent.SiacoinOutputs[0].Value, }, } err := tpt.tpool.AcceptTransaction(parent) if err != nil { tpt.t.Fatal(err) } tpt.tpUpdateWait() err = tpt.tpool.AcceptTransaction(dependent) if err != nil { tpt.t.Fatal(err) } tpt.tpUpdateWait() // Create a transaction that is in conflict with the parent. parentValue := parent.SiacoinOutputSum() conflict := types.Transaction{ SiacoinInputs: parent.SiacoinInputs, MinerFees: []types.Currency{ parentValue, }, } // Mine a block to put the conflict into the confirmed set. 'parent' has // dependencies of it's own, and 'conflict' has the same dependencies as // 'parent'. So the block we mine needs to include all of the dependencies // without including 'parent' or 'dependent'. tset := tpt.tpool.TransactionSet() tset = tset[:len(tset)-2] // strip 'parent' and 'dependent' tset = append(tset, conflict) // add 'conflict' target, exists := tpt.cs.ChildTarget(tpt.cs.CurrentBlock().ID()) if !exists { tpt.t.Fatal("unable to recover child target") } block := types.Block{ ParentID: tpt.cs.CurrentBlock().ID(), Timestamp: types.Timestamp(time.Now().Unix()), MinerPayouts: []types.SiacoinOutput{ types.SiacoinOutput{Value: parentValue.Add(types.CalculateCoinbase(tpt.cs.Height() + 1))}, }, Transactions: tset, } for { block, found := tpt.miner.SolveBlock(block, target) if found { err = tpt.cs.AcceptBlock(block) if err != nil { tpt.t.Fatal(err) } break } } tpt.csUpdateWait() // Check that 'parent' and 'dependent' have been removed from the // transaction set, since conflict has made the confirmed set. if len(tpt.tpool.TransactionSet()) != 0 { tpt.t.Error("parent and dependent transaction are still in the pool after a conflict has been introduced, have", len(tset)) } }
// testRewinding adds transactions in a block, then removes the block and // verifies that the transaction pool adds the block transactions. func (tpt *tpoolTester) testRewinding() { // Put some transactions into the unconfirmed set. tpt.addSiacoinTransactionToPool() if len(tpt.tpool.TransactionSet()) == 0 { tpt.t.Fatal("transaction pool has no transactions") } // Prepare an empty block to cause a rewind (by forking). target, exists := tpt.cs.ChildTarget(tpt.cs.CurrentBlock().ID()) if !exists { tpt.t.Fatal("unable to recover child target") } forkStart := types.Block{ ParentID: tpt.cs.CurrentBlock().ID(), Timestamp: types.Timestamp(time.Now().Unix()), MinerPayouts: []types.SiacoinOutput{ types.SiacoinOutput{Value: types.CalculateCoinbase(tpt.cs.Height() + 1)}, }, } for { var found bool forkStart, found = tpt.miner.SolveBlock(forkStart, target) if found { break } } // Mine a block with the transaction. b, _ := tpt.miner.FindBlock() err := tpt.cs.AcceptBlock(b) if err != nil { tpt.t.Fatal(err) } tpt.csUpdateWait() if len(tpt.tpool.TransactionSet()) != 0 { tpt.t.Fatal("tset should be empty after FindBlock()") } // Fork around the block with the transaction. err = tpt.cs.AcceptBlock(forkStart) if err != nil && err != modules.ErrNonExtendingBlock { tpt.t.Fatal(err) } target, exists = tpt.cs.ChildTarget(tpt.cs.CurrentBlock().ID()) if !exists { tpt.t.Fatal("unable to recover child target") } forkCommit := types.Block{ ParentID: forkStart.ID(), Timestamp: types.Timestamp(time.Now().Unix()), MinerPayouts: []types.SiacoinOutput{ types.SiacoinOutput{Value: types.CalculateCoinbase(tpt.cs.Height() + 1)}, }, } for { var found bool forkCommit, found = tpt.miner.SolveBlock(forkCommit, target) if found { tpt.cs.AcceptBlock(forkCommit) break } } tpt.csUpdateWait() // Check that the transaction which was once confirmed but no longer is // confirmed is now unconfirmed. if len(tpt.tpool.TransactionSet()) == 0 { tpt.t.Error("tset should contain transactions that used to be confirmed but no longer are") } }
// New returns a new State, containing at least the genesis block. If there is // an existing block database present in saveDir, it will be loaded. Otherwise, // a new database will be created. func New(gateway modules.Gateway, saveDir string) (*State, error) { if gateway == nil { return nil, ErrNilGateway } // Create the State object. cs := &State{ blockMap: make(map[types.BlockID]*blockNode), dosBlocks: make(map[types.BlockID]struct{}), currentPath: make([]types.BlockID, 1), siacoinOutputs: make(map[types.SiacoinOutputID]types.SiacoinOutput), fileContracts: make(map[types.FileContractID]types.FileContract), siafundOutputs: make(map[types.SiafundOutputID]types.SiafundOutput), delayedSiacoinOutputs: make(map[types.BlockHeight]map[types.SiacoinOutputID]types.SiacoinOutput), gateway: gateway, mu: sync.New(modules.SafeMutexDelay, 1), } // Create the genesis block and add it as the BlockRoot. genesisBlock := types.Block{ Timestamp: types.GenesisTimestamp, Transactions: []types.Transaction{ {SiafundOutputs: types.GenesisSiafundAllocation}, }, } cs.blockRoot = &blockNode{ block: genesisBlock, childTarget: types.RootTarget, depth: types.RootDepth, diffsGenerated: true, } cs.blockMap[genesisBlock.ID()] = cs.blockRoot // Fill out the consensus information for the genesis block. cs.currentPath[0] = genesisBlock.ID() cs.siacoinOutputs[genesisBlock.MinerPayoutID(0)] = types.SiacoinOutput{ Value: types.CalculateCoinbase(0), UnlockHash: types.ZeroUnlockHash, } // Allocate the Siafund addresses by putting them all in a big transaction // and applying the diffs. for i, siafundOutput := range genesisBlock.Transactions[0].SiafundOutputs { sfid := genesisBlock.Transactions[0].SiafundOutputID(i) sfod := modules.SiafundOutputDiff{ Direction: modules.DiffApply, ID: sfid, SiafundOutput: siafundOutput, } cs.commitSiafundOutputDiff(sfod, modules.DiffApply) cs.blockRoot.siafundOutputDiffs = append(cs.blockRoot.siafundOutputDiffs, sfod) } // Send out genesis block update. cs.updateSubscribers(nil, []*blockNode{cs.blockRoot}) // Create the consensus directory. err := os.MkdirAll(saveDir, 0700) if err != nil { return nil, err } // During short tests, use an in-memory database. if build.Release == "testing" && testing.Short() { cs.db = persist.NilDB } else { // Otherwise, try to load an existing database from disk. err = cs.load(saveDir) if err != nil { return nil, err } } // Register RPCs gateway.RegisterRPC("SendBlocks", cs.sendBlocks) gateway.RegisterRPC("RelayBlock", cs.RelayBlock) gateway.RegisterConnectCall("SendBlocks", cs.receiveBlocks) // Spawn resynchronize loop. go cs.threadedResynchronize() return cs, nil }
func BenchmarkAcceptBigTxBlocks(b *testing.B) { b.ReportAllocs() numSigs := 7 cst, err := createConsensusSetTester("BenchmarkEmptyBlocksA") if err != nil { b.Fatal(err) } defer cst.closeCst() // Mine until the wallet has 100 utxos for cst.cs.height() < (types.BlockHeight(numSigs) + types.MaturityDelay) { _, err := cst.miner.AddBlock() if err != nil { b.Fatal(err) } } // Create an alternate testing consensus set, which does not // have any subscribers testdir := build.TempDir(modules.ConsensusDir, "BenchmarkEmptyBlocksB") g, err := gateway.New(":0", filepath.Join(testdir, modules.GatewayDir)) if err != nil { b.Fatal(err) } cs, err := New(g, filepath.Join(testdir, modules.ConsensusDir)) if err != nil { b.Fatal("Error creating consensus: " + err.Error()) } defer cs.Close() h := cst.cs.db.pathHeight() for i := types.BlockHeight(1); i < h; i++ { err = cs.AcceptBlock(cst.cs.db.getBlockMap(cst.cs.db.getPath(i)).Block) if err != nil { b.Fatal(err) } } // construct a transaction using numSigs utxo's, and signed numSigs times outputValues := make([]types.Currency, numSigs) txValue := types.ZeroCurrency for i := 1; i <= numSigs; i++ { outputValues[i-1] = types.CalculateCoinbase(types.BlockHeight(i)) txValue = txValue.Add(outputValues[i-1]) } b.ResetTimer() b.StopTimer() for j := 0; j < b.N; j++ { txnBuilder := cst.wallet.StartTransaction() err = txnBuilder.FundSiacoins(txValue) if err != nil { b.Fatal(err) } for i := 0; i < numSigs; i++ { unlockConditions, err := cst.wallet.NextAddress() if err != nil { b.Fatal(err) } txnBuilder.AddSiacoinOutput(types.SiacoinOutput{Value: outputValues[i], UnlockHash: unlockConditions.UnlockHash()}) } txnSet, err := txnBuilder.Sign(true) if err != nil { b.Fatal(err) } outputVolume := types.ZeroCurrency for _, out := range txnSet[0].SiacoinOutputs { outputVolume = outputVolume.Add(out.Value) } blk := types.Block{ ParentID: cst.cs.CurrentBlock().ID(), Timestamp: types.CurrentTimestamp(), MinerPayouts: []types.SiacoinOutput{ {Value: types.CalculateCoinbase(cst.cs.height())}, }, Transactions: txnSet, } target, _ := cst.cs.ChildTarget(cst.cs.CurrentBlock().ID()) block, _ := cst.miner.SolveBlock(blk, target) // Submit it to the first consensus set for validity err = cst.cs.AcceptBlock(block) if err != nil { b.Fatal(err) } b.StartTimer() // Time the consensus set without subscribers err = cs.AcceptBlock(block) if err != nil { b.Fatal(err) } b.StopTimer() } }
// TestIntegrationWalletGETSiacoins probes the GET call to /wallet when the // siacoin balance is being manipulated. func TestIntegrationWalletGETSiacoins(t *testing.T) { if testing.Short() { t.SkipNow() } st, err := createServerTester("TestIntegrationWalletGETSiacoins") if err != nil { t.Fatal(err) } // Check the intial wallet is encrypted, unlocked, and has the siacoins // that got mined. var wg WalletGET err = st.getAPI("/wallet", &wg) if err != nil { t.Fatal(err) } if !wg.Encrypted { t.Error("Wallet has been encrypted") } if !wg.Unlocked { t.Error("Wallet has been unlocked") } if wg.ConfirmedSiacoinBalance.Cmp(types.CalculateCoinbase(1)) != 0 { t.Error("reported wallet balance does not reflect the single block that has been mined") } if wg.UnconfirmedOutgoingSiacoins.Cmp(types.NewCurrency64(0)) != 0 { t.Error("there should not be unconfirmed outgoing siacoins") } if wg.UnconfirmedIncomingSiacoins.Cmp(types.NewCurrency64(0)) != 0 { t.Error("there should not be unconfirmed incoming siacoins") } // Send coins to a wallet address through the api. var wag WalletAddressGET err = st.getAPI("/wallet/address", &wag) if err != nil { t.Fatal(err) } sendSiacoinsValues := url.Values{} sendSiacoinsValues.Set("amount", "1234") sendSiacoinsValues.Add("destination", wag.Address.String()) err = st.stdPostAPI("/wallet/siacoins", sendSiacoinsValues) if err != nil { t.Fatal(err) } // Check that the wallet is reporting unconfirmed siacoins. err = st.getAPI("/wallet", &wg) if err != nil { t.Fatal(err) } if !wg.Encrypted { t.Error("Wallet has been encrypted") } if !wg.Unlocked { t.Error("Wallet has been unlocked") } if wg.ConfirmedSiacoinBalance.Cmp(types.CalculateCoinbase(1)) != 0 { t.Error("reported wallet balance does not reflect the single block that has been mined") } if wg.UnconfirmedOutgoingSiacoins.Cmp(types.NewCurrency64(0)) <= 0 { t.Error("there should be unconfirmed outgoing siacoins") } if wg.UnconfirmedIncomingSiacoins.Cmp(types.NewCurrency64(0)) <= 0 { t.Error("there should be unconfirmed incoming siacoins") } if wg.UnconfirmedOutgoingSiacoins.Cmp(wg.UnconfirmedIncomingSiacoins) <= 0 { t.Error("net movement of siacoins should be outgoing (miner fees)") } // Mine a block and see that the unconfirmed balances reduce back to // nothing. _, err = st.miner.AddBlock() if err != nil { t.Fatal(err) } err = st.getAPI("/wallet", &wg) if err != nil { t.Fatal(err) } if wg.ConfirmedSiacoinBalance.Cmp(types.CalculateCoinbase(1).Add(types.CalculateCoinbase(2))) >= 0 { t.Error("reported wallet balance does not reflect mining two blocks and eating a miner fee") } if wg.UnconfirmedOutgoingSiacoins.Cmp(types.NewCurrency64(0)) != 0 { t.Error("there should not be unconfirmed outgoing siacoins") } if wg.UnconfirmedIncomingSiacoins.Cmp(types.NewCurrency64(0)) != 0 { t.Error("there should not be unconfirmed incoming siacoins") } }
// postEncryptionTesting runs a series of checks on the wallet after it has // been encrypted, to make sure that locking, unlocking, and spending after // unlocking are all happening in the correct order and returning the correct // errors. func postEncryptionTesting(m modules.Miner, w *Wallet, masterKey crypto.TwofishKey) { if !w.Encrypted() { panic("wallet is not encrypted when starting postEncryptionTesting") } if w.Unlocked() { panic("wallet is unlocked when starting postEncryptionTesting") } if len(w.seeds) != 0 { panic("wallet has seeds in it when startin postEncryptionTesting") } // Try unlocking and using the wallet. err := w.Unlock(masterKey) if err != nil { panic(err) } err = w.Unlock(masterKey) if err != errAlreadyUnlocked { panic(err) } // Mine enough coins so that a balance appears (and some buffer for the // send later). for i := types.BlockHeight(0); i <= types.MaturityDelay+1; i++ { _, err := m.AddBlock() if err != nil { panic(err) } } siacoinBal, _, _ := w.ConfirmedBalance() if siacoinBal.Cmp(types.NewCurrency64(0)) <= 0 { panic("wallet balance reported as 0 after maturing some mined blocks") } err = w.Unlock(masterKey) if err != errAlreadyUnlocked { panic(err) } // Lock, unlock, and trying using the wallet some more. err = w.Lock() if err != nil { panic(err) } err = w.Lock() if err != modules.ErrLockedWallet { panic(err) } err = w.Unlock(crypto.TwofishKey{}) if err != modules.ErrBadEncryptionKey { panic(err) } err = w.Unlock(masterKey) if err != nil { panic(err) } // Verify that the secret keys have been restored by sending coins to the // void. Send more coins than are received by mining a block. _, err = w.SendSiacoins(types.CalculateCoinbase(0), types.UnlockHash{}) if err != nil { panic(err) } _, err = m.AddBlock() if err != nil { panic(err) } siacoinBal2, _, _ := w.ConfirmedBalance() if siacoinBal2.Cmp(siacoinBal) >= 0 { panic("balance did not increase") } }
// 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")) } }