From 95d0a371d9ee14856e66c9f216ee460b772be06e Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Thu, 2 May 2019 19:44:51 -0700 Subject: [PATCH] mempool: implement RBF signaling policy --- config.go | 1 + mempool/mempool.go | 342 +++++++++++++- mempool/mempool_test.go | 972 +++++++++++++++++++++++++++++++++++++++- server.go | 1 + 4 files changed, 1287 insertions(+), 29 deletions(-) diff --git a/config.go b/config.go index 5934f6bc..3bcc65da 100644 --- a/config.go +++ b/config.go @@ -165,6 +165,7 @@ type config struct { DropAddrIndex bool `long:"dropaddrindex" description:"Deletes the address-based transaction index from the database on start up and then exits."` RelayNonStd bool `long:"relaynonstd" description:"Relay non-standard transactions regardless of the default settings for the active network."` RejectNonStd bool `long:"rejectnonstd" description:"Reject non-standard transactions regardless of the default settings for the active network."` + RejectReplacement bool `long:"rejectreplacement" description:"Reject transactions that attempt to replace existing transactions within the mempool through the Replace-By-Fee (RBF) signaling policy."` lookup func(string) ([]net.IP, error) oniondial func(string, string, time.Duration) (net.Conn, error) dial func(string, string, time.Duration) (net.Conn, error) diff --git a/mempool/mempool.go b/mempool/mempool.go index 7bb70445..35eaf234 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -38,6 +38,16 @@ const ( // orphanExpireScanInterval is the minimum amount of time in between // scans of the orphan pool to evict expired transactions. orphanExpireScanInterval = time.Minute * 5 + + // MaxRBFSequence is the maximum sequence number an input can use to + // signal that the transaction spending it can be replaced using the + // Replace-By-Fee (RBF) policy. + MaxRBFSequence = 0xfffffffd + + // MaxReplacementEvictions is the maximum number of transactions that + // can be evicted from the mempool when accepting a transaction + // replacement. + MaxReplacementEvictions = 100 ) // Tag represents an identifier to use for tagging orphan transactions. The @@ -133,6 +143,11 @@ type Policy struct { // MinRelayTxFee defines the minimum transaction fee in BTC/kB to be // considered a non-zero fee. MinRelayTxFee btcutil.Amount + + // RejectReplacement, if true, rejects accepting replacement + // transactions using the Replace-By-Fee (RBF) signaling policy into + // the mempool. + RejectReplacement bool } // TxDesc is a descriptor containing a transaction in the mempool along with @@ -554,21 +569,200 @@ func (mp *TxPool) addTransaction(utxoView *blockchain.UtxoViewpoint, tx *btcutil // checkPoolDoubleSpend checks whether or not the passed transaction is // attempting to spend coins already spent by other transactions in the pool. -// Note it does not check for double spends against transactions already in the -// main chain. +// If it does, we'll check whether each of those transactions are signaling for +// replacement. If just one of them isn't, an error is returned. Otherwise, a +// boolean is returned signaling that the transaction is a replacement. Note it +// does not check for double spends against transactions already in the main +// chain. // // This function MUST be called with the mempool lock held (for reads). -func (mp *TxPool) checkPoolDoubleSpend(tx *btcutil.Tx) error { +func (mp *TxPool) checkPoolDoubleSpend(tx *btcutil.Tx) (bool, error) { + var isReplacement bool for _, txIn := range tx.MsgTx().TxIn { - if txR, exists := mp.outpoints[txIn.PreviousOutPoint]; exists { + conflict, ok := mp.outpoints[txIn.PreviousOutPoint] + if !ok { + continue + } + + // Reject the transaction if we don't accept replacement + // transactions or if it doesn't signal replacement. + if mp.cfg.Policy.RejectReplacement || + !mp.signalsReplacement(conflict, nil) { str := fmt.Sprintf("output %v already spent by "+ "transaction %v in the memory pool", - txIn.PreviousOutPoint, txR.Hash()) - return txRuleError(wire.RejectDuplicate, str) + txIn.PreviousOutPoint, conflict.Hash()) + return false, txRuleError(wire.RejectDuplicate, str) + } + + isReplacement = true + } + + return isReplacement, nil +} + +// signalsReplacement determines if a transaction is signaling that it can be +// replaced using the Replace-By-Fee (RBF) policy. This policy specifies two +// ways a transaction can signal that it is replaceable: +// +// Explicit signaling: A transaction is considered to have opted in to allowing +// replacement of itself if any of its inputs have a sequence number less than +// 0xfffffffe. +// +// Inherited signaling: Transactions that don't explicitly signal replaceability +// are replaceable under this policy for as long as any one of their ancestors +// signals replaceability and remains unconfirmed. +// +// The cache is optional and serves as an optimization to avoid visiting +// transactions we've already determined don't signal replacement. +// +// This function MUST be called with the mempool lock held (for reads). +func (mp *TxPool) signalsReplacement(tx *btcutil.Tx, + cache map[chainhash.Hash]struct{}) bool { + + // If a cache was not provided, we'll initialize one now to use for the + // recursive calls. + if cache == nil { + cache = make(map[chainhash.Hash]struct{}) + } + + for _, txIn := range tx.MsgTx().TxIn { + if txIn.Sequence <= MaxRBFSequence { + return true + } + + hash := txIn.PreviousOutPoint.Hash + unconfirmedAncestor, ok := mp.pool[hash] + if !ok { + continue + } + + // If we've already determined the transaction doesn't signal + // replacement, we can avoid visiting it again. + if _, ok := cache[hash]; ok { + continue + } + + if mp.signalsReplacement(unconfirmedAncestor.Tx, cache) { + return true + } + + // Since the transaction doesn't signal replacement, we'll cache + // its result to ensure we don't attempt to determine so again. + cache[hash] = struct{}{} + } + + return false +} + +// txAncestors returns all of the unconfirmed ancestors of the given +// transaction. Given transactions A, B, and C where C spends B and B spends A, +// A and B are considered ancestors of C. +// +// The cache is optional and serves as an optimization to avoid visiting +// transactions we've already determined ancestors of. +// +// This function MUST be called with the mempool lock held (for reads). +func (mp *TxPool) txAncestors(tx *btcutil.Tx, + cache map[chainhash.Hash]map[chainhash.Hash]*btcutil.Tx) map[chainhash.Hash]*btcutil.Tx { + + // If a cache was not provided, we'll initialize one now to use for the + // recursive calls. + if cache == nil { + cache = make(map[chainhash.Hash]map[chainhash.Hash]*btcutil.Tx) + } + + ancestors := make(map[chainhash.Hash]*btcutil.Tx) + for _, txIn := range tx.MsgTx().TxIn { + parent, ok := mp.pool[txIn.PreviousOutPoint.Hash] + if !ok { + continue + } + ancestors[*parent.Tx.Hash()] = parent.Tx + + // Determine if the ancestors of this ancestor have already been + // computed. If they haven't, we'll do so now and cache them to + // use them later on if necessary. + moreAncestors, ok := cache[*parent.Tx.Hash()] + if !ok { + moreAncestors = mp.txAncestors(parent.Tx, cache) + cache[*parent.Tx.Hash()] = moreAncestors + } + + for hash, ancestor := range moreAncestors { + ancestors[hash] = ancestor } } - return nil + return ancestors +} + +// txDescendants returns all of the unconfirmed descendants of the given +// transaction. Given transactions A, B, and C where C spends B and B spends A, +// B and C are considered descendants of A. A cache can be provided in order to +// easily retrieve the descendants of transactions we've already determined the +// descendants of. +// +// This function MUST be called with the mempool lock held (for reads). +func (mp *TxPool) txDescendants(tx *btcutil.Tx, + cache map[chainhash.Hash]map[chainhash.Hash]*btcutil.Tx) map[chainhash.Hash]*btcutil.Tx { + + // If a cache was not provided, we'll initialize one now to use for the + // recursive calls. + if cache == nil { + cache = make(map[chainhash.Hash]map[chainhash.Hash]*btcutil.Tx) + } + + // We'll go through all of the outputs of the transaction to determine + // if they are spent by any other mempool transactions. + descendants := make(map[chainhash.Hash]*btcutil.Tx) + op := wire.OutPoint{Hash: *tx.Hash()} + for i := range tx.MsgTx().TxOut { + op.Index = uint32(i) + descendant, ok := mp.outpoints[op] + if !ok { + continue + } + descendants[*descendant.Hash()] = descendant + + // Determine if the descendants of this descendant have already + // been computed. If they haven't, we'll do so now and cache + // them to use them later on if necessary. + moreDescendants, ok := cache[*descendant.Hash()] + if !ok { + moreDescendants = mp.txDescendants(descendant, cache) + cache[*descendant.Hash()] = moreDescendants + } + + for _, moreDescendant := range moreDescendants { + descendants[*moreDescendant.Hash()] = moreDescendant + } + } + + return descendants +} + +// txConflicts returns all of the unconfirmed transactions that would become +// conflicts if we were to accept the given transaction into the mempool. An +// unconfirmed conflict is known as a transaction that spends an output already +// spent by a different transaction within the mempool. Any descendants of these +// transactions are also considered conflicts as they would no longer exist. +// These are generally not allowed except for transactions that signal RBF +// support. +// +// This function MUST be called with the mempool lock held (for reads). +func (mp *TxPool) txConflicts(tx *btcutil.Tx) map[chainhash.Hash]*btcutil.Tx { + conflicts := make(map[chainhash.Hash]*btcutil.Tx) + for _, txIn := range tx.MsgTx().TxIn { + conflict, ok := mp.outpoints[txIn.PreviousOutPoint] + if !ok { + continue + } + conflicts[*conflict.Hash()] = conflict + for hash, descendant := range mp.txDescendants(conflict, nil) { + conflicts[hash] = descendant + } + } + return conflicts } // CheckSpend checks whether the passed outpoint is already spent by a @@ -631,6 +825,100 @@ func (mp *TxPool) FetchTransaction(txHash *chainhash.Hash) (*btcutil.Tx, error) return nil, fmt.Errorf("transaction is not in the pool") } +// validateReplacement determines whether a transaction is deemed as a valid +// replacement of all of its conflicts according to the RBF policy. If it is +// valid, no error is returned. Otherwise, an error is returned indicating what +// went wrong. +// +// This function MUST be called with the mempool lock held (for reads). +func (mp *TxPool) validateReplacement(tx *btcutil.Tx, + txFee int64) (map[chainhash.Hash]*btcutil.Tx, error) { + + // First, we'll make sure the set of conflicting transactions doesn't + // exceed the maximum allowed. + conflicts := mp.txConflicts(tx) + if len(conflicts) > MaxReplacementEvictions { + str := fmt.Sprintf("replacement transaction %v evicts more "+ + "transactions than permitted: max is %v, evicts %v", + tx.Hash(), MaxReplacementEvictions, len(conflicts)) + return nil, txRuleError(wire.RejectNonstandard, str) + } + + // The set of conflicts (transactions we'll replace) and ancestors + // should not overlap, otherwise the replacement would be spending an + // output that no longer exists. + for ancestorHash := range mp.txAncestors(tx, nil) { + if _, ok := conflicts[ancestorHash]; !ok { + continue + } + str := fmt.Sprintf("replacement transaction %v spends parent "+ + "transaction %v", tx.Hash(), ancestorHash) + return nil, txRuleError(wire.RejectInvalid, str) + } + + // The replacement should have a higher fee rate than each of the + // conflicting transactions and a higher absolute fee than the fee sum + // of all the conflicting transactions. + // + // We usually don't want to accept replacements with lower fee rates + // than what they replaced as that would lower the fee rate of the next + // block. Requiring that the fee rate always be increased is also an + // easy-to-reason about way to prevent DoS attacks via replacements. + var ( + txSize = GetTxVirtualSize(tx) + txFeeRate = txFee * 1000 / txSize + conflictsFee int64 + conflictsParents = make(map[chainhash.Hash]struct{}) + ) + for hash, conflict := range conflicts { + if txFeeRate <= mp.pool[hash].FeePerKB { + str := fmt.Sprintf("replacement transaction %v has an "+ + "insufficient fee rate: needs more than %v, "+ + "has %v", tx.Hash(), mp.pool[hash].FeePerKB, + txFeeRate) + return nil, txRuleError(wire.RejectInsufficientFee, str) + } + + conflictsFee += mp.pool[hash].Fee + + // We'll track each conflict's parents to ensure the replacement + // isn't spending any new unconfirmed inputs. + for _, txIn := range conflict.MsgTx().TxIn { + conflictsParents[txIn.PreviousOutPoint.Hash] = struct{}{} + } + } + + // It should also have an absolute fee greater than all of the + // transactions it intends to replace and pay for its own bandwidth, + // which is determined by our minimum relay fee. + minFee := calcMinRequiredTxRelayFee(txSize, mp.cfg.Policy.MinRelayTxFee) + if txFee < conflictsFee+minFee { + str := fmt.Sprintf("replacement transaction %v has an "+ + "insufficient absolute fee: needs %v, has %v", + tx.Hash(), conflictsFee+minFee, txFee) + return nil, txRuleError(wire.RejectInsufficientFee, str) + } + + // Finally, it should not spend any new unconfirmed outputs, other than + // the ones already included in the parents of the conflicting + // transactions it'll replace. + for _, txIn := range tx.MsgTx().TxIn { + if _, ok := conflictsParents[txIn.PreviousOutPoint.Hash]; ok { + continue + } + // Confirmed outputs are valid to spend in the replacement. + if _, ok := mp.pool[txIn.PreviousOutPoint.Hash]; !ok { + continue + } + str := fmt.Sprintf("replacement transaction spends new "+ + "unconfirmed input %v not found in conflicting "+ + "transactions", txIn.PreviousOutPoint) + return nil, txRuleError(wire.RejectInvalid, str) + } + + return conflicts, nil +} + // maybeAcceptTransaction is the internal function which implements the public // MaybeAcceptTransaction. See the comment for MaybeAcceptTransaction for // more details. @@ -714,13 +1002,14 @@ func (mp *TxPool) maybeAcceptTransaction(tx *btcutil.Tx, isNew, rateLimit, rejec // The transaction may not use any of the same outputs as other // transactions already in the pool as that would ultimately result in a - // double spend. This check is intended to be quick and therefore only - // detects double spends within the transaction pool itself. The - // transaction could still be double spending coins from the main chain - // at this point. There is a more in-depth check that happens later - // after fetching the referenced transaction inputs from the main chain - // which examines the actual spend data and prevents double spends. - err = mp.checkPoolDoubleSpend(tx) + // double spend, unless those transactions signal for RBF. This check is + // intended to be quick and therefore only detects double spends within + // the transaction pool itself. The transaction could still be double + // spending coins from the main chain at this point. There is a more + // in-depth check that happens later after fetching the referenced + // transaction inputs from the main chain which examines the actual + // spend data and prevents double spends. + isReplacement, err := mp.checkPoolDoubleSpend(tx) if err != nil { return nil, nil, err } @@ -899,6 +1188,16 @@ func (mp *TxPool) maybeAcceptTransaction(tx *btcutil.Tx, isNew, rateLimit, rejec mp.cfg.Policy.FreeTxRelayLimit*10*1000) } + // If the transaction has any conflicts and we've made it this far, then + // we're processing a potential replacement. + var conflicts map[chainhash.Hash]*btcutil.Tx + if isReplacement { + conflicts, err = mp.validateReplacement(tx, txFee) + if err != nil { + return nil, nil, err + } + } + // Verify crypto signatures for each input and reject the transaction if // any don't verify. err = blockchain.ValidateTransactionScripts(tx, utxoView, @@ -911,7 +1210,20 @@ func (mp *TxPool) maybeAcceptTransaction(tx *btcutil.Tx, isNew, rateLimit, rejec return nil, nil, err } - // Add to transaction pool. + // Now that we've deemed the transaction as valid, we can add it to the + // mempool. If it ended up replacing any transactions, we'll remove them + // first. + for _, conflict := range conflicts { + log.Debugf("Replacing transaction %v (fee_rate=%v sat/kb) "+ + "with %v (fee_rate=%v sat/kb)\n", conflict.Hash(), + mp.pool[*conflict.Hash()].FeePerKB, tx.Hash(), + txFee*1000/serializedSize) + + // The conflict set should already include the descendants for + // each one, so we don't need to remove the redeemers within + // this call as they'll be removed eventually. + mp.removeTransaction(conflict, false) + } txD := mp.addTransaction(utxoView, tx, bestHeight, txFee) log.Debugf("Accepted transaction %v (pool size: %v)", txHash, diff --git a/mempool/mempool_test.go b/mempool/mempool_test.go index 7a29598b..84aee10a 100644 --- a/mempool/mempool_test.go +++ b/mempool/mempool_test.go @@ -7,7 +7,7 @@ package mempool import ( "encoding/hex" "reflect" - "runtime" + "strings" "sync" "testing" "time" @@ -187,22 +187,30 @@ func (p *poolHarness) CreateCoinbaseTx(blockHeight int32, numOutputs uint32) (*b // 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) { +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: wire.MaxTxInSequenceNum, + Sequence: sequence, }) } for i := uint32(0); i < numOutputs; i++ { @@ -356,33 +364,89 @@ type testContext struct { 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 { - _, file, line, _ := runtime.Caller(1) - tc.t.Fatalf("%s:%d -- IsOrphanInPool: want %v, got %v", file, - line, inOrphanPool, gotOrphanPool) + tc.t.Fatalf("IsOrphanInPool: want %v, got %v", inOrphanPool, + gotOrphanPool) } gotTxPool := tc.harness.txPool.IsTransactionInPool(txHash) if inTxPool != gotTxPool { - _, file, line, _ := runtime.Caller(1) - tc.t.Fatalf("%s:%d -- IsTransactionInPool: want %v, got %v", - file, line, inTxPool, gotTxPool) + tc.t.Fatalf("IsTransactionInPool: want %v, got %v", inTxPool, + gotTxPool) } gotHaveTx := tc.harness.txPool.HaveTransaction(txHash) wantHaveTx := inOrphanPool || inTxPool if wantHaveTx != gotHaveTx { - _, file, line, _ := runtime.Caller(1) - tc.t.Fatalf("%s:%d -- HaveTransaction: want %v, got %v", file, - line, wantHaveTx, gotHaveTx) + tc.t.Fatalf("HaveTransaction: want %v, got %v", wantHaveTx, + gotHaveTx) } } @@ -618,7 +682,7 @@ func TestBasicOrphanRemoval(t *testing.T) { nonChainedOrphanTx, err := harness.CreateSignedTx([]spendableOutput{{ amount: btcutil.Amount(5000000000), outPoint: wire.OutPoint{Hash: chainhash.Hash{}, Index: 0}, - }}, 1) + }}, 1, 0, false) if err != nil { t.Fatalf("unable to create signed tx: %v", err) } @@ -754,7 +818,7 @@ func TestMultiInputOrphanDoubleSpend(t *testing.T) { doubleSpendTx, err := harness.CreateSignedTx([]spendableOutput{ txOutToSpendableOut(chainedTxns[1], 0), txOutToSpendableOut(chainedTxns[maxOrphans], 0), - }, 1) + }, 1, 0, false) if err != nil { t.Fatalf("unable to create signed tx: %v", err) } @@ -866,3 +930,883 @@ func TestCheckSpend(t *testing.T) { 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(): struct{}{}, *b.Hash(): struct{}{}, + *c.Hash(): struct{}{}, *d.Hash(): struct{}{}, + } + 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(): struct{}{}, *c.Hash(): struct{}{}, + *d.Hash(): struct{}{}, *e.Hash(): struct{}{}, + } + 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 + + 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: "", + }, + } + + 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 + } + } +} diff --git a/server.go b/server.go index d8fac256..bed4de95 100644 --- a/server.go +++ b/server.go @@ -2709,6 +2709,7 @@ func newServer(listenAddrs, agentBlacklist, agentWhitelist []string, MaxSigOpCostPerTx: blockchain.MaxBlockSigOpsCost / 4, MinRelayTxFee: cfg.minRelayTxFee, MaxTxVersion: 2, + RejectReplacement: cfg.RejectReplacement, }, ChainParams: chainParams, FetchUtxoView: s.chain.FetchUtxoView,