From 15bace88dc95887139d877aca092f9c41b5d63fe Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Mon, 22 Aug 2016 22:53:22 -0500 Subject: [PATCH] mempool: Add basic test harness infrastructure. This adds a basic test harness infrastructure for the mempool package which aims to make writing tests for it much easier. The harness provides functionality for creating and signing transactions as well as a fake chain that provides utxos for use in generating valid transactions and allows an arbitrary chain height to be set. In order to simplify transaction creation, a single signing key and payment address is used throughout and a convenience type for spendable outputs is provided. The harness is initialized with a spendable coinbase output by default and the fake chain height set to the maturity height needed to ensure the provided output is in fact spendable as well as a policy that is suitable for testing. Since tests are in the same package and each harness provides a unique pool and fake chain instance, the tests can safely reach into the pool policy, or any other state, and change it for a given harness without affecting the others. In order to be able to make use of the existing blockchain.Viewpoint type, a Clone method has been to the UtxoEntry type which allows the fake chain instance to keep a single view with the actual available unspent utxos while the mempool ends up fetching a subset of the view with the specifically requested entries cloned. To demo the harness, this also contains a couple of tests which make use of it: - TestSimpleOrphanChain -- Ensures an entire chain of orphans is properly accepted and connects up when the missing parent transaction is added - TestOrphanRejects -- Ensure orphans are actually rejected when the flag on ProcessTransactions is set to reject them --- blockchain/utxoviewpoint.go | 23 ++ mempool/mempool_test.go | 463 ++++++++++++++++++++++++++++++++++++ 2 files changed, 486 insertions(+) create mode 100644 mempool/mempool_test.go diff --git a/blockchain/utxoviewpoint.go b/blockchain/utxoviewpoint.go index d6cff9cf..e16ffbdd 100644 --- a/blockchain/utxoviewpoint.go +++ b/blockchain/utxoviewpoint.go @@ -153,6 +153,29 @@ func (entry *UtxoEntry) PkScriptByIndex(outputIndex uint32) []byte { return output.pkScript } +// Clone returns a deep copy of the utxo entry. +func (entry *UtxoEntry) Clone() *UtxoEntry { + if entry == nil { + return nil + } + + newEntry := &UtxoEntry{ + version: entry.version, + isCoinBase: entry.isCoinBase, + blockHeight: entry.blockHeight, + sparseOutputs: make(map[uint32]*utxoOutput), + } + for outputIndex, output := range entry.sparseOutputs { + newEntry.sparseOutputs[outputIndex] = &utxoOutput{ + spent: output.spent, + compressed: output.compressed, + amount: output.amount, + pkScript: output.pkScript, + } + } + return newEntry +} + // newUtxoEntry returns a new unspent transaction output entry with the provided // coinbase flag and block height ready to have unspent outputs added. func newUtxoEntry(version int32, isCoinBase bool, blockHeight int32) *UtxoEntry { diff --git a/mempool/mempool_test.go b/mempool/mempool_test.go new file mode 100644 index 00000000..8d38e826 --- /dev/null +++ b/mempool/mempool_test.go @@ -0,0 +1,463 @@ +// Copyright (c) 2016 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mempool + +import ( + "encoding/hex" + "reflect" + "sync" + "testing" + + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" +) + +// fakeChain is used by the pool harness to provide generated test utxos and +// a current faked chain height to the pool callbacks. This, in turn, allows +// transations to be appear as though they are spending completely valid utxos. +type fakeChain struct { + sync.RWMutex + utxos *blockchain.UtxoViewpoint + currentHeight int32 +} + +// FetchUtxoView loads utxo details about the input transactions referenced by +// the passed transaction from the point of view of the fake chain. +// It also attempts to fetch the utxo details for the transaction itself so the +// returned view can be examined for duplicate unspent transaction outputs. +// +// This function is safe for concurrent access however the returned view is NOT. +func (s *fakeChain) FetchUtxoView(tx *btcutil.Tx) (*blockchain.UtxoViewpoint, error) { + s.RLock() + defer s.RUnlock() + + // All entries are cloned to ensure modifications to the returned view + // do not affect the fake chain's view. + + // Add an entry for the tx itself to the new view. + viewpoint := blockchain.NewUtxoViewpoint() + entry := s.utxos.LookupEntry(tx.Hash()) + viewpoint.Entries()[*tx.Hash()] = entry.Clone() + + // Add entries for all of the inputs to the tx to the new view. + for _, txIn := range tx.MsgTx().TxIn { + originHash := &txIn.PreviousOutPoint.Hash + entry := s.utxos.LookupEntry(originHash) + viewpoint.Entries()[*originHash] = entry.Clone() + } + + return viewpoint, nil +} + +// BestHeight returns the current height associated with the fake chain +// instance. +func (s *fakeChain) BestHeight() int32 { + s.RLock() + height := s.currentHeight + s.RUnlock() + return height +} + +// SetHeight sets the current height associated with the fake chain instance. +func (s *fakeChain) SetHeight(height int32) { + s.Lock() + s.currentHeight = height + s.Unlock() +} + +// spendableOutput is a convenience type that houses a particular utxo and the +// amount associated with it. +type spendableOutput struct { + outPoint wire.OutPoint + amount btcutil.Amount +} + +// txOutToSpendableOut returns a spendable output given a transaction and index +// of the output to use. This is useful as a convenience when creating test +// transactions. +func txOutToSpendableOut(tx *btcutil.Tx, outputNum uint32) spendableOutput { + return spendableOutput{ + outPoint: wire.OutPoint{Hash: *tx.Hash(), Index: outputNum}, + amount: btcutil.Amount(tx.MsgTx().TxOut[outputNum].Value), + } +} + +// poolHarness provides a harness that includes functionality for creating and +// signing transactions as well as a fake chain that provides utxos for use in +// generating valid transactions. +type poolHarness struct { + // signKey is the signing key used for creating transactions throughout + // the tests. + // + // payAddr is the p2sh address for the signing key and is used for the + // payment address throughout the tests. + signKey *btcec.PrivateKey + payAddr btcutil.Address + payScript []byte + chainParams *chaincfg.Params + + chain *fakeChain + txPool *TxPool +} + +// CreateCoinbaseTx returns a coinbase transaction with the requested number of +// outputs paying an appropriate subsidy based on the passed block height to the +// address associated with the harness. It automatically uses a standard +// signature script that starts with the block height that is required by +// version 2 blocks. +func (p *poolHarness) CreateCoinbaseTx(blockHeight int32, numOutputs uint32) (*btcutil.Tx, error) { + // Create standard coinbase script. + extraNonce := int64(0) + coinbaseScript, err := txscript.NewScriptBuilder(). + AddInt64(int64(blockHeight)).AddInt64(extraNonce).Script() + if err != nil { + return nil, err + } + + tx := wire.NewMsgTx() + tx.AddTxIn(&wire.TxIn{ + // Coinbase transactions have no inputs, so previous outpoint is + // zero hash and max index. + PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{}, + wire.MaxPrevOutIndex), + SignatureScript: coinbaseScript, + Sequence: wire.MaxTxInSequenceNum, + }) + totalInput := blockchain.CalcBlockSubsidy(blockHeight, p.chainParams) + amountPerOutput := totalInput / int64(numOutputs) + remainder := totalInput - amountPerOutput*int64(numOutputs) + for i := uint32(0); i < numOutputs; i++ { + // Ensure the final output accounts for any remainder that might + // be left from splitting the input amount. + amount := amountPerOutput + if i == numOutputs-1 { + amount = amountPerOutput + remainder + } + tx.AddTxOut(&wire.TxOut{ + PkScript: p.payScript, + Value: amount, + }) + } + + return btcutil.NewTx(tx), nil +} + +// CreateSignedTx creates a new signed transaction that consumes the provided +// inputs and generates the provided number of outputs by evenly splitting the +// total input amount. All outputs will be to the payment script associated +// with the harness and all inputs are assumed to do the same. +func (p *poolHarness) CreateSignedTx(inputs []spendableOutput, numOutputs uint32) (*btcutil.Tx, error) { + // Calculate the total input amount and split it amongst the requested + // number of outputs. + var totalInput btcutil.Amount + for _, input := range inputs { + totalInput += input.amount + } + amountPerOutput := int64(totalInput) / int64(numOutputs) + remainder := int64(totalInput) - amountPerOutput*int64(numOutputs) + + tx := wire.NewMsgTx() + for _, input := range inputs { + tx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: input.outPoint, + SignatureScript: nil, + Sequence: wire.MaxTxInSequenceNum, + }) + } + for i := uint32(0); i < numOutputs; i++ { + // Ensure the final output accounts for any remainder that might + // be left from splitting the input amount. + amount := amountPerOutput + if i == numOutputs-1 { + amount = amountPerOutput + remainder + } + tx.AddTxOut(&wire.TxOut{ + PkScript: p.payScript, + Value: amount, + }) + } + + // Sign the new transaction. + for i := range tx.TxIn { + sigScript, err := txscript.SignatureScript(tx, i, p.payScript, + txscript.SigHashAll, p.signKey, true) + if err != nil { + return nil, err + } + tx.TxIn[i].SignatureScript = sigScript + } + + return btcutil.NewTx(tx), nil +} + +// CreateTxChain creates a chain of zero-fee transactions (each subsequent +// transaction spends the entire amount from the previous one) with the first +// one spending the provided outpoint. Each transaction spends the entire +// amount of the previous one and as such does not include any fees. +func (p *poolHarness) CreateTxChain(firstOutput spendableOutput, numTxns uint32) ([]*btcutil.Tx, error) { + txChain := make([]*btcutil.Tx, 0, numTxns) + prevOutPoint := firstOutput.outPoint + spendableAmount := firstOutput.amount + for i := uint32(0); i < numTxns; i++ { + // Create the transaction using the previous transaction output + // and paying the full amount to the payment address associated + // with the harness. + tx := wire.NewMsgTx() + tx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: prevOutPoint, + SignatureScript: nil, + Sequence: wire.MaxTxInSequenceNum, + }) + tx.AddTxOut(&wire.TxOut{ + PkScript: p.payScript, + Value: int64(spendableAmount), + }) + + // Sign the new transaction. + sigScript, err := txscript.SignatureScript(tx, 0, p.payScript, + txscript.SigHashAll, p.signKey, true) + if err != nil { + return nil, err + } + tx.TxIn[0].SignatureScript = sigScript + + txChain = append(txChain, btcutil.NewTx(tx)) + + // Next transaction uses outputs from this one. + prevOutPoint = wire.OutPoint{Hash: tx.TxHash(), Index: 0} + } + + return txChain, nil +} + +// newPoolHarness returns a new instance of a pool harness initialized with a +// fake chain and a TxPool bound to it that is configured with a policy suitable +// for testing. Also, the fake chain is populated with the returned spendable +// outputs so the caller can easily create new valid transactions which build +// off of it. +func newPoolHarness(chainParams *chaincfg.Params) (*poolHarness, []spendableOutput, error) { + // Use a hard coded key pair for deterministic results. + keyBytes, err := hex.DecodeString("700868df1838811ffbdf918fb482c1f7e" + + "ad62db4b97bd7012c23e726485e577d") + if err != nil { + return nil, nil, err + } + signKey, signPub := btcec.PrivKeyFromBytes(btcec.S256(), keyBytes) + + // Generate associated pay-to-script-hash address and resulting payment + // script. + pubKeyBytes := signPub.SerializeCompressed() + payPubKeyAddr, err := btcutil.NewAddressPubKey(pubKeyBytes, chainParams) + if err != nil { + return nil, nil, err + } + payAddr := payPubKeyAddr.AddressPubKeyHash() + pkScript, err := txscript.PayToAddrScript(payAddr) + if err != nil { + return nil, nil, err + } + + // Create a new fake chain and harness bound to it. + chain := &fakeChain{utxos: blockchain.NewUtxoViewpoint()} + harness := poolHarness{ + signKey: signKey, + payAddr: payAddr, + payScript: pkScript, + chainParams: chainParams, + + chain: chain, + txPool: New(&Config{ + Policy: Policy{ + DisableRelayPriority: true, + FreeTxRelayLimit: 15.0, + MaxOrphanTxs: 5, + MaxOrphanTxSize: 1000, + MaxSigOpsPerTx: blockchain.MaxSigOpsPerBlock / 5, + MinRelayTxFee: 1000, // 1 Satoshi per byte + }, + ChainParams: chainParams, + FetchUtxoView: chain.FetchUtxoView, + BestHeight: chain.BestHeight, + SigCache: nil, + TimeSource: blockchain.NewMedianTime(), + AddrIndex: nil, + }), + } + + // Create a single coinbase transaction and add it to the harness + // chain's utxo set and set the harness chain height such that the + // coinbase will mature in the next block. This ensures the txpool + // accepts transactions which spend immature coinbases that will become + // mature in the next block. + numOutputs := uint32(1) + outputs := make([]spendableOutput, 0, numOutputs) + curHeight := harness.chain.BestHeight() + coinbase, err := harness.CreateCoinbaseTx(curHeight+1, numOutputs) + if err != nil { + return nil, nil, err + } + harness.chain.utxos.AddTxOuts(coinbase, curHeight+1) + for i := uint32(0); i < numOutputs; i++ { + outputs = append(outputs, txOutToSpendableOut(coinbase, i)) + } + harness.chain.SetHeight(int32(chainParams.CoinbaseMaturity) + curHeight) + + return &harness, outputs, nil +} + +// TestSimpleOrphanChain ensures that a simple chain of orphans is handled +// properly. In particular, it generates a chain of single input, single output +// transactions and inserts them while skipping the first linking transaction so +// they are all orphans. Finally, it adds the linking transaction and ensures +// the entire orphan chain is moved to the transaction pool. +func TestSimpleOrphanChain(t *testing.T) { + t.Parallel() + + harness, spendableOuts, err := newPoolHarness(&chaincfg.MainNetParams) + if err != nil { + t.Fatalf("unable to create test pool: %v", err) + } + + // Create a chain of transactions rooted with the first spendable output + // provided by the harness. + maxOrphans := uint32(harness.txPool.cfg.Policy.MaxOrphanTxs) + chainedTxns, err := harness.CreateTxChain(spendableOuts[0], maxOrphans+1) + if err != nil { + t.Fatalf("unable to create transaction chain: %v", err) + } + + // Ensure the orphans are accepted (only up to the maximum allowed so + // none are evicted). + for _, tx := range chainedTxns[1 : maxOrphans+1] { + acceptedTxns, err := harness.txPool.ProcessTransaction(tx, true, + false) + if err != nil { + t.Fatalf("ProcessTransaction: failed to accept valid "+ + "orphan %v", err) + } + + // Ensure no transactions were reported as accepted. + if len(acceptedTxns) != 0 { + t.Fatalf("ProcessTransaction: reported %d accepted "+ + "transactions from what should be an orphan", + len(acceptedTxns)) + } + + // Ensure the transaction is in the orphan pool. + if !harness.txPool.IsOrphanInPool(tx.Hash()) { + t.Fatal("IsOrphanInPool: false for accepted orphan") + } + + // Ensure the transaction is not in the transaction pool. + if harness.txPool.IsTransactionInPool(tx.Hash()) { + t.Fatal("IsTransactionInPool: true for accepted orphan") + } + + // Ensure the transaction is reported as available. + if !harness.txPool.HaveTransaction(tx.Hash()) { + t.Fatal("HaveTransaction: false for accepted orphan") + } + } + + // Add the transaction which completes the orphan chain and ensure they + // all get accepted. Notice the accept orphans flag is also false here + // to ensure it has no bearing on whether or not already existing + // orphans in the pool are linked. + acceptedTxns, err := harness.txPool.ProcessTransaction(chainedTxns[0], + false, false) + if err != nil { + t.Fatalf("ProcessTransaction: failed to accept valid "+ + "orphan %v", err) + } + if len(acceptedTxns) != len(chainedTxns) { + t.Fatalf("ProcessTransaction: reported accepted transactions "+ + "length does not match expected -- got %d, want %d", + len(acceptedTxns), len(chainedTxns)) + } + for _, tx := range acceptedTxns { + // Ensure none of the transactions are still in the orphan pool. + if harness.txPool.IsOrphanInPool(tx.Hash()) { + t.Fatalf("IsOrphanInPool: true for accepted tx %v", + tx.Hash()) + } + + // Ensure all of the transactions are now in the transaction + // pool. + if !harness.txPool.IsTransactionInPool(tx.Hash()) { + t.Fatalf("IsTransactionInPool: false for accepted tx %v", + tx.Hash()) + } + } +} + +// TestOrphanReject ensures that orphans are properly rejected when the allow +// orphans flag is not set on ProcessTransaction. +func TestOrphanReject(t *testing.T) { + t.Parallel() + + harness, outputs, err := newPoolHarness(&chaincfg.MainNetParams) + if err != nil { + t.Fatalf("unable to create test pool: %v", err) + } + + // Create a chain of transactions rooted with the first spendable output + // provided by the harness. + maxOrphans := uint32(harness.txPool.cfg.Policy.MaxOrphanTxs) + chainedTxns, err := harness.CreateTxChain(outputs[0], maxOrphans+1) + if err != nil { + t.Fatalf("unable to create transaction chain: %v", err) + } + + // Ensure orphans are rejected when the allow orphans flag is not set. + for _, tx := range chainedTxns[1:] { + acceptedTxns, err := harness.txPool.ProcessTransaction(tx, false, + false) + if err == nil { + t.Fatalf("ProcessTransaction: did not fail on orphan "+ + "%v when allow orphans flag is false", tx.Hash()) + } + expectedErr := RuleError{} + if reflect.TypeOf(err) != reflect.TypeOf(expectedErr) { + t.Fatalf("ProcessTransaction: wrong error got: <%T> %v, "+ + "want: <%T>", err, err, expectedErr) + } + code, extracted := extractRejectCode(err) + if !extracted { + t.Fatalf("ProcessTransaction: failed to extract reject "+ + "code from error %q", err) + } + if code != wire.RejectDuplicate { + t.Fatalf("ProcessTransaction: unexpected reject code "+ + "-- got %v, want %v", code, wire.RejectDuplicate) + } + + // Ensure no transactions were reported as accepted. + if len(acceptedTxns) != 0 { + t.Fatal("ProcessTransaction: reported %d accepted "+ + "transactions from failed orphan attempt", + len(acceptedTxns)) + } + + // Ensure the transaction is not in the orphan pool. + if harness.txPool.IsOrphanInPool(tx.Hash()) { + t.Fatal("IsOrphanInPool: true for rejected orphan") + } + + // Ensure the transaction is not in the transaction pool. + if harness.txPool.IsTransactionInPool(tx.Hash()) { + t.Fatal("IsTransactionInPool: true for rejected orphan") + } + + // Ensure the transaction is not reported as available. + if harness.txPool.HaveTransaction(tx.Hash()) { + t.Fatal("HaveTransaction: true for rejected orphan") + } + } +}