Commit 9e7687fd authored by David Vorick's avatar David Vorick

Change the transaction pool update model

parent a18206ed
*.swp
cover
release
doc/whitepaper.aux
doc/whitepaper.log
doc/whitepaper.pdf
*.swp
package api
import (
"fmt"
"strings"
"github.com/stretchr/graceful"
"github.com/NebulousLabs/Sia/modules"
......
This diff is collapsed.
......@@ -7,10 +7,7 @@ import (
"github.com/NebulousLabs/Sia/types"
)
// ReceiveTransactionPoolUpdate listens to the transaction pool for changes in
// the transaction pool. These changes will be applied to the blocks being
// mined.
func (m *Miner) ReceiveTransactionPoolUpdate(cc modules.ConsensusChange, unconfirmedTransactions []types.Transaction, _ []modules.SiacoinOutputDiff) {
func (m *Miner) ReceiveConsensusSetUpdate(cc modules.ConsensusChange) {
lockID := m.mu.Lock()
defer m.mu.Unlock(lockID)
defer m.notifySubscribers()
......@@ -18,34 +15,10 @@ func (m *Miner) ReceiveTransactionPoolUpdate(cc modules.ConsensusChange, unconfi
m.height -= types.BlockHeight(len(cc.RevertedBlocks))
m.height += types.BlockHeight(len(cc.AppliedBlocks))
// The total encoded size of the transactions cannot exceed the block size.
m.transactions = nil
remainingSize := int(types.BlockSizeLimit - 5e3)
for {
if len(unconfirmedTransactions) == 0 {
break
}
remainingSize -= len(encoding.Marshal(unconfirmedTransactions[0]))
if remainingSize < 0 {
break
}
m.transactions = append(m.transactions, unconfirmedTransactions[0])
unconfirmedTransactions = unconfirmedTransactions[1:]
}
// If no blocks have been applied, the block variables do not need to be
// updated.
if len(cc.AppliedBlocks) == 0 {
if build.DEBUG {
if len(cc.RevertedBlocks) != 0 {
panic("blocks reverted without being added")
}
}
return
}
// Update the parent, target, and earliest timestamp fields for the miner.
m.parent = cc.AppliedBlocks[len(cc.AppliedBlocks)-1].ID()
target, exists1 := m.cs.ChildTarget(m.parent)
timestamp, exists2 := m.cs.EarliestChildTimestamp(m.parent)
......@@ -60,3 +33,24 @@ func (m *Miner) ReceiveTransactionPoolUpdate(cc modules.ConsensusChange, unconfi
m.target = target
m.earliestTimestamp = timestamp
}
func (m *Miner) ReceiveUpdatedUnconfirmedTransactions(unconfirmedTransactions []types.Transaction, _ modules.ConsensusChange) {
lockID := m.mu.Lock()
defer m.mu.Unlock(lockID)
defer m.notifySubscribers()
m.transactions = nil
remainingSize := int(types.BlockSizeLimit - 5e3)
for {
if len(unconfirmedTransactions) == 0 {
break
}
remainingSize -= len(encoding.Marshal(unconfirmedTransactions[0]))
if remainingSize < 0 {
break
}
m.transactions = append(m.transactions, unconfirmedTransactions[0])
unconfirmedTransactions = unconfirmedTransactions[1:]
}
}
......@@ -29,8 +29,9 @@ type TransactionPoolSubscriber interface {
ConsensusSetSubscriber
// ReceiveTransactionPoolUpdate notifies subscribers of a change to the
// consensus set and/or unconfirmed set.
ReceiveUpdatedUnconfirmedTransactions([]types.Transaction)
// consensus set and/or unconfirmed set, and includes the consensus change
// that would result if all of the transactions made it into a block.
ReceiveUpdatedUnconfirmedTransactions([]types.Transaction, ConsensusChange)
}
type TransactionPool interface {
......
......@@ -114,20 +114,16 @@ func (tp *TransactionPool) acceptTransactionSet(ts []types.Transaction) error {
// Add the transaction set to the pool.
setID := TransactionSetID(crypto.HashObject(ts))
tp.transactionSets[setID] = ts
var oids []ObjectID
for _, diff := range cc.SiacoinOutputDiffs {
tp.knownObjects[ObjectID(diff.ID)] = setID
oids = append(oids, ObjectID(diff.ID))
}
for _, diff := range cc.FileContractDiffs {
tp.knownObjects[ObjectID(diff.ID)] = setID
oids = append(oids, ObjectID(diff.ID))
}
for _, diff := range cc.SiafundOutputDiffs {
tp.knownObjects[ObjectID(diff.ID)] = setID
oids = append(oids, ObjectID(diff.ID))
}
tp.transactionSetDiffs[setID] = oids
tp.transactionSetDiffs[setID] = cc
tp.databaseSize += len(encoding.Marshal(ts))
return nil
}
......@@ -165,3 +161,13 @@ func (tp *TransactionPool) RelayTransactionSet(conn modules.PeerConn) error {
}
return err
}
// DEPRECATED
func (tp *TransactionPool) AcceptTransaction(t types.Transaction) error {
return tp.AcceptTransactionSet([]types.Transaction{t})
}
// DEPRECATED
func (tp *TransactionPool) RelayTransaction(conn modules.PeerConn) error {
return tp.RelayTransactionSet(conn)
}
package transactionpool
import (
"crypto/rand"
"testing"
"github.com/NebulousLabs/Sia/encoding"
"github.com/NebulousLabs/Sia/modules"
"github.com/NebulousLabs/Sia/types"
)
// addSiacoinTransactionToPool creates a transaction with siacoin outputs and
// adds them to the pool, returning the transaction.
func (tpt *tpoolTester) addSiacoinTransactionToPool() (txn types.Transaction) {
// sendCoins will automatically add transaction(s) to the transaction pool.
// They will contain siacoin output(s).
txn, err := tpt.sendCoins(types.NewCurrency64(1), types.ZeroUnlockHash)
if err != nil {
tpt.t.Fatal(err)
}
return
}
// addDependentSiacoinTransactionToPool adds a transaction to the pool with a
// siacoin output, and then adds a second transaction to the pool that requires
// the unconfirmed siacoin output.
func (tpt *tpoolTester) addDependentSiacoinTransactionToPool() (firstTxn, dependentTxn types.Transaction) {
// Get an address to receive coins.
addr, _, err := tpt.wallet.CoinAddress(false) // false means hide the address from the user; doesn't matter for test.
if err != nil {
tpt.t.Fatal(err)
}
// sendCoins will automatically add transaction(s) to the transaction
// pool. They will contain siacoin output(s). We send all of our coins to
// ourself to guarantee that the next transaction will depend on an
// existing unconfirmed transaction.
balance := tpt.wallet.Balance(false)
firstTxn, err = tpt.sendCoins(balance, addr)
if err != nil {
tpt.t.Fatal(err)
}
// Send the full balance to ourselves again. The second transaction will
// necesarily require the first transaction as a dependency, since we're
// sending all of the coins again.
dependentTxn, err = tpt.sendCoins(balance, addr)
if err != nil {
tpt.t.Fatal(err)
}
return
}
// TestAddSiacoinTransactionToPool creates a tpoolTester and uses it to call
// addSiacoinTransactionToPool.
func TestAddSiacoinTransactionToPool(t *testing.T) {
tpt := newTpoolTester("TestAddSiacoinTransactionToPool", t)
tpt.addSiacoinTransactionToPool()
}
// TestAddDependentSiacoinTransactionToPool creates a tpoolTester and uses it
// to cal addDependentSiacoinTransactionToPool.
func TestAddDependentSiacoinTransactionToPool(t *testing.T) {
tpt := newTpoolTester("TestAddDependentSiacoinTransactionToPool", t)
tpt.addDependentSiacoinTransactionToPool()
}
// TestDuplicateTransaction checks that a duplicate transaction error is
// triggered when duplicate transactions are added to the transaction pool.
// This test won't be needed after the duplication prevention mechanism is
// removed, and that will be removed after fees are required in all
// transactions submitted to the pool.
func TestDuplicateTransaction(t *testing.T) {
tpt := newTpoolTester("TestDuplicateTransaction", t)
txn := tpt.addSiacoinTransactionToPool()
err := tpt.tpool.AcceptTransaction(txn)
if err != modules.ErrTransactionPoolDuplicate {
t.Fatal("expecting ErrTransactionPoolDuplicate got:", err)
}
}
/* TODO: Implement wallet fee adding to test ErrLargeTransactionPool
func TestLargePoolTransaction(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
tpt := newTpoolTester("TestLargePoolTransaction", t)
// Needed: for loop to add Transactions until transactionPoolSize = 60 MB?
txn := tpt.addSiacoinTransactionToPool()
// Test the transaction that should be rejected at >60 MB
err := tpt.tpool.AcceptTransaction(txn)
if err != ErrLargeTransactionPool {
t.Fatal("expecting ErrLargeTransactionPool got:", err)
}
}
*/
func TestLowFeeTransaction(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
// Initialize variables to populate transaction pool
tpt := newTpoolTester("TestLowFeeTransaction", t)
emptyData := make([]byte, 15e3-16)
randData := make([]byte, 16) // not yet random
emptyTxn := types.Transaction{
ArbitraryData: [][]byte{append(modules.PrefixNonSia[:], (append(emptyData, randData...))...)},
}
transSize := len(encoding.Marshal(emptyTxn))
// Fill it to 20 MB
for i := 0; i <= (TransactionPoolSizeForFee / transSize); i++ {
// Make a unique transaction to accept
rand.Read(randData)
uniqueTxn := types.Transaction{
ArbitraryData: [][]byte{append(modules.PrefixNonSia[:], append(emptyData, randData...)...)},
}
// Accept said transaction
err := tpt.tpool.AcceptTransaction(uniqueTxn)
if err != nil {
t.Error(err)
}
}
// Should be the straw to break the camel's back (i.e. the transaction at >20 MB)
err := tpt.tpool.AcceptTransaction(emptyTxn)
if err != ErrLowMinerFees {
t.Fatal("expecting ErrLowMinerFees got:", err)
}
}
package transactionpool
import (
"crypto/rand"
"testing"
"github.com/NebulousLabs/Sia/modules"
"github.com/NebulousLabs/Sia/types"
)
// Try to add a transaction that is too large to the transaction pool.
func TestLargeTransaction(t *testing.T) {
tpt := newTpoolTester("TestLargeTransaction", t)
// Create a transaction that's larger than the size limit.
largeArbitraryData := make([]byte, modules.TransactionSizeLimit)
rand.Read(largeArbitraryData)
acceptableData := append(modules.PrefixNonSia[:], largeArbitraryData...)
txn := types.Transaction{
ArbitraryData: [][]byte{acceptableData},
}
// Check IsStandard.
err := tpt.tpool.IsStandardTransaction(txn)
if err != ErrLargeTransaction {
t.Error("expecting errLargeTransaction, got:", err)
}
// Check that transaction is rejected when calling 'accept'.
err = tpt.tpool.AcceptTransaction(txn)
if err != ErrLargeTransaction {
t.Error("expecting errLargeTransaction, got:", err)
}
if len(tpt.tpool.TransactionSet()) != 0 {
t.Error("tpool is not empty after accepting a bad transaction")
}
}
......@@ -14,11 +14,15 @@ func (tp *TransactionPool) sendNotification() {
func (tp *TransactionPool) updateSubscribersTransactions() {
var txns []types.Transaction
var cc modules.ConsensusChange
for _, tSet := range tp.transactionSets {
txns = append(txns, tSet...)
}
for _, tSetDiff := range tp.transactionSetDiffs {
cc = cc.Append(tSetDiff)
}
for _, subscriber := range tp.subscribers {
subscriber.ReceiveUpdatedUnconfirmedTransactions(txns)
subscriber.ReceiveUpdatedUnconfirmedTransactions(txns, cc)
}
tp.sendNotification()
}
......@@ -30,9 +34,6 @@ func (tp *TransactionPool) updateSubscribersConsensus(cc modules.ConsensusChange
tp.sendNotification()
}
// TransactionPoolNotify adds a subscriber to the list who will be notified any
// time that there is a change to the transaction pool (new transaction or
// block), but that subscriber will not be told any details about the change.
func (tp *TransactionPool) TransactionPoolNotify() <-chan struct{} {
c := make(chan struct{}, modules.NotifyBuffer)
id := tp.mu.Lock()
......@@ -42,9 +43,6 @@ func (tp *TransactionPool) TransactionPoolNotify() <-chan struct{} {
return c
}
// TransactionPoolSubscribe adds a subscriber to the transaction pool that will
// be given a full list of changes via ReceiveTransactionPoolUpdate any time
// that there is a change.
func (tp *TransactionPool) TransactionPoolSubscribe(subscriber modules.TransactionPoolSubscriber) {
id := tp.mu.Lock()
tp.subscribers = append(tp.subscribers, subscriber)
......
......@@ -9,40 +9,42 @@ import (
"github.com/NebulousLabs/Sia/types"
)
type ObjectID crypto.Hash
type TransactionSetID crypto.Hash
type (
ObjectID crypto.Hash
TransactionSetID crypto.Hash
type TransactionPool struct {
// Depedencies of the transaction pool. The state height is needed
// separately from the state because the transaction pool may not be
// synchronized to the state.
consensusSet modules.ConsensusSet
gateway modules.Gateway
TransactionPool struct {
// Depedencies of the transaction pool. The state height is needed
// separately from the state because the transaction pool may not be
// synchronized to the state.
consensusSet modules.ConsensusSet
gateway modules.Gateway
// unconfirmedIDs is a set of hashes representing the ID of an object in
// the unconfirmed set of transactions. Each unconfirmed ID points to the
// transaciton set containing that object. Transaction sets are sets of
// transactions that get id'd by their hash. transacitonSetDiffs contain
// the set of IDs that each transaction set is associated with.
knownObjects map[ObjectID]TransactionSetID
transactionSets map[TransactionSetID][]types.Transaction
transactionSetDiffs map[TransactionSetID][]ObjectID
databaseSize int
// TODO: Write a consistency check comparing transactionSets,
// transactionSetDiffs.
//
// TODO: Write a consistency check making sure that all unconfirmedIDs
// point to the right place, and that all UnconfirmedIDs are accounted for.
//
// TODO: Need some sort of first-come-first-serve memory.
// unconfirmedIDs is a set of hashes representing the ID of an object in
// the unconfirmed set of transactions. Each unconfirmed ID points to the
// transaciton set containing that object. Transaction sets are sets of
// transactions that get id'd by their hash. transacitonSetDiffs contain
// the set of IDs that each transaction set is associated with.
knownObjects map[ObjectID]TransactionSetID
transactionSets map[TransactionSetID][]types.Transaction
transactionSetDiffs map[TransactionSetID]modules.ConsensusChange
databaseSize int
// TODO: Write a consistency check comparing transactionSets,
// transactionSetDiffs.
//
// TODO: Write a consistency check making sure that all unconfirmedIDs
// point to the right place, and that all UnconfirmedIDs are accounted for.
//
// TODO: Need some sort of first-come-first-serve memory.
// TODO: docstring
consensusChangeIndex int
subscribers []modules.TransactionPoolSubscriber
notifySubscribers []chan struct{}
// TODO: docstring
consensusChangeIndex int
subscribers []modules.TransactionPoolSubscriber
notifySubscribers []chan struct{}
mu *sync.RWMutex
}
mu *sync.RWMutex
}
)
// New creates a transaction pool that is ready to receive transactions.
func New(cs modules.ConsensusSet, g modules.Gateway) (tp *TransactionPool, err error) {
......@@ -63,7 +65,7 @@ func New(cs modules.ConsensusSet, g modules.Gateway) (tp *TransactionPool, err e
knownObjects: make(map[ObjectID]TransactionSetID),
transactionSets: make(map[TransactionSetID][]types.Transaction),
transactionSetDiffs: make(map[TransactionSetID][]ObjectID),
transactionSetDiffs: make(map[TransactionSetID]modules.ConsensusChange),
mu: sync.New(modules.SafeMutexDelay, 1),
}
......@@ -76,3 +78,11 @@ func New(cs modules.ConsensusSet, g modules.Gateway) (tp *TransactionPool, err e
return
}
func (tp *TransactionPool) TransactionSet() []types.Transaction {
var txns []types.Transaction
for _, tSet := range tp.transactionSets {
txns = append(txns, tSet...)
}
return txns
}
......@@ -9,7 +9,7 @@ import (
func (tp *TransactionPool) purge() {
tp.knownObjects = make(map[ObjectID]TransactionSetID)
tp.transactionSets = make(map[TransactionSetID][]types.Transaction)
tp.transactionSetDiffs = make(map[TransactionSetID][]ObjectID)
tp.transactionSetDiffs = make(map[TransactionSetID]modules.ConsensusChange)
tp.databaseSize = 0
}
......
package transactionpool
import (
"testing"
"time"
"github.com/NebulousLabs/Sia/modules"
"github.com/NebulousLabs/Sia/types"
)
// testUpdateTransactionRemoval checks that when a transaction moves from the
// unconfirmed set into the confirmed set, the transaction gets correctly
// removed from the unconfirmed set.
func (tpt *tpoolTester) testUpdateTransactionRemoval() {
// Create some unconfirmed transactions.
tpt.addDependentSiacoinTransactionToPool()
if len(tpt.tpool.TransactionSet()) == 0 {
tpt.t.Error("tset should have some transacitons")
}
// Mine a block to put the transactions into the confirmed set.
b, _ := tpt.miner.FindBlock()
err := tpt.cs.AcceptBlock(b)
if err != nil {
tpt.t.Fatal(err)
}
tpt.csUpdateWait()
// Check that the transactions have been removed from the unconfirmed set.
if len(tpt.tpool.TransactionSet()) != 0 {
tpt.t.Error("unconfirmed transaction set is not empty, len", len(tpt.tpool.TransactionSet()))
}
}
// testDataTransactions checks transactions that are data-only, and makes sure
// that the data is put into the blockchain only a single time.
func (tpt *tpoolTester) testDataTransactions() {
// Make a data transaction and put it into the blockchain.
txn := types.Transaction{
ArbitraryData: [][]byte{append(modules.PrefixNonSia[:], 'd')},
}
err := tpt.tpool.AcceptTransaction(txn)
if err != nil {
tpt.t.Fatal(err)
}
tpt.tpUpdateWait()
b, _ := tpt.miner.FindBlock()
err = tpt.cs.AcceptBlock(b)
if err != nil {
tpt.t.Fatal(err)
}
if len(b.Transactions) != 2 {
tpt.t.Error(len(b.Transactions))
tpt.t.Fatal("only expecting 2 transactions in the test block")
}
tpt.csUpdateWait()
// Mine a second block, this block should not have the data transaction.
b, _ = tpt.miner.FindBlock()
err = tpt.cs.AcceptBlock(b)
if err != nil {
tpt.t.Fatal(err)
}
if len(b.Transactions) != 1 {
tpt.t.Fatal("Block should contain only the uniqueness transaction after mining a data transaction")
}
tpt.csUpdateWait()
}
// 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))
}
}
// 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))
}
}
// 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(),