From 72bbdd55a6e40e0723b56f8e50440fd49f461ead Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 23 Feb 2024 15:29:57 +0800 Subject: [PATCH] rpcserver+mempool: implement `gettxspendingprevout` for `btcd` This commit adds the RPC method `gettxspendingprevout` for btcd. --- mempool/mocks.go | 4 +++ rpcserver.go | 44 +++++++++++++++++++++++++ rpcserver_test.go | 84 +++++++++++++++++++++++++++++++++++++++++++++++ rpcserverhelp.go | 12 +++++++ 4 files changed, 144 insertions(+) diff --git a/mempool/mocks.go b/mempool/mocks.go index 5f50bb07..e81309c5 100644 --- a/mempool/mocks.go +++ b/mempool/mocks.go @@ -117,5 +117,9 @@ func (m *MockTxMempool) CheckMempoolAcceptance( func (m *MockTxMempool) CheckSpend(op wire.OutPoint) *btcutil.Tx { args := m.Called(op) + if args.Get(0) == nil { + return nil + } + return args.Get(0).(*btcutil.Tx) } diff --git a/rpcserver.go b/rpcserver.go index 2433286a..d6f3167f 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -185,6 +185,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "verifymessage": handleVerifyMessage, "version": handleVersion, "testmempoolaccept": handleTestMempoolAccept, + "gettxspendingprevout": handleGetTxSpendingPrevOut, } // list of commands that we recognize, but for which btcd has no support because @@ -3906,6 +3907,49 @@ func handleTestMempoolAccept(s *rpcServer, cmd interface{}, return results, nil } +// handleGetTxSpendingPrevOut implements the gettxspendingprevout command. +func handleGetTxSpendingPrevOut(s *rpcServer, cmd interface{}, + closeChan <-chan struct{}) (interface{}, error) { + + c := cmd.(*btcjson.GetTxSpendingPrevOutCmd) + + // Convert the outpoints. + ops := make([]wire.OutPoint, 0, len(c.Outputs)) + for _, o := range c.Outputs { + hash, err := chainhash.NewHashFromStr(o.Txid) + if err != nil { + return nil, err + } + + ops = append(ops, wire.OutPoint{ + Hash: *hash, + Index: o.Vout, + }) + } + + // Check mempool spend for all the outpoints. + results := make([]*btcjson.GetTxSpendingPrevOutResult, 0, len(ops)) + for _, op := range ops { + // Create a result entry. + result := &btcjson.GetTxSpendingPrevOutResult{ + Txid: op.Hash.String(), + Vout: op.Index, + } + + // Check the mempool spend. + spendingTx := s.cfg.TxMemPool.CheckSpend(op) + + // Set the spending txid if found. + if spendingTx != nil { + result.SpendingTxid = spendingTx.Hash().String() + } + + results = append(results, result) + } + + return results, nil +} + // validateFeeRate checks that the fee rate used by transaction doesn't exceed // the max fee rate specified. func validateFeeRate(feeSats btcutil.Amount, txSize int64, diff --git a/rpcserver_test.go b/rpcserver_test.go index 6ca15766..0aa93913 100644 --- a/rpcserver_test.go +++ b/rpcserver_test.go @@ -9,6 +9,7 @@ import ( "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" ) @@ -411,3 +412,86 @@ func TestHandleTestMempoolAcceptFees(t *testing.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) +} diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 0ee84851..0cc384db 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -734,6 +734,17 @@ var helpDescsEnUS = map[string]string{ "testmempoolacceptfees-base": "Transaction fees (only present if 'allowed' is true).", "testmempoolacceptfees-effective-feerate": "The effective feerate in BTC per KvB.", "testmempoolacceptfees-effective-includes": "Transactions whose fees and vsizes are included in effective-feerate. Each item is a transaction wtxid in hex.", + + // GetTxSpendingPrevOutCmd help. + "gettxspendingprevout--synopsis": "Scans the mempool to find transactions spending any of the given outputs", + "gettxspendingprevout-outputs": "The transaction outputs that we want to check, and within each, the txid (string) vout (numeric).", + "gettxspendingprevout-txid": "The transaction id", + "gettxspendingprevout-vout": "The output number", + + // GetTxSpendingPrevOutCmd result help. + "gettxspendingprevoutresult-txid": "The transaction hash in hex.", + "gettxspendingprevoutresult-vout": "The output index.", + "gettxspendingprevoutresult-spendingtxid": "The hash of the transaction that spends the output.", } // rpcResultTypes specifies the result types that each RPC command can return. @@ -790,6 +801,7 @@ var rpcResultTypes = map[string][]interface{}{ "verifymessage": {(*bool)(nil)}, "version": {(*map[string]btcjson.VersionResult)(nil)}, "testmempoolaccept": {(*[]btcjson.TestMempoolAcceptResult)(nil)}, + "gettxspendingprevout": {(*[]btcjson.GetTxSpendingPrevOutResult)(nil)}, // Websocket commands. "loadtxfilter": nil,