btcd/rpcserver_test.go

498 lines
14 KiB
Go
Raw Normal View History

package main
import (
"encoding/hex"
"errors"
"testing"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/mempool"
"github.com/btcsuite/btcd/wire"
"github.com/stretchr/testify/require"
)
// TestHandleTestMempoolAcceptFailDecode checks that when invalid hex string is
// used as the raw txns, the corresponding error is returned.
func TestHandleTestMempoolAcceptFailDecode(t *testing.T) {
t.Parallel()
require := require.New(t)
// Create a testing server.
s := &rpcServer{}
testCases := []struct {
name string
txns []string
expectedErrCode btcjson.RPCErrorCode
}{
{
name: "hex decode fail",
txns: []string{"invalid"},
expectedErrCode: btcjson.ErrRPCDecodeHexString,
},
{
name: "tx decode fail",
txns: []string{"696e76616c6964"},
expectedErrCode: btcjson.ErrRPCDeserialization,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Create a request that uses invalid raw txns.
cmd := btcjson.NewTestMempoolAcceptCmd(tc.txns, 0)
// Call the method under test.
closeChan := make(chan struct{})
result, err := handleTestMempoolAccept(
s, cmd, closeChan,
)
// Ensure the expected error is returned.
require.Error(err)
rpcErr, ok := err.(*btcjson.RPCError)
require.True(ok)
require.Equal(tc.expectedErrCode, rpcErr.Code)
// No result should be returned.
require.Nil(result)
})
}
}
var (
// TODO(yy): make a `btctest` package and move these testing txns there
// so they be used in other tests.
//
// txHex1 is taken from `txscript/data/tx_valid.json`.
txHex1 = "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b" +
"49aa43ad90ba26000000000490047304402203f16c6f40162ab686621ef3" +
"000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507a" +
"c48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0" +
"140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271a" +
"d504b88ac00000000"
// txHex2 is taken from `txscript/data/tx_valid.json`.
txHex2 = "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b" +
"49aa43ad90ba260000000004a0048304402203f16c6f40162ab686621ef3" +
"000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507a" +
"c48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2bab01fffffff" +
"f0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c27" +
"1ad504b88ac00000000"
// txHex3 is taken from `txscript/data/tx_valid.json`.
txHex3 = "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b" +
"49aa43ad90ba260000000004a01ff47304402203f16c6f40162ab686621e" +
"f3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc350" +
"7ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01fffffff" +
"f0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c27" +
"1ad504b88ac00000000"
)
// decodeTxHex decodes the given hex string into a transaction.
func decodeTxHex(t *testing.T, txHex string) *btcutil.Tx {
rawBytes, err := hex.DecodeString(txHex)
require.NoError(t, err)
tx, err := btcutil.NewTxFromBytes(rawBytes)
require.NoError(t, err)
return tx
}
// TestHandleTestMempoolAcceptMixedResults checks that when different txns get
// different responses from calling the mempool method `CheckMempoolAcceptance`
// their results are correctly returned.
func TestHandleTestMempoolAcceptMixedResults(t *testing.T) {
t.Parallel()
require := require.New(t)
// Create a mock mempool.
mm := &mempool.MockTxMempool{}
// Create a testing server with the mock mempool.
s := &rpcServer{cfg: rpcserverConfig{
TxMemPool: mm,
}}
// Decode the hex so we can assert the mock mempool is called with it.
tx1 := decodeTxHex(t, txHex1)
tx2 := decodeTxHex(t, txHex2)
tx3 := decodeTxHex(t, txHex3)
// Create a slice to hold the expected results. We will use three txns
// so we expect threeresults.
expectedResults := make([]*btcjson.TestMempoolAcceptResult, 3)
// We now mock the first call to `CheckMempoolAcceptance` to return an
// error.
dummyErr := errors.New("dummy error")
mm.On("CheckMempoolAcceptance", tx1).Return(nil, dummyErr).Once()
// Since the call failed, we expect the first result to give us the
// error.
expectedResults[0] = &btcjson.TestMempoolAcceptResult{
Txid: tx1.Hash().String(),
Wtxid: tx1.WitnessHash().String(),
Allowed: false,
RejectReason: dummyErr.Error(),
}
// We mock the second call to `CheckMempoolAcceptance` to return a
// result saying the tx is missing inputs.
mm.On("CheckMempoolAcceptance", tx2).Return(
&mempool.MempoolAcceptResult{
MissingParents: []*chainhash.Hash{},
}, nil,
).Once()
// We expect the second result to give us the missing-inputs error.
expectedResults[1] = &btcjson.TestMempoolAcceptResult{
Txid: tx2.Hash().String(),
Wtxid: tx2.WitnessHash().String(),
Allowed: false,
RejectReason: "missing-inputs",
}
// We mock the third call to `CheckMempoolAcceptance` to return a
// result saying the tx allowed.
const feeSats = btcutil.Amount(1000)
mm.On("CheckMempoolAcceptance", tx3).Return(
&mempool.MempoolAcceptResult{
TxFee: feeSats,
TxSize: 100,
}, nil,
).Once()
// We expect the third result to give us the fee details.
expectedResults[2] = &btcjson.TestMempoolAcceptResult{
Txid: tx3.Hash().String(),
Wtxid: tx3.WitnessHash().String(),
Allowed: true,
Vsize: 100,
Fees: &btcjson.TestMempoolAcceptFees{
Base: feeSats.ToBTC(),
EffectiveFeeRate: feeSats.ToBTC() * 1e3 / 100,
},
}
// Create a mock request with default max fee rate of 0.1 BTC/KvB.
cmd := btcjson.NewTestMempoolAcceptCmd(
[]string{txHex1, txHex2, txHex3}, 0.1,
)
// Call the method handler and assert the expected results are
// returned.
closeChan := make(chan struct{})
results, err := handleTestMempoolAccept(s, cmd, closeChan)
require.NoError(err)
require.Equal(expectedResults, results)
// Assert the mocked method is called as expected.
mm.AssertExpectations(t)
}
// TestValidateFeeRate checks that `validateFeeRate` behaves as expected.
func TestValidateFeeRate(t *testing.T) {
t.Parallel()
const (
// testFeeRate is in BTC/kvB.
testFeeRate = 0.1
// testTxSize is in vb.
testTxSize = 100
// testFeeSats is in sats.
// We have 0.1BTC/kvB =
// 0.1 * 1e8 sats/kvB =
// 0.1 * 1e8 / 1e3 sats/vb = 0.1 * 1e5 sats/vb.
testFeeSats = btcutil.Amount(testFeeRate * 1e5 * testTxSize)
)
testCases := []struct {
name string
feeSats btcutil.Amount
txSize int64
maxFeeRate float64
expectedFees *btcjson.TestMempoolAcceptFees
allowed bool
}{
{
// When the fee rate(0.1) is above the max fee
// rate(0.01), we expect a nil result and false.
name: "fee rate above max",
feeSats: testFeeSats,
txSize: testTxSize,
maxFeeRate: testFeeRate / 10,
expectedFees: nil,
allowed: false,
},
{
// When the fee rate(0.1) is no greater than the max
// fee rate(0.1), we expect a result and true.
name: "fee rate below max",
feeSats: testFeeSats,
txSize: testTxSize,
maxFeeRate: testFeeRate,
expectedFees: &btcjson.TestMempoolAcceptFees{
Base: testFeeSats.ToBTC(),
EffectiveFeeRate: testFeeRate,
},
allowed: true,
},
{
// When the fee rate(1) is above the default max fee
// rate(0.1), we expect a nil result and false.
name: "fee rate above default max",
feeSats: testFeeSats,
txSize: testTxSize / 10,
expectedFees: nil,
allowed: false,
},
{
// When the fee rate(0.1) is no greater than the
// default max fee rate(0.1), we expect a result and
// true.
name: "fee rate below default max",
feeSats: testFeeSats,
txSize: testTxSize,
expectedFees: &btcjson.TestMempoolAcceptFees{
Base: testFeeSats.ToBTC(),
EffectiveFeeRate: testFeeRate,
},
allowed: true,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
result, allowed := validateFeeRate(
tc.feeSats, tc.txSize, tc.maxFeeRate,
)
require.Equal(tc.expectedFees, result)
require.Equal(tc.allowed, allowed)
})
}
}
// TestHandleTestMempoolAcceptFees checks that the `Fees` field is correctly
// populated based on the max fee rate and the tx being checked.
func TestHandleTestMempoolAcceptFees(t *testing.T) {
t.Parallel()
// Create a mock mempool.
mm := &mempool.MockTxMempool{}
// Create a testing server with the mock mempool.
s := &rpcServer{cfg: rpcserverConfig{
TxMemPool: mm,
}}
const (
// Set transaction's fee rate to be 0.2BTC/kvB.
feeRate = defaultMaxFeeRate * 2
// txSize is 100vb.
txSize = 100
// feeSats is 2e6 sats.
feeSats = feeRate * 1e8 * txSize / 1e3
)
testCases := []struct {
name string
maxFeeRate float64
txHex string
rejectReason string
allowed bool
}{
{
// When the fee rate(0.2) used by the tx is below the
// max fee rate(2) specified, the result should allow
// it.
name: "below max fee rate",
maxFeeRate: feeRate * 10,
txHex: txHex1,
allowed: true,
},
{
// When the fee rate(0.2) used by the tx is above the
// max fee rate(0.02) specified, the result should
// disallow it.
name: "above max fee rate",
maxFeeRate: feeRate / 10,
txHex: txHex1,
allowed: false,
rejectReason: "max-fee-exceeded",
},
{
// When the max fee rate is not set, the default
// 0.1BTC/kvB is used and the fee rate(0.2) used by the
// tx is above it, the result should disallow it.
name: "above default max fee rate",
txHex: txHex1,
allowed: false,
rejectReason: "max-fee-exceeded",
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
// Decode the hex so we can assert the mock mempool is
// called with it.
tx := decodeTxHex(t, txHex1)
// We mock the call to `CheckMempoolAcceptance` to
// return the result.
mm.On("CheckMempoolAcceptance", tx).Return(
&mempool.MempoolAcceptResult{
TxFee: feeSats,
TxSize: txSize,
}, nil,
).Once()
// We expect the third result to give us the fee
// details.
expected := &btcjson.TestMempoolAcceptResult{
Txid: tx.Hash().String(),
Wtxid: tx.WitnessHash().String(),
Allowed: tc.allowed,
}
if tc.allowed {
expected.Vsize = txSize
expected.Fees = &btcjson.TestMempoolAcceptFees{
Base: feeSats / 1e8,
EffectiveFeeRate: feeRate,
}
} else {
expected.RejectReason = tc.rejectReason
}
// Create a mock request with specified max fee rate.
cmd := btcjson.NewTestMempoolAcceptCmd(
[]string{txHex1}, tc.maxFeeRate,
)
// Call the method handler and assert the expected
// result is returned.
closeChan := make(chan struct{})
r, err := handleTestMempoolAccept(s, cmd, closeChan)
require.NoError(err)
// Check the interface type.
results, ok := r.([]*btcjson.TestMempoolAcceptResult)
require.True(ok)
// Expect exactly one result.
require.Len(results, 1)
// Check the result is returned as expected.
require.Equal(expected, results[0])
// Assert the mocked method is called as expected.
mm.AssertExpectations(t)
})
}
}
// TestGetTxSpendingPrevOut checks that handleGetTxSpendingPrevOut handles the
// cmd as expected.
func TestGetTxSpendingPrevOut(t *testing.T) {
t.Parallel()
require := require.New(t)
// Create a mock mempool.
mm := &mempool.MockTxMempool{}
defer mm.AssertExpectations(t)
// Create a testing server with the mock mempool.
s := &rpcServer{cfg: rpcserverConfig{
TxMemPool: mm,
}}
// First, check the error case.
//
// Create a request that will cause an error.
cmd := &btcjson.GetTxSpendingPrevOutCmd{
Outputs: []*btcjson.GetTxSpendingPrevOutCmdOutput{
{Txid: "invalid"},
},
}
// Call the method handler and assert the error is returned.
closeChan := make(chan struct{})
results, err := handleGetTxSpendingPrevOut(s, cmd, closeChan)
require.Error(err)
require.Nil(results)
// We now check the normal case. Two outputs will be tested - one found
// in mempool and other not.
//
// Decode the hex so we can assert the mock mempool is called with it.
tx := decodeTxHex(t, txHex1)
// Create testing outpoints.
opInMempool := wire.OutPoint{Hash: chainhash.Hash{1}, Index: 1}
opNotInMempool := wire.OutPoint{Hash: chainhash.Hash{2}, Index: 1}
// We only expect to see one output being found as spent in mempool.
expectedResults := []*btcjson.GetTxSpendingPrevOutResult{
{
Txid: opInMempool.Hash.String(),
Vout: opInMempool.Index,
SpendingTxid: tx.Hash().String(),
},
{
Txid: opNotInMempool.Hash.String(),
Vout: opNotInMempool.Index,
},
}
// We mock the first call to `CheckSpend` to return a result saying the
// output is found.
mm.On("CheckSpend", opInMempool).Return(tx).Once()
// We mock the second call to `CheckSpend` to return a result saying the
// output is NOT found.
mm.On("CheckSpend", opNotInMempool).Return(nil).Once()
// Create a request with the above outputs.
cmd = &btcjson.GetTxSpendingPrevOutCmd{
Outputs: []*btcjson.GetTxSpendingPrevOutCmdOutput{
{
Txid: opInMempool.Hash.String(),
Vout: opInMempool.Index,
},
{
Txid: opNotInMempool.Hash.String(),
Vout: opNotInMempool.Index,
},
},
}
// Call the method handler and assert the expected result is returned.
closeChan = make(chan struct{})
results, err = handleGetTxSpendingPrevOut(s, cmd, closeChan)
require.NoError(err)
require.Equal(expectedResults, results)
}