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
This commit is contained in:
parent
a109bea3f1
commit
15bace88dc
2 changed files with 486 additions and 0 deletions
|
@ -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 {
|
||||
|
|
463
mempool/mempool_test.go
Normal file
463
mempool/mempool_test.go
Normal file
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue