kaspad/mempool/mempool_test.go
Ori Newman b797436884
[NOD-1127] Implement transaction propagation (#803)
* [NOD-1128] Add all flows to a directory names flows

* [NOD-1128] Make everything in protocol package a manager method

* [NOD-1128] Add AddTransaction mechanism to protocol manager

* [NOD-1128] Add mempool related flows

* [NOD-1128] Add mempool related flows

* [NOD-1128] Add mempool related flows

* [NOD-1127] Fix router message types

* [NOD-1127] Inline updateQueues

* [NOD-1127] Rename acceptedTxs->transactionsAcceptedToMempool

* [NOD-1127] Add TODOs to notify transactions to RPC

* [NOD-1127] Fix comment

* [NOD-1127] Rename acceptedTxs->transactionsAcceptedToMempool

* [NOD-1127] Rename MsgTxInv->MsgInvTransaction

* [NOD-1127] Rename MsgTxInv.TXIDs->TxIDS

* [NOD-1127] Change flow name

* [NOD-1127] Call m.addTransactionRelayFlow

* [NOD-1127] Remove redundant line

* [NOD-1127] Use common.DefaultTimeout

* [NOD-1127] Return early if len(idsToRequest) == 0

* [NOD-1127] Add NewBlockHandler to IBD
2020-07-20 16:01:35 +03:00

1853 lines
63 KiB
Go

// 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 (
"bytes"
"fmt"
"math"
"reflect"
"runtime"
"strings"
"sync"
"testing"
"github.com/kaspanet/kaspad/util/mstime"
"github.com/pkg/errors"
"github.com/kaspanet/kaspad/util/subnetworkid"
"github.com/kaspanet/kaspad/util/testtools"
"github.com/kaspanet/kaspad/blockdag"
"github.com/kaspanet/kaspad/dagconfig"
"github.com/kaspanet/kaspad/mining"
"github.com/kaspanet/kaspad/txscript"
"github.com/kaspanet/kaspad/util"
"github.com/kaspanet/kaspad/util/daghash"
"github.com/kaspanet/kaspad/wire"
)
// fakeDAG is used by the pool harness to provide generated test utxos and
// a current faked blueScore to the pool callbacks. This, in turn, allows
// transactions to appear as though they are spending completely valid utxos.
type fakeDAG struct {
sync.RWMutex
currentBlueScore uint64
medianTimePast mstime.Time
}
// BlueScore returns the current blue score associated with the fake DAG
// instance.
func (s *fakeDAG) BlueScore() uint64 {
s.RLock()
defer s.RUnlock()
return s.currentBlueScore
}
// SetBlueScore sets the current blueScore associated with the fake DAG instance.
func (s *fakeDAG) SetBlueScore(blueScore uint64) {
s.Lock()
defer s.Unlock()
s.currentBlueScore = blueScore
}
// MedianTimePast returns the current median time past associated with the fake
// DAG instance.
func (s *fakeDAG) MedianTimePast() mstime.Time {
s.RLock()
defer s.RUnlock()
mtp := s.medianTimePast
return mtp
}
// SetMedianTimePast sets the current median time past associated with the fake
// DAG instance.
func (s *fakeDAG) SetMedianTimePast(mtp mstime.Time) {
s.Lock()
defer s.Unlock()
s.medianTimePast = mtp
}
func calcSequenceLock(tx *util.Tx,
utxoSet blockdag.UTXOSet) (*blockdag.SequenceLock, error) {
return &blockdag.SequenceLock{
Milliseconds: -1,
BlockBlueScore: -1,
}, nil
}
// spendableOutpoint is a convenience type that houses a particular utxo and the
// amount associated with it.
type spendableOutpoint struct {
outpoint wire.Outpoint
amount util.Amount
}
// txOutToSpendableOutpoint returns a spendable outpoint given a transaction and index
// of the output to use. This is useful as a convenience when creating test
// transactions.
func txOutToSpendableOutpoint(tx *util.Tx, outputNum uint32) spendableOutpoint {
return spendableOutpoint{
outpoint: wire.Outpoint{TxID: *tx.ID(), Index: outputNum},
amount: util.Amount(tx.MsgTx().TxOut[outputNum].Value),
}
}
// poolHarness provides a harness that includes functionality for creating and
// signing transactions as well as a fake DAG that provides utxos for use in
// generating valid transactions.
type poolHarness struct {
signatureScript []byte
payScript []byte
dagParams *dagconfig.Params
dag *fakeDAG
txPool *TxPool
}
// txRelayFeeForTest defines a convenient relay fee amount to pay for test transactions
var txRelayFeeForTest = util.Amount(calcMinRequiredTxRelayFee(1000, DefaultMinRelayTxFee))
// CreateCoinbaseTx returns a coinbase transaction with the requested number of
// outputs paying an appropriate subsidy based on the passed block blue score 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(blueScore uint64, numOutputs uint32) (*util.Tx, error) {
// Create standard coinbase script.
extraNonce := int64(0)
coinbaseScript, err := txscript.NewScriptBuilder().
AddInt64(extraNonce).Script()
if err != nil {
return nil, err
}
txIns := []*wire.TxIn{{
// Coinbase transactions have no inputs, so previous outpoint is
// zero hash and max index.
PreviousOutpoint: *wire.NewOutpoint(&daghash.TxID{},
wire.MaxPrevOutIndex),
SignatureScript: coinbaseScript,
Sequence: wire.MaxTxInSequenceNum,
}}
txOuts := []*wire.TxOut{}
totalInput := blockdag.CalcBlockSubsidy(blueScore, p.dagParams)
amountPerOutput := totalInput / uint64(numOutputs)
remainder := totalInput - amountPerOutput*uint64(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
}
txOuts = append(txOuts, &wire.TxOut{
ScriptPubKey: p.payScript,
Value: amount,
})
}
return util.NewTx(wire.NewNativeMsgTx(wire.TxVersion, txIns, txOuts)), nil
}
// CreateSignedTxForSubnetwork 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) CreateSignedTxForSubnetwork(inputs []spendableOutpoint, numOutputs uint32, subnetworkID *subnetworkid.SubnetworkID, gas uint64) (*util.Tx, error) {
// Calculate the total input amount and split it amongst the requested
// number of outputs.
var totalInput util.Amount
for _, input := range inputs {
totalInput += input.amount
}
totalInput -= txRelayFeeForTest
amountPerOutput := uint64(totalInput) / uint64(numOutputs)
remainder := uint64(totalInput) - amountPerOutput*uint64(numOutputs)
var txIns []*wire.TxIn
for _, input := range inputs {
txIns = append(txIns, &wire.TxIn{
PreviousOutpoint: input.outpoint,
SignatureScript: p.signatureScript,
Sequence: wire.MaxTxInSequenceNum,
})
}
var txOuts []*wire.TxOut
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
}
txOuts = append(txOuts, &wire.TxOut{
ScriptPubKey: p.payScript,
Value: amount,
})
}
tx := wire.NewSubnetworkMsgTx(wire.TxVersion, txIns, txOuts, subnetworkID, gas, []byte{})
// Sign the new transaction.
for i := range tx.TxIn {
tx.TxIn[i].SignatureScript = p.signatureScript
}
return util.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 []spendableOutpoint, numOutputs uint32) (*util.Tx, error) {
return p.CreateSignedTxForSubnetwork(inputs, numOutputs, subnetworkid.SubnetworkIDNative, 0)
}
// 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 spendableOutpoint, numTxns uint32) ([]*util.Tx, error) {
txChain := make([]*util.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 (minus fees) to the payment address
// associated with the harness.
spendableAmount = spendableAmount - txRelayFeeForTest
txIn := &wire.TxIn{
PreviousOutpoint: prevOutpoint,
SignatureScript: p.signatureScript,
Sequence: wire.MaxTxInSequenceNum,
}
txOut := &wire.TxOut{
ScriptPubKey: p.payScript,
Value: uint64(spendableAmount),
}
tx := wire.NewNativeMsgTx(wire.TxVersion, []*wire.TxIn{txIn}, []*wire.TxOut{txOut})
txChain = append(txChain, util.NewTx(tx))
// Next transaction uses outputs from this one.
prevOutpoint = wire.Outpoint{TxID: *tx.TxID(), Index: 0}
}
return txChain, nil
}
func (tc *testContext) mineTransactions(transactions []*util.Tx, numberOfBlocks uint64) []spendableOutpoint {
var outpoints []spendableOutpoint
msgTxs := make([]*wire.MsgTx, len(transactions))
for i, tx := range transactions {
msgTxs[i] = tx.MsgTx()
}
for i := uint64(0); i < numberOfBlocks; i++ {
var blockTxs []*wire.MsgTx
if i == 0 {
blockTxs = msgTxs
}
block, err := mining.PrepareBlockForTest(tc.harness.txPool.cfg.DAG, tc.harness.txPool.cfg.DAG.TipHashes(), blockTxs, false)
if err != nil {
tc.t.Fatalf("PrepareBlockForTest: %s", err)
}
utilBlock := util.NewBlock(block)
isOrphan, isDelayed, err := tc.harness.txPool.cfg.DAG.ProcessBlock(utilBlock, blockdag.BFNoPoWCheck)
if err != nil {
tc.t.Fatalf("ProcessBlock: %s", err)
}
if isDelayed {
tc.t.Fatalf("ProcessBlock: block %s "+
"is too far in the future", block.BlockHash())
}
if isOrphan {
tc.t.Fatalf("ProcessBlock incorrectly returned that block %s "+
"is an orphan", block.BlockHash())
}
// Handle new block by pool
ch := make(chan NewBlockMsg)
go func() {
err = tc.harness.txPool.HandleNewBlockOld(utilBlock, ch)
close(ch)
}()
// process messages pushed by HandleNewBlockOld
for range ch {
}
// ensure that HandleNewBlockOld has not failed
if err != nil {
tc.t.Fatalf("HandleNewBlockOld failed to handle block %s", err)
}
coinbaseTx := block.Transactions[util.CoinbaseTransactionIndex]
for i, txOut := range coinbaseTx.TxOut {
outpoints = append(outpoints, spendableOutpoint{
outpoint: *wire.NewOutpoint(coinbaseTx.TxID(), uint32(i)),
amount: util.Amount(txOut.Value),
})
}
}
return outpoints
}
// newPoolHarness returns a new instance of a pool harness initialized with a
// fake DAG and a TxPool bound to it that is configured with a policy suitable
// for testing. Also, the fake DAG is populated with the returned spendable
// outputs so the caller can easily create new valid transactions which build
// off of it.
func newPoolHarness(t *testing.T, dagParams *dagconfig.Params, numOutputs uint32, dbName string) (*testContext, []spendableOutpoint, func(), error) {
scriptPubKey, err := txscript.PayToScriptHashScript(blockdag.OpTrueScript)
if err != nil {
return nil, nil, nil, err
}
params := *dagParams
params.BlockCoinbaseMaturity = 0
// Create a new database and DAG instance to run tests against.
dag, teardownFunc, err := blockdag.DAGSetup(dbName, true, blockdag.Config{
DAGParams: &params,
})
if err != nil {
return nil, nil, nil, errors.Errorf("Failed to setup DAG instance: %v", err)
}
defer func() {
if err != nil {
teardownFunc()
}
}()
signatureScript, err := txscript.PayToScriptHashSignatureScript(blockdag.OpTrueScript, nil)
if err != nil {
return nil, nil, nil, errors.Errorf("Failed to build harness signature script: %s", err)
}
// Create a new fake DAG and harness bound to it.
fDAG := &fakeDAG{}
harness := &poolHarness{
signatureScript: signatureScript,
payScript: scriptPubKey,
dagParams: &params,
dag: fDAG,
txPool: New(&Config{
DAG: dag,
Policy: Policy{
MaxOrphanTxs: 5,
MaxOrphanTxSize: 1000,
MinRelayTxFee: 1000, // 1 sompi per byte
MaxTxVersion: 1,
},
CalcSequenceLockNoLock: calcSequenceLock,
SigCache: nil,
}),
}
tc := &testContext{harness: harness, t: t}
// Mine numOutputs blocks to get numOutputs coinbase outpoints
outpoints := tc.mineTransactions(nil, uint64(numOutputs))
curHeight := harness.dag.BlueScore()
if params.BlockCoinbaseMaturity != 0 {
harness.dag.SetBlueScore(params.BlockCoinbaseMaturity + curHeight)
} else {
harness.dag.SetBlueScore(curHeight + 1)
}
harness.dag.SetMedianTimePast(mstime.Now())
return tc, outpoints, teardownFunc, 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
}
// 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 *util.Tx, inOrphanPool, inTxPool bool, isDepends bool) {
txID := tx.ID()
gotOrphanPool := tc.harness.txPool.IsOrphanInPool(txID)
if inOrphanPool != gotOrphanPool {
_, file, line, _ := runtime.Caller(1)
tc.t.Fatalf("%s:%d -- IsOrphanInPool: want %v, got %v", file,
line, inOrphanPool, gotOrphanPool)
}
gotTxPool := tc.harness.txPool.IsTransactionInPool(txID)
if inTxPool != gotTxPool {
_, file, line, _ := runtime.Caller(1)
tc.t.Fatalf("%s:%d -- IsTransactionInPool: want %v, got %v",
file, line, inTxPool, gotTxPool)
}
gotIsDepends := tc.harness.txPool.IsInDependPool(txID)
if isDepends != gotIsDepends {
_, file, line, _ := runtime.Caller(1)
tc.t.Fatalf("%s:%d -- IsInDependPool: want %v, got %v",
file, line, isDepends, gotIsDepends)
}
gotHaveTx := tc.harness.txPool.HaveTransaction(txID)
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)
}
count := tc.harness.txPool.Count()
txIDs := tc.harness.txPool.TxIDs()
txDescs := tc.harness.txPool.TxDescs()
txMiningDescs := tc.harness.txPool.MiningDescs()
if count != len(txIDs) || count != len(txDescs) || count != len(txMiningDescs) {
tc.t.Error("mempool.TxIDs(), mempool.TxDescs() and mempool.MiningDescs() have different length")
}
if inTxPool && !isDepends {
wasFound := false
for _, txI := range txIDs {
if *txID == *txI {
wasFound = true
break
}
}
if !wasFound {
tc.t.Error("Can not find transaction in mempool.TxIDs")
}
wasFound = false
for _, txd := range txDescs {
if *txID == *txd.Tx.ID() {
wasFound = true
break
}
}
if !wasFound {
tc.t.Error("Can not find transaction in mempool.TxDescs")
}
wasFound = false
for _, txd := range txMiningDescs {
if *txID == *txd.Tx.ID() {
wasFound = true
break
}
}
if !wasFound {
tc.t.Error("Can not find transaction in mempool.MiningDescs")
}
}
}
func (p *poolHarness) createTx(outpoint spendableOutpoint, fee uint64, numOutputs int64) (*util.Tx, error) {
txIns := []*wire.TxIn{{
PreviousOutpoint: outpoint.outpoint,
SignatureScript: nil,
Sequence: wire.MaxTxInSequenceNum,
}}
var txOuts []*wire.TxOut
amountPerOutput := (uint64(outpoint.amount) - fee) / uint64(numOutputs)
for i := int64(0); i < numOutputs; i++ {
txOuts = append(txOuts, &wire.TxOut{
ScriptPubKey: p.payScript,
Value: amountPerOutput,
})
}
tx := wire.NewNativeMsgTx(wire.TxVersion, txIns, txOuts)
// Sign the new transaction.
tx.TxIn[0].SignatureScript = p.signatureScript
return util.NewTx(tx), nil
}
func TestProcessTransaction(t *testing.T) {
params := dagconfig.SimnetParams
params.BlockCoinbaseMaturity = 0
tc, spendableOuts, teardownFunc, err := newPoolHarness(t, &params, 6, "TestProcessTransaction")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.harness
// Checks that a transaction cannot be added to the transaction pool if it's already there
tx, err := harness.createTx(spendableOuts[0], uint64(txRelayFeeForTest), 1)
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
_, err = harness.txPool.ProcessTransaction(tx, true, 0)
if err != nil {
t.Errorf("ProcessTransaction: unexpected error: %v", err)
}
_, err = harness.txPool.ProcessTransaction(tx, true, 0)
if err == nil {
t.Errorf("ProcessTransaction: expected an error, not nil")
}
if code, _ := extractRejectCode(err); code != wire.RejectDuplicate {
t.Errorf("Unexpected error code. Expected %v but got %v", wire.RejectDuplicate, code)
}
orphanedTx, err := harness.CreateSignedTx([]spendableOutpoint{{
amount: util.Amount(5000000000),
outpoint: wire.Outpoint{TxID: daghash.TxID{}, Index: 1},
}}, 1)
if err != nil {
t.Fatalf("unable to create signed tx: %v", err)
}
// Checks that an orphaned transaction cannot be
// added to the orphan pool if MaxOrphanTxs is 0
harness.txPool.cfg.Policy.MaxOrphanTxs = 0
_, err = harness.txPool.ProcessTransaction(orphanedTx, true, 0)
if err != nil {
t.Errorf("ProcessTransaction: unexpected error: %v", err)
}
testPoolMembership(tc, orphanedTx, false, false, false)
harness.txPool.cfg.Policy.MaxOrphanTxs = 5
_, err = harness.txPool.ProcessTransaction(orphanedTx, true, 0)
if err != nil {
t.Errorf("ProcessTransaction: unexpected error: %v", err)
}
// Checks that an orphaned transaction cannot be
// added to the orphan pool if it's already there
_, err = harness.txPool.ProcessTransaction(tx, true, 0)
if err == nil {
t.Errorf("ProcessTransaction: expected an error, not nil")
}
if code, _ := extractRejectCode(err); code != wire.RejectDuplicate {
t.Errorf("Unexpected error code. Expected %v but got %v", wire.RejectDuplicate, code)
}
// Checks that a coinbase transaction cannot be added to the mempool
currentBlueScore := harness.dag.BlueScore()
coinbase, err := harness.CreateCoinbaseTx(currentBlueScore+1, 1)
if err != nil {
t.Errorf("CreateCoinbaseTx: %v", err)
}
_, err = harness.txPool.ProcessTransaction(coinbase, true, 0)
if err == nil {
t.Errorf("ProcessTransaction: expected an error, not nil")
}
if code, _ := extractRejectCode(err); code != wire.RejectInvalid {
t.Errorf("Unexpected error code. Expected %v but got %v", wire.RejectInvalid, code)
}
// Checks that a transaction is rejected from the mempool if its
// size is above 50KB, and its fee is below the minimum relay fee
tooBigTx, err := harness.createTx(spendableOuts[1], 0, 1000000) // A transaction with 2000 outputs, in order to make it bigger than 50kb
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
_, err = harness.txPool.ProcessTransaction(tooBigTx, true, 0)
if err == nil {
t.Errorf("ProcessTransaction: expected an error, not nil")
}
if code, _ := extractRejectCode(err); code != wire.RejectInvalid {
t.Errorf("Unexpected error code. Expected %v but got %v", wire.RejectInsufficientFee, code)
}
expectedErrStr := "serialized transaction is too big"
if !strings.Contains(err.Error(), expectedErrStr) {
t.Fatalf("ProcessBlock expected error \"%v\" but got \"%v\"", expectedErrStr, err)
}
// Checks that non standard transactions are rejected from the mempool
nonStdTx, err := harness.createTx(spendableOuts[0], 0, 1)
if err != nil {
t.Fatalf("Unexpected error from harness.createTx: %s", err)
}
nonStdTx.MsgTx().Version = wire.TxVersion + 1
_, err = harness.txPool.ProcessTransaction(nonStdTx, true, 0)
if err == nil {
t.Errorf("ProcessTransaction: expected an error, not nil")
}
if code, _ := extractRejectCode(err); code != wire.RejectNonstandard {
t.Errorf("Unexpected error code. Expected %v but got %v", wire.RejectNonstandard, code)
}
// Checks that a transaction is rejected from the mempool if its
// size is above 50KB, and its fee is below the minimum relay fee
bigLowFeeTx, err := harness.createTx(spendableOuts[1], 0, 2000) // A transaction with 2000 outputs, in order to make it bigger than 50kb
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
_, err = harness.txPool.ProcessTransaction(bigLowFeeTx, true, 0)
if err == nil {
t.Errorf("ProcessTransaction: expected an error, not nil")
}
if code, _ := extractRejectCode(err); code != wire.RejectInsufficientFee {
t.Errorf("Unexpected error code. Expected %v but got %v", wire.RejectInsufficientFee, code)
}
// Checks that if a ps2h sigscript has more sigops then maxStandardP2SHSigOps, it gets rejected
// maxStandardP2SHSigOps is 15, so 16 OpCheckSig will make it a non standard script
nonStdSigScript, err := txscript.NewScriptBuilder().
AddOp(txscript.OpCheckSig).
AddOp(txscript.OpCheckSig).
AddOp(txscript.OpCheckSig).
AddOp(txscript.OpCheckSig).
AddOp(txscript.OpCheckSig).
AddOp(txscript.OpCheckSig).
AddOp(txscript.OpCheckSig).
AddOp(txscript.OpCheckSig).
AddOp(txscript.OpCheckSig).
AddOp(txscript.OpCheckSig).
AddOp(txscript.OpCheckSig).
AddOp(txscript.OpCheckSig).
AddOp(txscript.OpCheckSig).
AddOp(txscript.OpCheckSig).
AddOp(txscript.OpCheckSig).
AddOp(txscript.OpCheckSig).
Script()
if err != nil {
t.Fatalf("Script: error creating nonStdSigScript: %v", err)
}
p2shScriptPubKey, err := txscript.NewScriptBuilder().
AddOp(txscript.OpHash160).
AddData(util.Hash160(nonStdSigScript)).
AddOp(txscript.OpEqual).
Script()
if err != nil {
t.Fatalf("Script: error creating p2shScriptPubKey: %v", err)
}
wrappedP2SHNonStdSigScript, err := txscript.NewScriptBuilder().AddData(nonStdSigScript).Script()
if err != nil {
t.Fatalf("Script: error creating wrappedP2shNonSigScript: %v", err)
}
dummyPrevOutTxID, err := daghash.NewTxIDFromStr("01")
if err != nil {
t.Fatalf("NewShaHashFromStr: unexpected error: %v", err)
}
dummyPrevOut := wire.Outpoint{TxID: *dummyPrevOutTxID, Index: 1}
dummySigScript := bytes.Repeat([]byte{0x00}, 65)
addrHash := [20]byte{0x01}
addr, err := util.NewAddressPubKeyHash(addrHash[:],
util.Bech32PrefixKaspaTest)
if err != nil {
t.Fatalf("NewAddressPubKeyHash: unexpected error: %v", err)
}
dummyScriptPubKey, err := txscript.PayToAddrScript(addr)
if err != nil {
t.Fatalf("PayToAddrScript: unexpected error: %v", err)
}
p2shTx := util.NewTx(wire.NewNativeMsgTx(1, nil, []*wire.TxOut{{Value: 5000000000, ScriptPubKey: p2shScriptPubKey}}))
if isAccepted, err := harness.txPool.mpUTXOSet.AddTx(p2shTx.MsgTx(), currentBlueScore+1); err != nil {
t.Fatalf("AddTx unexpectedly failed. Error: %s", err)
} else if !isAccepted {
t.Fatalf("AddTx unexpectedly didn't add tx %s", p2shTx.ID())
}
txIns := []*wire.TxIn{{
PreviousOutpoint: wire.Outpoint{TxID: *p2shTx.ID(), Index: 0},
SignatureScript: wrappedP2SHNonStdSigScript,
Sequence: wire.MaxTxInSequenceNum,
}}
txOuts := []*wire.TxOut{{
Value: 5000000000,
ScriptPubKey: dummyScriptPubKey,
}}
nonStdSigScriptTx := util.NewTx(wire.NewNativeMsgTx(1, txIns, txOuts))
_, err = harness.txPool.ProcessTransaction(nonStdSigScriptTx, true, 0)
if err == nil {
t.Errorf("ProcessTransaction: expected an error, not nil")
}
if code, _ := extractRejectCode(err); code != wire.RejectNonstandard {
t.Errorf("Unexpected error code. Expected %v but got %v", wire.RejectNonstandard, code)
}
expectedErrStr = fmt.Sprintf("transaction %v has a non-standard input: "+
"transaction input #%d has "+
"%d signature operations which is more "+
"than the allowed max amount of %d",
nonStdSigScriptTx.ID(), 0, 16, 15)
if expectedErrStr != err.Error() {
t.Errorf("Unexpected error message. Expected \"%s\" but got \"%s\"", expectedErrStr, err.Error())
}
// Checks that a transaction with no outputs will not get rejected
noOutsTx := util.NewTx(wire.NewNativeMsgTx(1, []*wire.TxIn{{
PreviousOutpoint: dummyPrevOut,
SignatureScript: dummySigScript,
Sequence: wire.MaxTxInSequenceNum,
}},
nil))
_, err = harness.txPool.ProcessTransaction(noOutsTx, true, 0)
if err != nil {
t.Errorf("ProcessTransaction: %v", err)
}
// Checks that transactions get rejected from mempool if sequence lock is not active
harness.txPool.cfg.CalcSequenceLockNoLock = func(tx *util.Tx,
view blockdag.UTXOSet) (*blockdag.SequenceLock, error) {
return &blockdag.SequenceLock{
Milliseconds: math.MaxInt64,
BlockBlueScore: math.MaxInt64,
}, nil
}
tx, err = harness.createTx(spendableOuts[2], 0, 1)
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
_, err = harness.txPool.ProcessTransaction(tx, true, 0)
if err == nil {
t.Errorf("ProcessTransaction: expected an error, not nil")
}
if code, _ := extractRejectCode(err); code != wire.RejectNonstandard {
t.Errorf("Unexpected error code. Expected %v but got %v", wire.RejectNonstandard, code)
}
expectedErrStr = "transaction's sequence locks on inputs not met"
if err.Error() != expectedErrStr {
t.Errorf("Unexpected error message. Expected \"%s\" but got \"%s\"", expectedErrStr, err.Error())
}
harness.txPool.cfg.CalcSequenceLockNoLock = calcSequenceLock
// Transaction should be rejected from mempool because it has low fee, and its priority is above mining.MinHighPriority
tx, err = harness.createTx(spendableOuts[4], 0, 1000)
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
_, err = harness.txPool.ProcessTransaction(tx, true, 0)
if err == nil {
t.Errorf("ProcessTransaction: expected an error, not nil")
}
if code, _ := extractRejectCode(err); code != wire.RejectInsufficientFee {
t.Errorf("Unexpected error code. Expected %v but got %v", wire.RejectInsufficientFee, code)
}
txIns = []*wire.TxIn{{
PreviousOutpoint: spendableOuts[5].outpoint,
SignatureScript: []byte{02, 01}, // Unparsable script
Sequence: wire.MaxTxInSequenceNum,
}}
txOuts = []*wire.TxOut{{
Value: 1,
ScriptPubKey: dummyScriptPubKey,
}}
tx = util.NewTx(wire.NewNativeMsgTx(1, txIns, txOuts))
_, err = harness.txPool.ProcessTransaction(tx, true, 0)
fmt.Println(err)
if err == nil {
t.Errorf("ProcessTransaction: expected an error, not nil")
}
if code, _ := extractRejectCode(err); code != wire.RejectNonstandard {
t.Errorf("Unexpected error code. Expected %v but got %v", wire.RejectNonstandard, code)
}
}
func TestDoubleSpends(t *testing.T) {
tc, spendableOuts, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 2, "TestDoubleSpends")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.harness
// Add two transactions to the mempool
tx1, err := harness.createTx(spendableOuts[0], uint64(txRelayFeeForTest), 1)
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
_, err = harness.txPool.ProcessTransaction(tx1, true, 0)
if err != nil {
t.Fatalf("ProcessTransaction: %s", err)
}
tx2, err := harness.createTx(spendableOuts[1], uint64(txRelayFeeForTest)+1, 1)
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
_, err = harness.txPool.ProcessTransaction(tx2, true, 0)
if err != nil {
t.Fatalf("ProcessTransaction: %s", err)
}
testPoolMembership(tc, tx1, false, true, false)
testPoolMembership(tc, tx2, false, true, false)
// Spends the same outpoint as tx2
tx3, err := harness.createTx(spendableOuts[0], uint64(txRelayFeeForTest)+2, 1) // We put here different fee to create different transaction hash
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
// First we try to add it to the mempool and see it rejected
_, err = harness.txPool.ProcessTransaction(tx3, true, 0)
if err == nil {
t.Errorf("ProcessTransaction expected an error, not nil")
}
if code, _ := extractRejectCode(err); code != wire.RejectDuplicate {
t.Errorf("Unexpected error code. Expected %v but got %v", wire.RejectDuplicate, code)
}
testPoolMembership(tc, tx3, false, false, false)
// Then we assume tx3 is already in the DAG, so we need to remove
// transactions that spends the same outpoints from the mempool
harness.txPool.RemoveDoubleSpends(tx3)
// Ensures that only the transaction that double spends the same
// funds as tx3 is removed, and the other one remains unaffected
testPoolMembership(tc, tx1, false, false, false)
testPoolMembership(tc, tx2, false, true, false)
}
func TestDoubleSpendsFromDAG(t *testing.T) {
tc, spendableOuts, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 2, "TestDoubleSpendsFromDAG")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.harness
tx, err := harness.createTx(spendableOuts[0], uint64(txRelayFeeForTest), 1)
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
dag := harness.txPool.cfg.DAG
blockdag.PrepareAndProcessBlockForTest(t, dag, dag.TipHashes(), []*wire.MsgTx{tx.MsgTx()})
// Check that a transaction that double spends the DAG UTXO set is orphaned.
doubleSpendTx, err := harness.createTx(spendableOuts[0], uint64(txRelayFeeForTest), 2)
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
_, err = harness.txPool.ProcessTransaction(doubleSpendTx, true, 0)
if err != nil {
t.Fatalf("ProcessTransaction: %s", err)
}
testPoolMembership(tc, doubleSpendTx, true, false, false)
// If you send a transaction that some of its outputs exist in the DAG UTXO
// set, it won't be added to the orphan pool, and will completely get rejected
// from the mempool.
// This happens because transactions with the same ID as old transactions
// are not allowed as long as some of the old transaction outputs exist
// in the UTXO.
_, err = harness.txPool.ProcessTransaction(tx, true, 0)
var ruleErr RuleError
if ok := errors.As(err, &ruleErr); ok {
var txRuleErr TxRuleError
if ok := errors.As(ruleErr.Err, &txRuleErr); ok {
if txRuleErr.RejectCode != wire.RejectDuplicate {
t.Errorf("ProcessTransaction expected an %v reject code but got %v", wire.RejectDuplicate, txRuleErr.RejectCode)
}
} else {
t.Errorf("ProcessTransaction expected a ruleErr.Err to be a TxRuleError but got %v", err)
}
} else {
t.Errorf("ProcessTransaction expected a RuleError but got %v", err)
}
testPoolMembership(tc, tx, false, false, false)
}
// TestFetchTransaction checks that FetchTransaction
// returns only transaction from the main pool and not from the orphan pool
func TestFetchTransaction(t *testing.T) {
tc, spendableOuts, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 1, "TestFetchTransaction")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.harness
orphanedTx, err := harness.CreateSignedTx([]spendableOutpoint{{
amount: util.Amount(5000000000),
outpoint: wire.Outpoint{TxID: daghash.TxID{1}, Index: 1},
}}, 1)
if err != nil {
t.Fatalf("unable to create signed tx: %v", err)
}
harness.txPool.ProcessTransaction(orphanedTx, true, 0)
testPoolMembership(tc, orphanedTx, true, false, false)
fetchedorphanedTx, err := harness.txPool.FetchTransaction(orphanedTx.ID())
if fetchedorphanedTx != nil {
t.Fatalf("FetchTransaction: expected fetchedorphanedTx to be nil")
}
if err == nil {
t.Errorf("FetchTransaction: expected an error, not nil")
}
tx, err := harness.createTx(spendableOuts[0], uint64(txRelayFeeForTest), 1)
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
harness.txPool.ProcessTransaction(tx, true, 0)
testPoolMembership(tc, tx, false, true, false)
fetchedTx, err := harness.txPool.FetchTransaction(tx.ID())
if !reflect.DeepEqual(fetchedTx, tx) {
t.Fatalf("FetchTransaction: returned a transaction, but not the right one")
}
if err != nil {
t.Errorf("FetchTransaction: unexpected error: %v", err)
}
}
// 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) {
tc, spendableOuts, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 1, "TestSimpleOrphanChain")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.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, 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, 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, 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, txD.Tx != chainedTxns[0])
}
}
// TestOrphanReject ensures that orphans are properly rejected when the allow
// orphans flag is not set on ProcessTransaction.
func TestOrphanReject(t *testing.T) {
tc, outputs, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 1, "TestOrphanReject")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.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, 0)
if err == nil {
t.Fatalf("ProcessTransaction: did not fail on orphan "+
"%v when allow orphans flag is false", tx.ID())
}
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, false)
}
}
// TestOrphanExpiration checks every time we add an orphan transaction
// it will check if we are beyond nextExpireScan, and if so, it will remove
// all expired orphan transactions
func TestOrphanExpiration(t *testing.T) {
tc, _, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 1, "TestOrphanExpiration")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.harness
expiredTx, err := harness.CreateSignedTx([]spendableOutpoint{{
amount: util.Amount(5000000000),
outpoint: wire.Outpoint{TxID: daghash.TxID{}, Index: 0},
}}, 1)
if err != nil {
t.Fatalf("Unexpected error on harness.CreateSignedTx: %s", err)
}
_, err = harness.txPool.ProcessTransaction(expiredTx, true, 0)
if err != nil {
t.Fatalf("Unexpected error on harness.ProcessTransaction: %s", err)
}
harness.txPool.orphans[*expiredTx.ID()].expiration = mstime.UnixMilliseconds(0)
tx1, err := harness.CreateSignedTx([]spendableOutpoint{{
amount: util.Amount(5000000000),
outpoint: wire.Outpoint{TxID: daghash.TxID{1}, Index: 0},
}}, 1)
if err != nil {
t.Fatalf("Unexpected error on harness.CreateSignedTx: %s", err)
}
_, err = harness.txPool.ProcessTransaction(tx1, true, 0)
if err != nil {
t.Fatalf("Unexpected error on harness.ProcessTransaction: %s", err)
}
// First check that expired orphan transactions are not removed before nextExpireScan
testPoolMembership(tc, tx1, true, false, false)
testPoolMembership(tc, expiredTx, true, false, false)
// Force nextExpireScan to be in the past
harness.txPool.nextExpireScan = mstime.UnixMilliseconds(0)
tx2, err := harness.CreateSignedTx([]spendableOutpoint{{
amount: util.Amount(5000000000),
outpoint: wire.Outpoint{TxID: daghash.TxID{2}, Index: 0},
}}, 1)
if err != nil {
t.Fatalf("Unexpected error on harness.CreateSignedTx: %s", err)
}
_, err = harness.txPool.ProcessTransaction(tx2, true, 0)
if err != nil {
t.Fatalf("Unexpected error on harness.ProcessTransaction: %s", err)
}
// Check that only expired orphan transactions are removed
testPoolMembership(tc, tx1, true, false, false)
testPoolMembership(tc, tx2, true, false, false)
testPoolMembership(tc, expiredTx, false, false, false)
}
// TestMaxOrphanTxSize ensures that a transaction that is
// bigger than MaxOrphanTxSize will get rejected
func TestMaxOrphanTxSize(t *testing.T) {
tc, _, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 1, "TestMaxOrphanTxSize")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.harness
harness.txPool.cfg.Policy.MaxOrphanTxSize = 1
tx, err := harness.CreateSignedTx([]spendableOutpoint{{
amount: util.Amount(5000000000),
outpoint: wire.Outpoint{TxID: daghash.TxID{}, Index: 0},
}}, 1)
if err != nil {
t.Fatalf("unable to create signed tx: %v", err)
}
harness.txPool.ProcessTransaction(tx, true, 0)
testPoolMembership(tc, tx, false, false, false)
harness.txPool.cfg.Policy.MaxOrphanTxSize = math.MaxInt32
harness.txPool.ProcessTransaction(tx, true, 0)
testPoolMembership(tc, tx, true, false, false)
}
func TestRemoveTransaction(t *testing.T) {
tc, outputs, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 2, "TestRemoveTransaction")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.harness
chainedTxns, err := harness.CreateTxChain(outputs[0], 5)
if err != nil {
t.Fatalf("unable to create transaction chain: %v", err)
}
for i, tx := range chainedTxns {
_, err := harness.txPool.ProcessTransaction(tx, true, 0)
if err != nil {
t.Fatalf("ProcessTransaction: %v", err)
}
testPoolMembership(tc, tx, false, true, i != 0)
}
// Checks that when removeRedeemers is false, the specified transaction is the only transaction that gets removed
tc.mineTransactions(chainedTxns[:1], 1)
testPoolMembership(tc, chainedTxns[0], false, false, false)
testPoolMembership(tc, chainedTxns[1], false, true, false)
testPoolMembership(tc, chainedTxns[2], false, true, true)
testPoolMembership(tc, chainedTxns[3], false, true, true)
testPoolMembership(tc, chainedTxns[4], false, true, true)
// Checks that when removeRedeemers is true, all of the transaction that are dependent on it get removed
err = harness.txPool.RemoveTransaction(chainedTxns[1], true, true)
if err != nil {
t.Fatalf("RemoveTransaction: %v", err)
}
testPoolMembership(tc, chainedTxns[1], false, false, false)
testPoolMembership(tc, chainedTxns[2], false, false, false)
testPoolMembership(tc, chainedTxns[3], false, false, false)
testPoolMembership(tc, chainedTxns[4], false, 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) {
tc, outputs, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 1, "TestOrphanEviction")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.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, 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, false)
}
// Figure out which transactions were evicted and make sure the number
// evicted matches the expected number.
var evictedTxns []*util.Tx
for _, tx := range chainedTxns[1:] {
if !harness.txPool.IsOrphanInPool(tx.ID()) {
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, false)
}
}
// Attempt to remove orphans by tag,
// and ensure the state of all other orphans are unaffected.
func TestRemoveOrphansByTag(t *testing.T) {
tc, _, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 1, "TestRemoveOrphansByTag")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.harness
orphanedTx1, err := harness.CreateSignedTx([]spendableOutpoint{{
amount: util.Amount(5000000000),
outpoint: wire.Outpoint{TxID: daghash.TxID{1}, Index: 1},
}}, 1)
if err != nil {
t.Fatalf("unable to create signed tx: %v", err)
}
harness.txPool.ProcessTransaction(orphanedTx1, true, 1)
orphanedTx2, err := harness.CreateSignedTx([]spendableOutpoint{{
amount: util.Amount(5000000000),
outpoint: wire.Outpoint{TxID: daghash.TxID{2}, Index: 2},
}}, 1)
if err != nil {
t.Fatalf("unable to create signed tx: %v", err)
}
harness.txPool.ProcessTransaction(orphanedTx2, true, 1)
orphanedTx3, err := harness.CreateSignedTx([]spendableOutpoint{{
amount: util.Amount(5000000000),
outpoint: wire.Outpoint{TxID: daghash.TxID{3}, Index: 3},
}}, 1)
if err != nil {
t.Fatalf("unable to create signed tx: %v", err)
}
harness.txPool.ProcessTransaction(orphanedTx3, true, 1)
orphanedTx4, err := harness.CreateSignedTx([]spendableOutpoint{{
amount: util.Amount(5000000000),
outpoint: wire.Outpoint{TxID: daghash.TxID{4}, Index: 4},
}}, 1)
if err != nil {
t.Fatalf("unable to create signed tx: %v", err)
}
harness.txPool.ProcessTransaction(orphanedTx4, true, 2)
harness.txPool.RemoveOrphansByTag(1)
testPoolMembership(tc, orphanedTx1, false, false, false)
testPoolMembership(tc, orphanedTx2, false, false, false)
testPoolMembership(tc, orphanedTx3, false, false, false)
testPoolMembership(tc, orphanedTx4, true, 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) {
const maxOrphans = 4
tc, spendableOuts, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 1, "TestBasicOrphanRemoval")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.harness
harness.txPool.cfg.Policy.MaxOrphanTxs = maxOrphans
// 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, 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, 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([]spendableOutpoint{{
amount: util.Amount(5000000000),
outpoint: wire.Outpoint{TxID: daghash.TxID{}, Index: 0},
}}, 1)
if err != nil {
t.Fatalf("unable to create signed tx: %v", err)
}
harness.txPool.RemoveOrphan(nonChainedOrphanTx)
testPoolMembership(tc, nonChainedOrphanTx, false, false, false)
for _, tx := range chainedTxns[1 : maxOrphans+1] {
testPoolMembership(tc, tx, true, false, 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, false)
for _, tx := range chainedTxns[1 : maxOrphans+1] {
testPoolMembership(tc, tx, true, false, 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, false)
}
}
// TestOrphanChainRemoval ensure that orphan chains (orphans that spend outputs
// from other orphans) are removed as expected.
func TestOrphanChainRemoval(t *testing.T) {
const maxOrphans = 10
tc, spendableOuts, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 1, "TestOrphanChainRemoval")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.harness
harness.txPool.cfg.Policy.MaxOrphanTxs = maxOrphans
// 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, 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, 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.
func() {
harness.txPool.mtx.Lock()
defer harness.txPool.mtx.Unlock()
harness.txPool.removeOrphan(chainedTxns[1], false)
}()
testPoolMembership(tc, chainedTxns[1], false, false, false)
for _, tx := range chainedTxns[2 : maxOrphans+1] {
testPoolMembership(tc, tx, true, false, false)
}
// Remove the first remaining orphan that starts the orphan chain with
// the remove redeemer flag set and ensure they are all removed.
func() {
harness.txPool.mtx.Lock()
defer harness.txPool.mtx.Unlock()
harness.txPool.removeOrphan(chainedTxns[2], true)
}()
for _, tx := range chainedTxns[2 : maxOrphans+1] {
testPoolMembership(tc, tx, false, 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) {
const maxOrphans = 4
tc, outputs, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 1, "TestMultiInputOrphanDoubleSpend")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.harness
harness.txPool.cfg.Policy.MaxOrphanTxs = maxOrphans
// 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, 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, 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([]spendableOutpoint{
txOutToSpendableOutpoint(chainedTxns[1], 0),
txOutToSpendableOutpoint(chainedTxns[maxOrphans], 0),
}, 1)
if err != nil {
t.Fatalf("unable to create signed tx: %v", err)
}
acceptedTxns, err := harness.txPool.ProcessTransaction(doubleSpendTx, true, 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, 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, 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, txD.Tx != chainedTxns[0])
}
// 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, false)
}
// TestCheckSpend tests that CheckSpend returns the expected spends found in
// the mempool.
func TestCheckSpend(t *testing.T) {
tc, outputs, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 1, "TestCheckSpend")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.harness
// 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, 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{
TxID: *chainedTxns[i].ID(),
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{
TxID: *chainedTxns[txChainLength-1].ID(),
Index: 0,
}
spend = harness.txPool.CheckSpend(op)
if spend != nil {
t.Fatalf("Unexpeced spend found in pool: %v", spend)
}
}
func TestCount(t *testing.T) {
tc, outputs, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 1, "TestCount")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.harness
if harness.txPool.Count() != 0 {
t.Errorf("TestCount: txPool should be initialized with 0 transactions")
}
chainedTxns, err := harness.CreateTxChain(outputs[0], 3)
if err != nil {
t.Fatalf("harness.CreateTxChain: unexpected error: %v", err)
}
for i, tx := range chainedTxns {
_, err = harness.txPool.ProcessTransaction(tx, true, 0)
if err != nil {
t.Errorf("ProcessTransaction: unexpected error: %v", err)
}
if harness.txPool.Count()+harness.txPool.DepCount() != i+1 {
t.Errorf("TestCount: txPool expected to have %v transactions but got %v", i+1, harness.txPool.Count())
}
}
err = harness.txPool.RemoveTransaction(chainedTxns[0], false, false)
if err != nil {
t.Fatalf("harness.CreateTxChain: unexpected error: %v", err)
}
if harness.txPool.Count()+harness.txPool.DepCount() != 2 {
t.Errorf("TestCount: txPool expected to have 2 transactions but got %v", harness.txPool.Count())
}
}
func TestExtractRejectCode(t *testing.T) {
tests := []struct {
blockdagRuleErrorCode blockdag.ErrorCode
wireRejectCode wire.RejectCode
}{
{
blockdagRuleErrorCode: blockdag.ErrDuplicateBlock,
wireRejectCode: wire.RejectDuplicate,
},
{
blockdagRuleErrorCode: blockdag.ErrBlockVersionTooOld,
wireRejectCode: wire.RejectObsolete,
},
{
blockdagRuleErrorCode: blockdag.ErrFinalityPointTimeTooOld,
wireRejectCode: wire.RejectFinality,
},
{
blockdagRuleErrorCode: blockdag.ErrDifficultyTooLow,
wireRejectCode: wire.RejectDifficulty,
},
{
blockdagRuleErrorCode: math.MaxUint32,
wireRejectCode: wire.RejectInvalid,
},
}
for _, test := range tests {
err := blockdag.RuleError{ErrorCode: test.blockdagRuleErrorCode}
code, ok := extractRejectCode(err)
if !ok {
t.Errorf("TestExtractRejectCode: %v could not be extracted", test.blockdagRuleErrorCode)
}
if test.wireRejectCode != code {
t.Errorf("TestExtractRejectCode: expected %v to extract %v but got %v", test.blockdagRuleErrorCode, test.wireRejectCode, code)
}
}
txRuleError := TxRuleError{RejectCode: wire.RejectDust}
txExtractedCode, ok := extractRejectCode(txRuleError)
if !ok {
t.Errorf("TestExtractRejectCode: %v could not be extracted", txRuleError)
}
if txExtractedCode != wire.RejectDust {
t.Errorf("TestExtractRejectCode: expected %v to extract %v but got %v", wire.RejectDust, wire.RejectDust, txExtractedCode)
}
var nilErr error
nilErrExtractedCode, ok := extractRejectCode(nilErr)
if nilErrExtractedCode != wire.RejectInvalid {
t.Errorf("TestExtractRejectCode: expected %v to extract %v but got %v", wire.RejectInvalid, wire.RejectInvalid, nilErrExtractedCode)
}
if ok {
t.Errorf("TestExtractRejectCode: a nil error is expected to return false but got %v", ok)
}
nonRuleError := errors.New("nonRuleError")
fErrExtractedCode, ok := extractRejectCode(nonRuleError)
if fErrExtractedCode != wire.RejectInvalid {
t.Errorf("TestExtractRejectCode: expected %v to extract %v but got %v", wire.RejectInvalid, wire.RejectInvalid, nilErrExtractedCode)
}
if ok {
t.Errorf("TestExtractRejectCode: a nonRuleError is expected to return false but got %v", ok)
}
}
// TestHandleNewBlock
func TestHandleNewBlock(t *testing.T) {
tc, spendableOuts, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 2, "TestHandleNewBlock")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.harness
// Create parent transaction for orphan transaction below
blockTx1, err := harness.CreateSignedTx(spendableOuts[:1], 1)
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
// Create orphan transaction and add it to UTXO set
txID := blockTx1.ID()
orphanTx, err := harness.CreateSignedTx([]spendableOutpoint{{
amount: util.Amount(2500000000 - txRelayFeeForTest),
outpoint: wire.Outpoint{TxID: *txID, Index: 0},
}}, 1)
if err != nil {
t.Fatalf("unable to create signed tx: %v", err)
}
_, err = harness.txPool.ProcessTransaction(orphanTx, true, 0)
if err != nil {
t.Fatalf("ProcessTransaction: unexpected error: %v", err)
}
// ensure that transaction added to orphan pool
testPoolMembership(tc, orphanTx, true, false, false)
// Add one more transaction to block
blockTx2, err := harness.CreateSignedTx(spendableOuts[1:], 1)
if err != nil {
t.Fatalf("unable to create transaction 1: %v", err)
}
dummyBlock.Transactions = append(dummyBlock.Transactions, blockTx1.MsgTx(), blockTx2.MsgTx())
// Create block and add its transactions to UTXO set
block := util.NewBlock(&dummyBlock)
for i, tx := range block.Transactions() {
if isAccepted, err := harness.txPool.mpUTXOSet.AddTx(tx.MsgTx(), 1); err != nil {
t.Fatalf("Failed to add transaction (%v,%v) to UTXO set: %v", i, tx.ID(), err)
} else if !isAccepted {
t.Fatalf("AddTx unexpectedly didn't add tx %s", tx.ID())
}
}
// Handle new block by pool
ch := make(chan NewBlockMsg)
go func() {
err = harness.txPool.HandleNewBlockOld(block, ch)
close(ch)
}()
// process messages pushed by HandleNewBlockOld
blockTransactions := make(map[daghash.TxID]int)
for msg := range ch {
blockTransactions[*msg.Tx.ID()] = 1
if *msg.Tx.ID() != *blockTx1.ID() {
if len(msg.AcceptedTxs) != 0 {
t.Fatalf("Expected amount of accepted transactions 0. Got: %v", len(msg.AcceptedTxs))
}
} else {
if len(msg.AcceptedTxs) != 1 {
t.Fatalf("Wrong accepted transactions length")
}
if *msg.AcceptedTxs[0].Tx.ID() != *orphanTx.ID() {
t.Fatalf("Wrong accepted transaction ID")
}
}
}
// ensure that HandleNewBlockOld has not failed
if err != nil {
t.Fatalf("HandleNewBlockOld failed to handle block %v", err)
}
// Validate messages pushed by HandleNewBlockOld into the channel
if len(blockTransactions) != 2 {
t.Fatalf("Wrong size of blockTransactions after new block handling")
}
if _, ok := blockTransactions[*blockTx1.ID()]; !ok {
t.Fatalf("Transaction 1 of new block is not handled")
}
if _, ok := blockTransactions[*blockTx2.ID()]; !ok {
t.Fatalf("Transaction 2 of new block is not handled")
}
// ensure that orphan transaction moved to main pool
testPoolMembership(tc, orphanTx, false, true, false)
}
// dummyBlock defines a block on the block DAG. It is used to test block operations.
var dummyBlock = wire.MsgBlock{
Header: wire.BlockHeader{
Version: 1,
ParentHashes: []*daghash.Hash{
{
0x82, 0xdc, 0xbd, 0xe6, 0x88, 0x37, 0x74, 0x5b,
0x78, 0x6b, 0x03, 0x1d, 0xa3, 0x48, 0x3c, 0x45,
0x3f, 0xc3, 0x2e, 0xd4, 0x53, 0x5b, 0x6f, 0x26,
0x26, 0xb0, 0x48, 0x4f, 0x09, 0x00, 0x00, 0x00,
}, // Mainnet genesis
{
0xc1, 0x5b, 0x71, 0xfe, 0x20, 0x70, 0x0f, 0xd0,
0x08, 0x49, 0x88, 0x1b, 0x32, 0xb5, 0xbd, 0x13,
0x17, 0xbe, 0x75, 0xe7, 0x29, 0x46, 0xdd, 0x03,
0x01, 0x92, 0x90, 0xf1, 0xca, 0x8a, 0x88, 0x11,
}}, // Simnet genesis
HashMerkleRoot: &daghash.Hash{
0x66, 0x57, 0xa9, 0x25, 0x2a, 0xac, 0xd5, 0xc0,
0xb2, 0x94, 0x09, 0x96, 0xec, 0xff, 0x95, 0x22,
0x28, 0xc3, 0x06, 0x7c, 0xc3, 0x8d, 0x48, 0x85,
0xef, 0xb5, 0xa4, 0xac, 0x42, 0x47, 0xe9, 0xf3,
}, // f3e94742aca4b5ef85488dc37c06c3282295ffec960994b2c0d5ac2a25a95766
Timestamp: mstime.UnixMilliseconds(1529483563000), // 2018-06-20 08:32:43 +0000 UTC
Bits: 0x1e00ffff, // 503382015
Nonce: 0x000ae53f, // 714047
},
Transactions: []*wire.MsgTx{
{
Version: 1,
TxIn: []*wire.TxIn{},
TxOut: []*wire.TxOut{
{
Value: 0x12a05f200, // 5000000000
ScriptPubKey: []byte{
0xa9, 0x14, 0xda, 0x17, 0x45, 0xe9, 0xb5, 0x49,
0xbd, 0x0b, 0xfa, 0x1a, 0x56, 0x99, 0x71, 0xc7,
0x7e, 0xba, 0x30, 0xcd, 0x5a, 0x4b, 0x87,
},
},
},
LockTime: 0,
SubnetworkID: *subnetworkid.SubnetworkIDCoinbase,
Payload: []byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
},
PayloadHash: &daghash.Hash{
0x14, 0x06, 0xe0, 0x58, 0x81, 0xe2, 0x99, 0x36,
0x77, 0x66, 0xd3, 0x13, 0xe2, 0x6c, 0x05, 0x56,
0x4e, 0xc9, 0x1b, 0xf7, 0x21, 0xd3, 0x17, 0x26,
0xbd, 0x6e, 0x46, 0xe6, 0x06, 0x89, 0x53, 0x9a,
},
},
},
}
func TestTransactionGas(t *testing.T) {
params := dagconfig.SimnetParams
params.BlockCoinbaseMaturity = 0
params.EnableNonNativeSubnetworks = true
tc, spendableOuts, teardownFunc, err := newPoolHarness(t, &params, 6, "TestTransactionGas")
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
defer teardownFunc()
harness := tc.harness
const gasLimit = 10000
subnetworkID, err := testtools.RegisterSubnetworkForTest(harness.txPool.cfg.DAG, &params, gasLimit)
if err != nil {
t.Fatalf("unable to register network: %v", err)
}
// Create valid transaction
tx, err := harness.CreateSignedTxForSubnetwork(spendableOuts[:1], 1, subnetworkID, gasLimit)
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
_, err = harness.txPool.ProcessTransaction(tx, true, 0)
if err != nil {
t.Errorf("ProcessTransaction: unexpected error: %v", err)
}
// Create invalid transaction
tx, err = harness.CreateSignedTxForSubnetwork(spendableOuts[1:], 1, subnetworkID, gasLimit+1)
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
_, err = harness.txPool.ProcessTransaction(tx, true, 0)
if err == nil {
t.Error("ProcessTransaction did not return error, expecting ErrInvalidGas")
}
}