// 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" "strings" "sync" "testing" "time" "github.com/lbryio/lbcd/blockchain" "github.com/lbryio/lbcd/btcec" "github.com/lbryio/lbcd/chaincfg" "github.com/lbryio/lbcd/chaincfg/chainhash" "github.com/lbryio/lbcd/txscript" "github.com/lbryio/lbcd/wire" btcutil "github.com/lbryio/lbcutil" ) // 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 // transactions to appear as though they are spending completely valid utxos. type fakeChain struct { sync.RWMutex utxos *blockchain.UtxoViewpoint currentHeight int32 medianTimePast time.Time } // FetchUtxoView loads utxo details about the inputs referenced by the passed // transaction from the point of view of the fake chain. It also attempts to // fetch the utxos for the outputs of the transaction itself so the returned // view can be examined for duplicate transactions. // // 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() prevOut := wire.OutPoint{Hash: *tx.Hash()} for txOutIdx := range tx.MsgTx().TxOut { prevOut.Index = uint32(txOutIdx) entry := s.utxos.LookupEntry(prevOut) viewpoint.Entries()[prevOut] = entry.Clone() } // Add entries for all of the inputs to the tx to the new view. for _, txIn := range tx.MsgTx().TxIn { entry := s.utxos.LookupEntry(txIn.PreviousOutPoint) viewpoint.Entries()[txIn.PreviousOutPoint] = 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() } // MedianTimePast returns the current median time past associated with the fake // chain instance. func (s *fakeChain) MedianTimePast() time.Time { s.RLock() mtp := s.medianTimePast s.RUnlock() return mtp } // SetMedianTimePast sets the current median time past associated with the fake // chain instance. func (s *fakeChain) SetMedianTimePast(mtp time.Time) { s.Lock() s.medianTimePast = mtp s.Unlock() } // CalcSequenceLock returns the current sequence lock for the passed // transaction associated with the fake chain instance. func (s *fakeChain) CalcSequenceLock(tx *btcutil.Tx, view *blockchain.UtxoViewpoint) (*blockchain.SequenceLock, error) { return &blockchain.SequenceLock{ Seconds: -1, BlockHeight: -1, }, nil } // 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(wire.TxVersion) 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, fee btcutil.Amount, signalsReplacement bool) (*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 } totalInput -= fee amountPerOutput := int64(totalInput) / int64(numOutputs) remainder := int64(totalInput) - amountPerOutput*int64(numOutputs) tx := wire.NewMsgTx(wire.TxVersion) sequence := wire.MaxTxInSequenceNum if signalsReplacement { sequence = MaxRBFSequence } for _, input := range inputs { tx.AddTxIn(&wire.TxIn{ PreviousOutPoint: input.outPoint, SignatureScript: nil, Sequence: sequence, }) } 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(wire.TxVersion) 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, MaxSigOpCostPerTx: blockchain.MaxBlockSigOpsCost / 4, MinRelayTxFee: 1000, // 1 Satoshi per byte MaxTxVersion: 1, }, ChainParams: chainParams, FetchUtxoView: chain.FetchUtxoView, BestHeight: chain.BestHeight, MedianTimePast: chain.MedianTimePast, CalcSequenceLock: chain.CalcSequenceLock, SigCache: nil, 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) harness.chain.SetMedianTimePast(time.Now()) return &harness, outputs, nil } // testContext houses a test-related state that is useful to pass to helper // functions as a single argument. type testContext struct { t *testing.T harness *poolHarness } // addCoinbaseTx adds a spendable coinbase transaction to the test context's // mock chain. func (ctx *testContext) addCoinbaseTx(numOutputs uint32) *btcutil.Tx { ctx.t.Helper() coinbaseHeight := ctx.harness.chain.BestHeight() + 1 coinbase, err := ctx.harness.CreateCoinbaseTx(coinbaseHeight, numOutputs) if err != nil { ctx.t.Fatalf("unable to create coinbase: %v", err) } ctx.harness.chain.utxos.AddTxOuts(coinbase, coinbaseHeight) maturity := int32(ctx.harness.chainParams.CoinbaseMaturity) ctx.harness.chain.SetHeight(coinbaseHeight + maturity) ctx.harness.chain.SetMedianTimePast(time.Now()) return coinbase } // addSignedTx creates a transaction that spends the inputs with the given fee. // It can be added to the test context's mempool or mock chain based on the // confirmed boolean. func (ctx *testContext) addSignedTx(inputs []spendableOutput, numOutputs uint32, fee btcutil.Amount, signalsReplacement, confirmed bool) *btcutil.Tx { ctx.t.Helper() tx, err := ctx.harness.CreateSignedTx( inputs, numOutputs, fee, signalsReplacement, ) if err != nil { ctx.t.Fatalf("unable to create transaction: %v", err) } if confirmed { newHeight := ctx.harness.chain.BestHeight() + 1 ctx.harness.chain.utxos.AddTxOuts(tx, newHeight) ctx.harness.chain.SetHeight(newHeight) ctx.harness.chain.SetMedianTimePast(time.Now()) } else { acceptedTxns, err := ctx.harness.txPool.ProcessTransaction( tx, true, false, 0, ) if err != nil { ctx.t.Fatalf("unable to process transaction: %v", err) } if len(acceptedTxns) != 1 { ctx.t.Fatalf("expected one accepted transaction, got %d", len(acceptedTxns)) } testPoolMembership(ctx, tx, false, true) } return tx } // testPoolMembership tests the transaction pool associated with the provided // test context to determine if the passed transaction matches the provided // orphan pool and transaction pool status. It also further determines if it // should be reported as available by the HaveTransaction function based upon // the two flags and tests that condition as well. func testPoolMembership(tc *testContext, tx *btcutil.Tx, inOrphanPool, inTxPool bool) { tc.t.Helper() txHash := tx.Hash() gotOrphanPool := tc.harness.txPool.IsOrphanInPool(txHash) if inOrphanPool != gotOrphanPool { tc.t.Fatalf("IsOrphanInPool: want %v, got %v", inOrphanPool, gotOrphanPool) } gotTxPool := tc.harness.txPool.IsTransactionInPool(txHash) if inTxPool != gotTxPool { tc.t.Fatalf("IsTransactionInPool: want %v, got %v", inTxPool, gotTxPool) } gotHaveTx := tc.harness.txPool.HaveTransaction(txHash) wantHaveTx := inOrphanPool || inTxPool if wantHaveTx != gotHaveTx { tc.t.Fatalf("HaveTransaction: want %v, got %v", wantHaveTx, gotHaveTx) } } // 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) } tc := &testContext{t, harness} // 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, 0) 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, is not in the // transaction pool, and is reported as available. testPoolMembership(tc, tx, true, false) } // 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, 0) 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 _, txD := range acceptedTxns { // Ensure the transaction is no longer in the orphan pool, is // now in the transaction pool, and is reported as available. testPoolMembership(tc, txD.Tx, false, true) } } // 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) } tc := &testContext{t, harness} // 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, 0) 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, not in the // transaction pool, and not reported as available testPoolMembership(tc, tx, false, false) } } // TestOrphanEviction ensures that exceeding the maximum number of orphans // evicts entries to make room for the new ones. func TestOrphanEviction(t *testing.T) { t.Parallel() harness, outputs, err := newPoolHarness(&chaincfg.MainNetParams) if err != nil { t.Fatalf("unable to create test pool: %v", err) } tc := &testContext{t, harness} // Create a chain of transactions rooted with the first spendable output // provided by the harness that is long enough to be able to force // several orphan evictions. maxOrphans := uint32(harness.txPool.cfg.Policy.MaxOrphanTxs) chainedTxns, err := harness.CreateTxChain(outputs[0], maxOrphans+5) if err != nil { t.Fatalf("unable to create transaction chain: %v", err) } // Add enough orphans to exceed the max allowed while ensuring they are // all accepted. This will cause an eviction. for _, tx := range chainedTxns[1:] { acceptedTxns, err := harness.txPool.ProcessTransaction(tx, true, false, 0) 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, is not in the // transaction pool, and is reported as available. testPoolMembership(tc, tx, true, false) } // Figure out which transactions were evicted and make sure the number // evicted matches the expected number. var evictedTxns []*btcutil.Tx for _, tx := range chainedTxns[1:] { if !harness.txPool.IsOrphanInPool(tx.Hash()) { evictedTxns = append(evictedTxns, tx) } } expectedEvictions := len(chainedTxns) - 1 - int(maxOrphans) if len(evictedTxns) != expectedEvictions { t.Fatalf("unexpected number of evictions -- got %d, want %d", len(evictedTxns), expectedEvictions) } // Ensure none of the evicted transactions ended up in the transaction // pool. for _, tx := range evictedTxns { testPoolMembership(tc, tx, false, false) } } // TestBasicOrphanRemoval ensure that orphan removal works as expected when an // orphan that doesn't exist is removed both when there is another orphan that // redeems it and when there is not. func TestBasicOrphanRemoval(t *testing.T) { t.Parallel() const maxOrphans = 4 harness, spendableOuts, err := newPoolHarness(&chaincfg.MainNetParams) if err != nil { t.Fatalf("unable to create test pool: %v", err) } harness.txPool.cfg.Policy.MaxOrphanTxs = maxOrphans tc := &testContext{t, harness} // Create a chain of transactions rooted with the first spendable output // provided by the harness. 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, 0) 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, not in the // transaction pool, and reported as available. testPoolMembership(tc, tx, true, false) } // Attempt to remove an orphan that has no redeemers and is not present, // and ensure the state of all other orphans are unaffected. nonChainedOrphanTx, err := harness.CreateSignedTx([]spendableOutput{{ amount: btcutil.Amount(5000000000), outPoint: wire.OutPoint{Hash: chainhash.Hash{}, Index: 0}, }}, 1, 0, false) if err != nil { t.Fatalf("unable to create signed tx: %v", err) } harness.txPool.RemoveOrphan(nonChainedOrphanTx) testPoolMembership(tc, nonChainedOrphanTx, false, false) for _, tx := range chainedTxns[1 : maxOrphans+1] { testPoolMembership(tc, tx, true, false) } // Attempt to remove an orphan that has a existing redeemer but itself // is not present and ensure the state of all other orphans (including // the one that redeems it) are unaffected. harness.txPool.RemoveOrphan(chainedTxns[0]) testPoolMembership(tc, chainedTxns[0], false, false) for _, tx := range chainedTxns[1 : maxOrphans+1] { testPoolMembership(tc, tx, true, false) } // Remove each orphan one-by-one and ensure they are removed as // expected. for _, tx := range chainedTxns[1 : maxOrphans+1] { harness.txPool.RemoveOrphan(tx) testPoolMembership(tc, tx, false, false) } } // TestOrphanChainRemoval ensure that orphan chains (orphans that spend outputs // from other orphans) are removed as expected. func TestOrphanChainRemoval(t *testing.T) { t.Parallel() const maxOrphans = 10 harness, spendableOuts, err := newPoolHarness(&chaincfg.MainNetParams) if err != nil { t.Fatalf("unable to create test pool: %v", err) } harness.txPool.cfg.Policy.MaxOrphanTxs = maxOrphans tc := &testContext{t, harness} // Create a chain of transactions rooted with the first spendable output // provided by the harness. 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, 0) 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, not in the // transaction pool, and reported as available. testPoolMembership(tc, tx, true, false) } // Remove the first orphan that starts the orphan chain without the // remove redeemer flag set and ensure that only the first orphan was // removed. harness.txPool.mtx.Lock() harness.txPool.removeOrphan(chainedTxns[1], false) harness.txPool.mtx.Unlock() testPoolMembership(tc, chainedTxns[1], false, false) for _, tx := range chainedTxns[2 : maxOrphans+1] { testPoolMembership(tc, tx, true, false) } // Remove the first remaining orphan that starts the orphan chain with // the remove redeemer flag set and ensure they are all removed. harness.txPool.mtx.Lock() harness.txPool.removeOrphan(chainedTxns[2], true) harness.txPool.mtx.Unlock() for _, tx := range chainedTxns[2 : maxOrphans+1] { testPoolMembership(tc, tx, false, false) } } // TestMultiInputOrphanDoubleSpend ensures that orphans that spend from an // output that is spend by another transaction entering the pool are removed. func TestMultiInputOrphanDoubleSpend(t *testing.T) { t.Parallel() const maxOrphans = 4 harness, outputs, err := newPoolHarness(&chaincfg.MainNetParams) if err != nil { t.Fatalf("unable to create test pool: %v", err) } harness.txPool.cfg.Policy.MaxOrphanTxs = maxOrphans tc := &testContext{t, harness} // Create a chain of transactions rooted with the first spendable output // provided by the harness. chainedTxns, err := harness.CreateTxChain(outputs[0], maxOrphans+1) if err != nil { t.Fatalf("unable to create transaction chain: %v", err) } // Start by adding the orphan transactions from the generated chain // except the final one. for _, tx := range chainedTxns[1:maxOrphans] { acceptedTxns, err := harness.txPool.ProcessTransaction(tx, true, false, 0) if err != nil { t.Fatalf("ProcessTransaction: failed to accept valid "+ "orphan %v", err) } if len(acceptedTxns) != 0 { t.Fatalf("ProcessTransaction: reported %d accepted transactions "+ "from what should be an orphan", len(acceptedTxns)) } testPoolMembership(tc, tx, true, false) } // Ensure a transaction that contains a double spend of the same output // as the second orphan that was just added as well as a valid spend // from that last orphan in the chain generated above (and is not in the // orphan pool) is accepted to the orphan pool. This must be allowed // since it would otherwise be possible for a malicious actor to disrupt // tx chains. doubleSpendTx, err := harness.CreateSignedTx([]spendableOutput{ txOutToSpendableOut(chainedTxns[1], 0), txOutToSpendableOut(chainedTxns[maxOrphans], 0), }, 1, 0, false) if err != nil { t.Fatalf("unable to create signed tx: %v", err) } acceptedTxns, err := harness.txPool.ProcessTransaction(doubleSpendTx, true, false, 0) if err != nil { t.Fatalf("ProcessTransaction: failed to accept valid orphan %v", err) } if len(acceptedTxns) != 0 { t.Fatalf("ProcessTransaction: reported %d accepted transactions "+ "from what should be an orphan", len(acceptedTxns)) } testPoolMembership(tc, doubleSpendTx, true, false) // Add the transaction which completes the orphan chain and ensure the // chain gets 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. // // This will cause the shared output to become a concrete spend which // will in turn must cause the double spending orphan to be removed. acceptedTxns, err = harness.txPool.ProcessTransaction(chainedTxns[0], false, false, 0) if err != nil { t.Fatalf("ProcessTransaction: failed to accept valid tx %v", err) } if len(acceptedTxns) != maxOrphans { t.Fatalf("ProcessTransaction: reported accepted transactions "+ "length does not match expected -- got %d, want %d", len(acceptedTxns), maxOrphans) } for _, txD := range acceptedTxns { // Ensure the transaction is no longer in the orphan pool, is // in the transaction pool, and is reported as available. testPoolMembership(tc, txD.Tx, false, true) } // Ensure the double spending orphan is no longer in the orphan pool and // was not moved to the transaction pool. testPoolMembership(tc, doubleSpendTx, false, false) } // TestCheckSpend tests that CheckSpend returns the expected spends found in // the mempool. func TestCheckSpend(t *testing.T) { t.Parallel() harness, outputs, err := newPoolHarness(&chaincfg.MainNetParams) if err != nil { t.Fatalf("unable to create test pool: %v", err) } // The mempool is empty, so none of the spendable outputs should have a // spend there. for _, op := range outputs { spend := harness.txPool.CheckSpend(op.outPoint) if spend != nil { t.Fatalf("Unexpeced spend found in pool: %v", spend) } } // Create a chain of transactions rooted with the first spendable // output provided by the harness. const txChainLength = 5 chainedTxns, err := harness.CreateTxChain(outputs[0], txChainLength) if err != nil { t.Fatalf("unable to create transaction chain: %v", err) } for _, tx := range chainedTxns { _, err := harness.txPool.ProcessTransaction(tx, true, false, 0) if err != nil { t.Fatalf("ProcessTransaction: failed to accept "+ "tx: %v", err) } } // The first tx in the chain should be the spend of the spendable // output. op := outputs[0].outPoint spend := harness.txPool.CheckSpend(op) if spend != chainedTxns[0] { t.Fatalf("expected %v to be spent by %v, instead "+ "got %v", op, chainedTxns[0], spend) } // Now all but the last tx should be spent by the next. for i := 0; i < len(chainedTxns)-1; i++ { op = wire.OutPoint{ Hash: *chainedTxns[i].Hash(), Index: 0, } expSpend := chainedTxns[i+1] spend = harness.txPool.CheckSpend(op) if spend != expSpend { t.Fatalf("expected %v to be spent by %v, instead "+ "got %v", op, expSpend, spend) } } // The last tx should have no spend. op = wire.OutPoint{ Hash: *chainedTxns[txChainLength-1].Hash(), Index: 0, } spend = harness.txPool.CheckSpend(op) if spend != nil { t.Fatalf("Unexpeced spend found in pool: %v", spend) } } // TestSignalsReplacement tests that transactions properly signal they can be // replaced using RBF. func TestSignalsReplacement(t *testing.T) { t.Parallel() testCases := []struct { name string setup func(ctx *testContext) *btcutil.Tx signalsReplacement bool }{ { // Transactions can signal replacement through // inheritance if any of its ancestors does. name: "non-signaling with unconfirmed non-signaling parent", setup: func(ctx *testContext) *btcutil.Tx { coinbase := ctx.addCoinbaseTx(1) coinbaseOut := txOutToSpendableOut(coinbase, 0) outs := []spendableOutput{coinbaseOut} parent := ctx.addSignedTx(outs, 1, 0, false, false) parentOut := txOutToSpendableOut(parent, 0) outs = []spendableOutput{parentOut} return ctx.addSignedTx(outs, 1, 0, false, false) }, signalsReplacement: false, }, { // Transactions can signal replacement through // inheritance if any of its ancestors does, but they // must be unconfirmed. name: "non-signaling with confirmed signaling parent", setup: func(ctx *testContext) *btcutil.Tx { coinbase := ctx.addCoinbaseTx(1) coinbaseOut := txOutToSpendableOut(coinbase, 0) outs := []spendableOutput{coinbaseOut} parent := ctx.addSignedTx(outs, 1, 0, true, true) parentOut := txOutToSpendableOut(parent, 0) outs = []spendableOutput{parentOut} return ctx.addSignedTx(outs, 1, 0, false, false) }, signalsReplacement: false, }, { name: "inherited signaling", setup: func(ctx *testContext) *btcutil.Tx { coinbase := ctx.addCoinbaseTx(1) // We'll create a chain of transactions // A -> B -> C where C is the transaction we'll // be checking for replacement signaling. The // transaction can signal replacement through // any of its ancestors as long as they also // signal replacement. coinbaseOut := txOutToSpendableOut(coinbase, 0) outs := []spendableOutput{coinbaseOut} a := ctx.addSignedTx(outs, 1, 0, true, false) aOut := txOutToSpendableOut(a, 0) outs = []spendableOutput{aOut} b := ctx.addSignedTx(outs, 1, 0, false, false) bOut := txOutToSpendableOut(b, 0) outs = []spendableOutput{bOut} return ctx.addSignedTx(outs, 1, 0, false, false) }, signalsReplacement: true, }, { name: "explicit signaling", setup: func(ctx *testContext) *btcutil.Tx { coinbase := ctx.addCoinbaseTx(1) coinbaseOut := txOutToSpendableOut(coinbase, 0) outs := []spendableOutput{coinbaseOut} return ctx.addSignedTx(outs, 1, 0, true, false) }, signalsReplacement: true, }, } for _, testCase := range testCases { success := t.Run(testCase.name, func(t *testing.T) { // We'll start each test by creating our mempool // harness. harness, _, err := newPoolHarness(&chaincfg.MainNetParams) if err != nil { t.Fatalf("unable to create test pool: %v", err) } ctx := &testContext{t, harness} // Each test includes a setup method, which will set up // its required dependencies. The transaction returned // is the one we'll be using to determine if it signals // replacement support. tx := testCase.setup(ctx) // Each test should match the expected response. signalsReplacement := ctx.harness.txPool.signalsReplacement( tx, nil, ) if signalsReplacement && !testCase.signalsReplacement { ctx.t.Fatalf("expected transaction %v to not "+ "signal replacement", tx.Hash()) } if !signalsReplacement && testCase.signalsReplacement { ctx.t.Fatalf("expected transaction %v to "+ "signal replacement", tx.Hash()) } }) if !success { break } } } // TestCheckPoolDoubleSpend ensures that the mempool can properly detect // unconfirmed double spends in the case of replacement and non-replacement // transactions. func TestCheckPoolDoubleSpend(t *testing.T) { t.Parallel() testCases := []struct { name string setup func(ctx *testContext) *btcutil.Tx isReplacement bool }{ { // Transactions that don't double spend any inputs, // regardless of whether they signal replacement or not, // are valid. name: "no double spend", setup: func(ctx *testContext) *btcutil.Tx { coinbase := ctx.addCoinbaseTx(1) coinbaseOut := txOutToSpendableOut(coinbase, 0) outs := []spendableOutput{coinbaseOut} parent := ctx.addSignedTx(outs, 1, 0, false, false) parentOut := txOutToSpendableOut(parent, 0) outs = []spendableOutput{parentOut} return ctx.addSignedTx(outs, 2, 0, false, false) }, isReplacement: false, }, { // Transactions that don't signal replacement and double // spend inputs are invalid. name: "non-replacement double spend", setup: func(ctx *testContext) *btcutil.Tx { coinbase1 := ctx.addCoinbaseTx(1) coinbaseOut1 := txOutToSpendableOut(coinbase1, 0) outs := []spendableOutput{coinbaseOut1} ctx.addSignedTx(outs, 1, 0, true, false) coinbase2 := ctx.addCoinbaseTx(1) coinbaseOut2 := txOutToSpendableOut(coinbase2, 0) outs = []spendableOutput{coinbaseOut2} ctx.addSignedTx(outs, 1, 0, false, false) // Create a transaction that spends both // coinbase outputs that were spent above. This // should be detected as a double spend as one // of the transactions doesn't signal // replacement. outs = []spendableOutput{coinbaseOut1, coinbaseOut2} tx, err := ctx.harness.CreateSignedTx( outs, 1, 0, false, ) if err != nil { ctx.t.Fatalf("unable to create "+ "transaction: %v", err) } return tx }, isReplacement: false, }, { // Transactions that double spend inputs and signal // replacement are invalid if the mempool's policy // rejects replacements. name: "reject replacement policy", setup: func(ctx *testContext) *btcutil.Tx { // Set the mempool's policy to reject // replacements. Even if we have a transaction // that spends inputs that signal replacement, // it should still be rejected. ctx.harness.txPool.cfg.Policy.RejectReplacement = true coinbase := ctx.addCoinbaseTx(1) // Create a replaceable parent that spends the // coinbase output. coinbaseOut := txOutToSpendableOut(coinbase, 0) outs := []spendableOutput{coinbaseOut} parent := ctx.addSignedTx(outs, 1, 0, true, false) parentOut := txOutToSpendableOut(parent, 0) outs = []spendableOutput{parentOut} ctx.addSignedTx(outs, 1, 0, false, false) // Create another transaction that spends the // same coinbase output. Since the original // spender of this output, all of its spends // should also be conflicts. outs = []spendableOutput{coinbaseOut} tx, err := ctx.harness.CreateSignedTx( outs, 2, 0, false, ) if err != nil { ctx.t.Fatalf("unable to create "+ "transaction: %v", err) } return tx }, isReplacement: false, }, { // Transactions that double spend inputs and signal // replacement are valid as long as the mempool's policy // accepts them. name: "replacement double spend", setup: func(ctx *testContext) *btcutil.Tx { coinbase := ctx.addCoinbaseTx(1) // Create a replaceable parent that spends the // coinbase output. coinbaseOut := txOutToSpendableOut(coinbase, 0) outs := []spendableOutput{coinbaseOut} parent := ctx.addSignedTx(outs, 1, 0, true, false) parentOut := txOutToSpendableOut(parent, 0) outs = []spendableOutput{parentOut} ctx.addSignedTx(outs, 1, 0, false, false) // Create another transaction that spends the // same coinbase output. Since the original // spender of this output, all of its spends // should also be conflicts. outs = []spendableOutput{coinbaseOut} tx, err := ctx.harness.CreateSignedTx( outs, 2, 0, false, ) if err != nil { ctx.t.Fatalf("unable to create "+ "transaction: %v", err) } return tx }, isReplacement: true, }, } for _, testCase := range testCases { success := t.Run(testCase.name, func(t *testing.T) { // We'll start each test by creating our mempool // harness. harness, _, err := newPoolHarness(&chaincfg.MainNetParams) if err != nil { t.Fatalf("unable to create test pool: %v", err) } ctx := &testContext{t, harness} // Each test includes a setup method, which will set up // its required dependencies. The transaction returned // is the one we'll be querying for the expected // conflicts. tx := testCase.setup(ctx) // Ensure that the mempool properly detected the double // spend unless this is a replacement transaction. isReplacement, err := ctx.harness.txPool.checkPoolDoubleSpend(tx) if testCase.isReplacement && err != nil { t.Fatalf("expected no error for replacement "+ "transaction, got: %v", err) } if isReplacement && !testCase.isReplacement { t.Fatalf("expected replacement transaction") } if !isReplacement && testCase.isReplacement { t.Fatalf("expected non-replacement transaction") } }) if !success { break } } } // TestConflicts ensures that the mempool can properly detect conflicts when // processing new incoming transactions. func TestConflicts(t *testing.T) { t.Parallel() testCases := []struct { name string // setup sets up the required dependencies for each test. It // returns the transaction we'll check for conflicts and its // expected unique conflicts. setup func(ctx *testContext) (*btcutil.Tx, []*btcutil.Tx) }{ { // Create a transaction that would introduce no // conflicts in the mempool. This is done by not // spending any outputs that are currently being spent // within the mempool. name: "no conflicts", setup: func(ctx *testContext) (*btcutil.Tx, []*btcutil.Tx) { coinbase := ctx.addCoinbaseTx(1) coinbaseOut := txOutToSpendableOut(coinbase, 0) outs := []spendableOutput{coinbaseOut} parent := ctx.addSignedTx(outs, 1, 0, false, false) parentOut := txOutToSpendableOut(parent, 0) outs = []spendableOutput{parentOut} tx, err := ctx.harness.CreateSignedTx( outs, 2, 0, false, ) if err != nil { ctx.t.Fatalf("unable to create "+ "transaction: %v", err) } return tx, nil }, }, { // Create a transaction that would introduce two // conflicts in the mempool by spending two outputs // which are each already being spent by a different // transaction within the mempool. name: "conflicts", setup: func(ctx *testContext) (*btcutil.Tx, []*btcutil.Tx) { coinbase1 := ctx.addCoinbaseTx(1) coinbaseOut1 := txOutToSpendableOut(coinbase1, 0) outs := []spendableOutput{coinbaseOut1} conflict1 := ctx.addSignedTx( outs, 1, 0, false, false, ) coinbase2 := ctx.addCoinbaseTx(1) coinbaseOut2 := txOutToSpendableOut(coinbase2, 0) outs = []spendableOutput{coinbaseOut2} conflict2 := ctx.addSignedTx( outs, 1, 0, false, false, ) // Create a transaction that spends both // coinbase outputs that were spent above. outs = []spendableOutput{coinbaseOut1, coinbaseOut2} tx, err := ctx.harness.CreateSignedTx( outs, 1, 0, false, ) if err != nil { ctx.t.Fatalf("unable to create "+ "transaction: %v", err) } return tx, []*btcutil.Tx{conflict1, conflict2} }, }, { // Create a transaction that would introduce two // conflicts in the mempool by spending an output // already being spent in the mempool by a different // transaction. The second conflict stems from spending // the transaction that spends the original spender of // the output, i.e., a descendant of the original // spender. name: "descendant conflicts", setup: func(ctx *testContext) (*btcutil.Tx, []*btcutil.Tx) { coinbase := ctx.addCoinbaseTx(1) // Create a replaceable parent that spends the // coinbase output. coinbaseOut := txOutToSpendableOut(coinbase, 0) outs := []spendableOutput{coinbaseOut} parent := ctx.addSignedTx(outs, 1, 0, false, false) parentOut := txOutToSpendableOut(parent, 0) outs = []spendableOutput{parentOut} child := ctx.addSignedTx(outs, 1, 0, false, false) // Create another transaction that spends the // same coinbase output. Since the original // spender of this output has descendants, they // should also be conflicts. outs = []spendableOutput{coinbaseOut} tx, err := ctx.harness.CreateSignedTx( outs, 2, 0, false, ) if err != nil { ctx.t.Fatalf("unable to create "+ "transaction: %v", err) } return tx, []*btcutil.Tx{parent, child} }, }, } for _, testCase := range testCases { success := t.Run(testCase.name, func(t *testing.T) { // We'll start each test by creating our mempool // harness. harness, _, err := newPoolHarness(&chaincfg.MainNetParams) if err != nil { t.Fatalf("unable to create test pool: %v", err) } ctx := &testContext{t, harness} // Each test includes a setup method, which will set up // its required dependencies. The transaction returned // is the one we'll be querying for the expected // conflicts. tx, conflicts := testCase.setup(ctx) // Assert the expected conflicts are returned. txConflicts := ctx.harness.txPool.txConflicts(tx) if len(txConflicts) != len(conflicts) { ctx.t.Fatalf("expected %d conflicts, got %d", len(conflicts), len(txConflicts)) } for _, conflict := range conflicts { conflictHash := *conflict.Hash() if _, ok := txConflicts[conflictHash]; !ok { ctx.t.Fatalf("expected %v to be found "+ "as a conflict", conflictHash) } } }) if !success { break } } } // TestAncestorsDescendants ensures that we can properly retrieve the // unconfirmed ancestors and descendants of a transaction. func TestAncestorsDescendants(t *testing.T) { t.Parallel() // We'll start the test by initializing our mempool harness. harness, outputs, err := newPoolHarness(&chaincfg.MainNetParams) if err != nil { t.Fatalf("unable to create test pool: %v", err) } ctx := &testContext{t, harness} // We'll be creating the following chain of unconfirmed transactions: // // B ---- // / \ // A E // \ / // C -- D // // where B and C spend A, D spends C, and E spends B and D. We set up a // chain like so to properly detect ancestors and descendants past a // single parent/child. aInputs := outputs[:1] a := ctx.addSignedTx(aInputs, 2, 0, false, false) bInputs := []spendableOutput{txOutToSpendableOut(a, 0)} b := ctx.addSignedTx(bInputs, 1, 0, false, false) cInputs := []spendableOutput{txOutToSpendableOut(a, 1)} c := ctx.addSignedTx(cInputs, 1, 0, false, false) dInputs := []spendableOutput{txOutToSpendableOut(c, 0)} d := ctx.addSignedTx(dInputs, 1, 0, false, false) eInputs := []spendableOutput{ txOutToSpendableOut(b, 0), txOutToSpendableOut(d, 0), } e := ctx.addSignedTx(eInputs, 1, 0, false, false) // We'll be querying for the ancestors of E. We should expect to see all // of the transactions that it depends on. expectedAncestors := map[chainhash.Hash]struct{}{ *a.Hash(): {}, *b.Hash(): {}, *c.Hash(): {}, *d.Hash(): {}, } ancestors := ctx.harness.txPool.txAncestors(e, nil) if len(ancestors) != len(expectedAncestors) { ctx.t.Fatalf("expected %d ancestors, got %d", len(expectedAncestors), len(ancestors)) } for ancestorHash := range ancestors { if _, ok := expectedAncestors[ancestorHash]; !ok { ctx.t.Fatalf("found unexpected ancestor %v", ancestorHash) } } // Then, we'll query for the descendants of A. We should expect to see // all of the transactions that depend on it. expectedDescendants := map[chainhash.Hash]struct{}{ *b.Hash(): {}, *c.Hash(): {}, *d.Hash(): {}, *e.Hash(): {}, } descendants := ctx.harness.txPool.txDescendants(a, nil) if len(descendants) != len(expectedDescendants) { ctx.t.Fatalf("expected %d descendants, got %d", len(expectedDescendants), len(descendants)) } for descendantHash := range descendants { if _, ok := expectedDescendants[descendantHash]; !ok { ctx.t.Fatalf("found unexpected descendant %v", descendantHash) } } } // TestRBF tests the different cases required for a transaction to properly // replace its conflicts given that they all signal replacement. func TestRBF(t *testing.T) { t.Parallel() const defaultFee = btcutil.SatoshiPerBitcoin / 50 testCases := []struct { name string setup func(ctx *testContext) (*btcutil.Tx, []*btcutil.Tx) err string }{ { // A transaction cannot replace another if it doesn't // signal replacement. name: "non-replaceable parent", setup: func(ctx *testContext) (*btcutil.Tx, []*btcutil.Tx) { coinbase := ctx.addCoinbaseTx(1) // Create a transaction that spends the coinbase // output and doesn't signal for replacement. coinbaseOut := txOutToSpendableOut(coinbase, 0) outs := []spendableOutput{coinbaseOut} ctx.addSignedTx(outs, 1, defaultFee, false, false) // Attempting to create another transaction that // spends the same output should fail since the // original transaction spending it doesn't // signal replacement. tx, err := ctx.harness.CreateSignedTx( outs, 2, defaultFee, false, ) if err != nil { ctx.t.Fatalf("unable to create "+ "transaction: %v", err) } return tx, nil }, err: "already spent by transaction", }, { // A transaction cannot replace another if we don't // allow accepting replacement transactions. name: "reject replacement policy", setup: func(ctx *testContext) (*btcutil.Tx, []*btcutil.Tx) { ctx.harness.txPool.cfg.Policy.RejectReplacement = true coinbase := ctx.addCoinbaseTx(1) // Create a transaction that spends the coinbase // output and doesn't signal for replacement. coinbaseOut := txOutToSpendableOut(coinbase, 0) outs := []spendableOutput{coinbaseOut} ctx.addSignedTx(outs, 1, defaultFee, true, false) // Attempting to create another transaction that // spends the same output should fail since the // original transaction spending it doesn't // signal replacement. tx, err := ctx.harness.CreateSignedTx( outs, 2, defaultFee, false, ) if err != nil { ctx.t.Fatalf("unable to create "+ "transaction: %v", err) } return tx, nil }, err: "already spent by transaction", }, { // A transaction cannot replace another if doing so // would cause more than 100 transactions being // replaced. name: "exceeds maximum conflicts", setup: func(ctx *testContext) (*btcutil.Tx, []*btcutil.Tx) { const numDescendants = 100 coinbaseOuts := make( []spendableOutput, numDescendants, ) for i := 0; i < numDescendants; i++ { tx := ctx.addCoinbaseTx(1) coinbaseOuts[i] = txOutToSpendableOut(tx, 0) } parent := ctx.addSignedTx( coinbaseOuts, numDescendants, defaultFee, true, false, ) // We'll then spend each output of the parent // transaction with a distinct transaction. for i := uint32(0); i < numDescendants; i++ { out := txOutToSpendableOut(parent, i) outs := []spendableOutput{out} ctx.addSignedTx( outs, 1, defaultFee, false, false, ) } // We'll then create a replacement transaction // by spending one of the coinbase outputs. // Replacing the original spender of the // coinbase output would evict the maximum // number of transactions from the mempool, // however, so we should reject it. tx, err := ctx.harness.CreateSignedTx( coinbaseOuts[:1], 1, defaultFee, false, ) if err != nil { ctx.t.Fatalf("unable to create "+ "transaction: %v", err) } return tx, nil }, err: "evicts more transactions than permitted", }, { // A transaction cannot replace another if the // replacement ends up spending an output that belongs // to one of the transactions it replaces. name: "replacement spends parent transaction", setup: func(ctx *testContext) (*btcutil.Tx, []*btcutil.Tx) { coinbase := ctx.addCoinbaseTx(1) // Create a transaction that spends the coinbase // output and signals replacement. coinbaseOut := txOutToSpendableOut(coinbase, 0) outs := []spendableOutput{coinbaseOut} parent := ctx.addSignedTx( outs, 1, defaultFee, true, false, ) // Attempting to create another transaction that // spends it, but also replaces it, should be // invalid. parentOut := txOutToSpendableOut(parent, 0) outs = []spendableOutput{coinbaseOut, parentOut} tx, err := ctx.harness.CreateSignedTx( outs, 2, defaultFee, false, ) if err != nil { ctx.t.Fatalf("unable to create "+ "transaction: %v", err) } return tx, nil }, err: "spends parent transaction", }, { // A transaction cannot replace another if it has a // lower fee rate than any of the transactions it // intends to replace. name: "insufficient fee rate", setup: func(ctx *testContext) (*btcutil.Tx, []*btcutil.Tx) { coinbase1 := ctx.addCoinbaseTx(1) coinbase2 := ctx.addCoinbaseTx(1) // We'll create two transactions that each spend // one of the coinbase outputs. The first will // have a higher fee rate than the second. coinbaseOut1 := txOutToSpendableOut(coinbase1, 0) outs := []spendableOutput{coinbaseOut1} ctx.addSignedTx(outs, 1, defaultFee*2, true, false) coinbaseOut2 := txOutToSpendableOut(coinbase2, 0) outs = []spendableOutput{coinbaseOut2} ctx.addSignedTx(outs, 1, defaultFee, true, false) // We'll then create the replacement transaction // by spending the coinbase outputs. It will be // an invalid one however, since it won't have a // higher fee rate than the first transaction. outs = []spendableOutput{coinbaseOut1, coinbaseOut2} tx, err := ctx.harness.CreateSignedTx( outs, 1, defaultFee*2, false, ) if err != nil { ctx.t.Fatalf("unable to create "+ "transaction: %v", err) } return tx, nil }, err: "insufficient fee rate", }, { // A transaction cannot replace another if it doesn't // have an absolute greater than the transactions its // replacing _plus_ the replacement transaction's // minimum relay fee. name: "insufficient absolute fee", setup: func(ctx *testContext) (*btcutil.Tx, []*btcutil.Tx) { coinbase := ctx.addCoinbaseTx(1) // We'll create a transaction with two outputs // and the default fee. coinbaseOut := txOutToSpendableOut(coinbase, 0) outs := []spendableOutput{coinbaseOut} ctx.addSignedTx(outs, 2, defaultFee, true, false) // We'll create a replacement transaction with // one output, which should cause the // transaction's absolute fee to be lower than // the above's, so it'll be invalid. tx, err := ctx.harness.CreateSignedTx( outs, 1, defaultFee, false, ) if err != nil { ctx.t.Fatalf("unable to create "+ "transaction: %v", err) } return tx, nil }, err: "insufficient absolute fee", }, { // A transaction cannot replace another if it introduces // a new unconfirmed input that was not already in any // of the transactions it's directly replacing. name: "spends new unconfirmed input", setup: func(ctx *testContext) (*btcutil.Tx, []*btcutil.Tx) { coinbase1 := ctx.addCoinbaseTx(1) coinbase2 := ctx.addCoinbaseTx(1) // We'll create two unconfirmed transactions // from our coinbase transactions. coinbaseOut1 := txOutToSpendableOut(coinbase1, 0) outs := []spendableOutput{coinbaseOut1} ctx.addSignedTx(outs, 1, defaultFee, true, false) coinbaseOut2 := txOutToSpendableOut(coinbase2, 0) outs = []spendableOutput{coinbaseOut2} newTx := ctx.addSignedTx( outs, 1, defaultFee, false, false, ) // We should not be able to accept a replacement // transaction that spends an unconfirmed input // that was not previously included. newTxOut := txOutToSpendableOut(newTx, 0) outs = []spendableOutput{coinbaseOut1, newTxOut} tx, err := ctx.harness.CreateSignedTx( outs, 1, defaultFee*2, false, ) if err != nil { ctx.t.Fatalf("unable to create "+ "transaction: %v", err) } return tx, nil }, err: "spends new unconfirmed input", }, { // A transaction can replace another with a higher fee. name: "higher fee", setup: func(ctx *testContext) (*btcutil.Tx, []*btcutil.Tx) { coinbase := ctx.addCoinbaseTx(1) // Create a transaction that we'll directly // replace. coinbaseOut := txOutToSpendableOut(coinbase, 0) outs := []spendableOutput{coinbaseOut} parent := ctx.addSignedTx( outs, 1, defaultFee, true, false, ) // Spend the parent transaction to create a // descendant that will be indirectly replaced. parentOut := txOutToSpendableOut(parent, 0) outs = []spendableOutput{parentOut} child := ctx.addSignedTx( outs, 1, defaultFee, false, false, ) // The replacement transaction should replace // both transactions above since it has a higher // fee and doesn't violate any other conditions // within the RBF policy. outs = []spendableOutput{coinbaseOut} tx, err := ctx.harness.CreateSignedTx( outs, 1, defaultFee*3, false, ) if err != nil { ctx.t.Fatalf("unable to create "+ "transaction: %v", err) } return tx, []*btcutil.Tx{parent, child} }, err: "", }, { // A transaction that doesn't signal replacement, can // be replaced if the parent signals replacement. name: "inherited replacement", setup: func(ctx *testContext) (*btcutil.Tx, []*btcutil.Tx) { coinbase := ctx.addCoinbaseTx(1) // Create an initial parent transaction that // marks replacement, we won't be replacing // this directly however. coinbaseOut := txOutToSpendableOut(coinbase, 0) outs := []spendableOutput{coinbaseOut} parent := ctx.addSignedTx( outs, 1, defaultFee, true, false, ) // Now create a transaction that spends that // parent transaction, which is marked as NOT // being RBF-able. parentOut := txOutToSpendableOut(parent, 0) parentOuts := []spendableOutput{parentOut} childNoReplace := ctx.addSignedTx( parentOuts, 1, defaultFee, false, false, ) // Now we'll create another transaction that // replaces the *child* only. This should work // as the parent has been marked for RBF, even // though the child hasn't. respendOuts := []spendableOutput{parentOut} childReplace, err := ctx.harness.CreateSignedTx( respendOuts, 1, defaultFee*3, false, ) if err != nil { ctx.t.Fatalf("unable to create child tx: %v", err) } return childReplace, []*btcutil.Tx{childNoReplace} }, err: "", }, } for _, testCase := range testCases { success := t.Run(testCase.name, func(t *testing.T) { // We'll start each test by creating our mempool // harness. harness, _, err := newPoolHarness(&chaincfg.MainNetParams) if err != nil { t.Fatalf("unable to create test pool: %v", err) } // We'll enable relay priority to ensure we can properly // test fees between replacement transactions and the // transactions it replaces. harness.txPool.cfg.Policy.DisableRelayPriority = false // Each test includes a setup method, which will set up // its required dependencies. The transaction returned // is the intended replacement, which should replace the // expected list of transactions. ctx := &testContext{t, harness} replacementTx, replacedTxs := testCase.setup(ctx) // Attempt to process the replacement transaction. If // it's not a valid one, we should see the error // expected by the test. _, err = ctx.harness.txPool.ProcessTransaction( replacementTx, false, false, 0, ) if testCase.err == "" && err != nil { ctx.t.Fatalf("expected no error when "+ "processing replacement transaction, "+ "got: %v", err) } if testCase.err != "" && err == nil { ctx.t.Fatalf("expected error when processing "+ "replacement transaction: %v", testCase.err) } if testCase.err != "" && err != nil { if !strings.Contains(err.Error(), testCase.err) { ctx.t.Fatalf("expected error: %v\n"+ "got: %v", testCase.err, err) } } // If the replacement transaction is valid, we'll check // that it has been included in the mempool and its // conflicts have been removed. Otherwise, the conflicts // should remain in the mempool. valid := testCase.err == "" for _, tx := range replacedTxs { testPoolMembership(ctx, tx, false, !valid) } testPoolMembership(ctx, replacementTx, false, valid) }) if !success { break } } }