mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-03-30 15:08:33 +00:00

* [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
1853 lines
63 KiB
Go
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: ¶ms,
|
|
})
|
|
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: ¶ms,
|
|
|
|
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, ¶ms, 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, ¶ms, 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, ¶ms, 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")
|
|
}
|
|
}
|