diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index 58fa8cd1b6..956b4db604 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -1064,6 +1064,38 @@ func NewTestMempoolAcceptCmd(rawTxns []string, } } +// GetTxSpendingPrevOutCmd defines the gettxspendingprevout JSON-RPC command. +type GetTxSpendingPrevOutCmd struct { + // Outputs is a list of transaction outputs to query. + Outputs []*GetTxSpendingPrevOutCmdOutput +} + +// GetTxSpendingPrevOutCmdOutput defines the output to query for the +// gettxspendingprevout JSON-RPC command. +type GetTxSpendingPrevOutCmdOutput struct { + Txid string `json:"txid"` + Vout uint32 `json:"vout"` +} + +// NewGetTxSpendingPrevOutCmd returns a new instance which can be used to issue +// a gettxspendingprevout JSON-RPC command. +func NewGetTxSpendingPrevOutCmd( + outpoints []wire.OutPoint) *GetTxSpendingPrevOutCmd { + + outputs := make([]*GetTxSpendingPrevOutCmdOutput, 0, len(outpoints)) + + for _, op := range outpoints { + outputs = append(outputs, &GetTxSpendingPrevOutCmdOutput{ + Txid: op.Hash.String(), + Vout: op.Index, + }) + } + + return &GetTxSpendingPrevOutCmd{ + Outputs: outputs, + } +} + func init() { // No special flags for commands in this file. flags := UsageFlag(0) @@ -1125,4 +1157,5 @@ func init() { MustRegisterCmd("verifymessage", (*VerifyMessageCmd)(nil), flags) MustRegisterCmd("verifytxoutproof", (*VerifyTxOutProofCmd)(nil), flags) MustRegisterCmd("testmempoolaccept", (*TestMempoolAcceptCmd)(nil), flags) + MustRegisterCmd("gettxspendingprevout", (*GetTxSpendingPrevOutCmd)(nil), flags) } diff --git a/btcjson/chainsvrcmds_test.go b/btcjson/chainsvrcmds_test.go index eddfb03788..d3143a528c 100644 --- a/btcjson/chainsvrcmds_test.go +++ b/btcjson/chainsvrcmds_test.go @@ -13,6 +13,7 @@ import ( "testing" "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" ) @@ -1500,6 +1501,29 @@ func TestChainSvrCmds(t *testing.T) { MaxFeeRate: 0.01, }, }, + { + name: "gettxspendingprevout", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd( + "gettxspendingprevout", + []*btcjson.GetTxSpendingPrevOutCmdOutput{ + {Txid: "0000000000000000000000000000000000000000000000000000000000000001", Vout: 0}, + }) + }, + staticCmd: func() interface{} { + outputs := []wire.OutPoint{ + {Hash: chainhash.Hash{1}, Index: 0}, + } + return btcjson.NewGetTxSpendingPrevOutCmd(outputs) + }, + marshalled: `{"jsonrpc":"1.0","method":"gettxspendingprevout","params":[[{"txid":"0000000000000000000000000000000000000000000000000000000000000001","vout":0}]],"id":1}`, + unmarshalled: &btcjson.GetTxSpendingPrevOutCmd{ + Outputs: []*btcjson.GetTxSpendingPrevOutCmdOutput{{ + Txid: "0000000000000000000000000000000000000000000000000000000000000001", + Vout: 0, + }}, + }, + }, } t.Logf("Running %d tests", len(tests)) diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index 8f59f77676..11c0483d31 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -911,3 +911,17 @@ type TestMempoolAcceptFees struct { // NOTE: this field only exists in bitcoind v25.0 and above. EffectiveIncludes []string `json:"effective-includes"` } + +// GetTxSpendingPrevOutResult defines a single item returned from the +// gettxspendingprevout command. +type GetTxSpendingPrevOutResult struct { + // Txid is the transaction id of the checked output. + Txid string `json:"txid"` + + // Vout is the vout value of the checked output. + Vout uint32 `json:"vout"` + + // SpendingTxid is the transaction id of the mempool transaction + // spending this output (omitted if unspent). + SpendingTxid string `json:"spendingtxid,omitempty"` +} diff --git a/integration/chain_test.go b/integration/chain_test.go new file mode 100644 index 0000000000..0f5cd94c83 --- /dev/null +++ b/integration/chain_test.go @@ -0,0 +1,146 @@ +//go:build rpctest +// +build rpctest + +package integration + +import ( + "testing" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/integration/rpctest" + "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/stretchr/testify/require" +) + +// TestGetTxSpendingPrevOut checks that `GetTxSpendingPrevOut` behaves as +// expected. +// - an error is returned when invalid params are used. +// - orphan tx is rejected. +// - fee rate above the max is rejected. +// - a mixed of both allowed and rejected can be returned in the same response. +func TestGetTxSpendingPrevOut(t *testing.T) { + t.Parallel() + + // Boilerplate codetestDir to make a pruned node. + btcdCfg := []string{"--rejectnonstd", "--debuglevel=debug"} + r, err := rpctest.New(&chaincfg.SimNetParams, nil, btcdCfg, "") + require.NoError(t, err) + + // Setup the node. + require.NoError(t, r.SetUp(true, 100)) + t.Cleanup(func() { + require.NoError(t, r.TearDown()) + }) + + // Create a tx and testing outpoints. + tx := createTxInMempool(t, r) + opInMempool := tx.TxIn[0].PreviousOutPoint + opNotInMempool := wire.OutPoint{ + Hash: tx.TxHash(), + Index: 0, + } + + testCases := []struct { + name string + outpoints []wire.OutPoint + expectedErr error + expectedResult []*btcjson.GetTxSpendingPrevOutResult + }{ + { + // When no outpoints are provided, the method should + // return an error. + name: "empty outpoints", + expectedErr: rpcclient.ErrInvalidParam, + expectedResult: nil, + }, + { + // When there are outpoints provided, check the + // expceted results are returned. + name: "outpoints", + outpoints: []wire.OutPoint{ + opInMempool, opNotInMempool, + }, + expectedErr: nil, + expectedResult: []*btcjson.GetTxSpendingPrevOutResult{ + { + Txid: opInMempool.Hash.String(), + Vout: opInMempool.Index, + SpendingTxid: tx.TxHash().String(), + }, + { + Txid: opNotInMempool.Hash.String(), + Vout: opNotInMempool.Index, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + + results, err := r.Client.GetTxSpendingPrevOut( + tc.outpoints, + ) + + require.ErrorIs(err, tc.expectedErr) + require.Len(results, len(tc.expectedResult)) + + // Check each item is returned as expected. + for i, r := range results { + e := tc.expectedResult[i] + + require.Equal(e.Txid, r.Txid) + require.Equal(e.Vout, r.Vout) + require.Equal(e.SpendingTxid, r.SpendingTxid) + } + }) + } +} + +// createTxInMempool creates a tx and puts it in the mempool. +func createTxInMempool(t *testing.T, r *rpctest.Harness) *wire.MsgTx { + // Create a fresh output for usage within the test below. + const outputValue = btcutil.SatoshiPerBitcoin + outputKey, testOutput, testPkScript, err := makeTestOutput( + r, t, outputValue, + ) + require.NoError(t, err) + + // Create a new transaction with a lock-time past the current known + // MTP. + tx := wire.NewMsgTx(1) + tx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: *testOutput, + }) + + // Fetch a fresh address from the harness, we'll use this address to + // send funds back into the Harness. + addr, err := r.NewAddress() + require.NoError(t, err) + + addrScript, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + + tx.AddTxOut(&wire.TxOut{ + PkScript: addrScript, + Value: outputValue - 1000, + }) + + sigScript, err := txscript.SignatureScript( + tx, 0, testPkScript, txscript.SigHashAll, outputKey, true, + ) + require.NoError(t, err) + tx.TxIn[0].SignatureScript = sigScript + + // Send the tx. + _, err = r.Client.SendRawTransaction(tx, true) + require.NoError(t, err) + + return tx +} diff --git a/mempool/mocks.go b/mempool/mocks.go index 5f50bb0730..e81309c51a 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/rpcclient/backend_version.go b/rpcclient/backend_version.go new file mode 100644 index 0000000000..cb2a46fc5e --- /dev/null +++ b/rpcclient/backend_version.go @@ -0,0 +1,208 @@ +package rpcclient + +import "strings" + +// BackendVersion defines an interface to handle the version of the backend +// used by the client. +type BackendVersion interface { + // String returns a human-readable backend version. + String() string + + // SupportUnifiedSoftForks returns true if the backend supports the + // unified softforks format. + SupportUnifiedSoftForks() bool + + // SupportTestMempoolAccept returns true if the backend supports the + // testmempoolaccept RPC. + SupportTestMempoolAccept() bool + + // SupportGetTxSpendingPrevOut returns true if the backend supports the + // gettxspendingprevout RPC. + SupportGetTxSpendingPrevOut() bool +} + +// BitcoindVersion represents the version of the bitcoind the client is +// currently connected to. +type BitcoindVersion uint8 + +const ( + // BitcoindPre19 represents a bitcoind version before 0.19.0. + BitcoindPre19 BitcoindVersion = iota + + // BitcoindPre22 represents a bitcoind version equal to or greater than + // 0.19.0 and smaller than 22.0.0. + BitcoindPre22 + + // BitcoindPre24 represents a bitcoind version equal to or greater than + // 22.0.0 and smaller than 24.0.0. + BitcoindPre24 + + // BitcoindPre25 represents a bitcoind version equal to or greater than + // 24.0.0 and smaller than 25.0.0. + BitcoindPre25 + + // BitcoindPre25 represents a bitcoind version equal to or greater than + // 25.0.0. + BitcoindPost25 +) + +// String returns a human-readable backend version. +func (b BitcoindVersion) String() string { + switch b { + case BitcoindPre19: + return "bitcoind 0.19 and below" + + case BitcoindPre22: + return "bitcoind v0.19.0-v22.0.0" + + case BitcoindPre24: + return "bitcoind v22.0.0-v24.0.0" + + case BitcoindPre25: + return "bitcoind v24.0.0-v25.0.0" + + case BitcoindPost25: + return "bitcoind v25.0.0 and above" + + default: + return "unknown" + } +} + +// SupportUnifiedSoftForks returns true if the backend supports the unified +// softforks format. +func (b BitcoindVersion) SupportUnifiedSoftForks() bool { + // Versions of bitcoind on or after v0.19.0 use the unified format. + return b > BitcoindPre19 +} + +// SupportTestMempoolAccept returns true if bitcoind version is 22.0.0 or +// above. +func (b BitcoindVersion) SupportTestMempoolAccept() bool { + return b > BitcoindPre22 +} + +// SupportGetTxSpendingPrevOut returns true if bitcoind version is 24.0.0 or +// above. +func (b BitcoindVersion) SupportGetTxSpendingPrevOut() bool { + return b > BitcoindPre24 +} + +// Compile-time checks to ensure that BitcoindVersion satisfy the +// BackendVersion interface. +var _ BackendVersion = BitcoindVersion(0) + +const ( + // bitcoind19Str is the string representation of bitcoind v0.19.0. + bitcoind19Str = "0.19.0" + + // bitcoind22Str is the string representation of bitcoind v22.0.0. + bitcoind22Str = "22.0.0" + + // bitcoind24Str is the string representation of bitcoind v24.0.0. + bitcoind24Str = "24.0.0" + + // bitcoind25Str is the string representation of bitcoind v25.0.0. + bitcoind25Str = "25.0.0" + + // bitcoindVersionPrefix specifies the prefix included in every bitcoind + // version exposed through GetNetworkInfo. + bitcoindVersionPrefix = "/Satoshi:" + + // bitcoindVersionSuffix specifies the suffix included in every bitcoind + // version exposed through GetNetworkInfo. + bitcoindVersionSuffix = "/" +) + +// parseBitcoindVersion parses the bitcoind version from its string +// representation. +func parseBitcoindVersion(version string) BitcoindVersion { + // Trim the version of its prefix and suffix to determine the + // appropriate version number. + version = strings.TrimPrefix( + strings.TrimSuffix(version, bitcoindVersionSuffix), + bitcoindVersionPrefix, + ) + switch { + case version < bitcoind19Str: + return BitcoindPre19 + + case version < bitcoind22Str: + return BitcoindPre22 + + case version < bitcoind24Str: + return BitcoindPre24 + + case version < bitcoind25Str: + return BitcoindPre25 + + default: + return BitcoindPost25 + } +} + +// BtcdVersion represents the version of the btcd the client is currently +// connected to. +type BtcdVersion int32 + +const ( + // BtcdPre2401 describes a btcd version before 0.24.1, which doesn't + // include the `testmempoolaccept` and `gettxspendingprevout` RPCs. + BtcdPre2401 BtcdVersion = iota + + // BtcdPost2401 describes a btcd version equal to or greater than + // 0.24.1. + BtcdPost2401 +) + +// String returns a human-readable backend version. +func (b BtcdVersion) String() string { + switch b { + case BtcdPre2401: + return "btcd 24.0.0 and below" + + case BtcdPost2401: + return "btcd 24.1.0 and above" + + default: + return "unknown" + } +} + +// SupportUnifiedSoftForks returns true if the backend supports the unified +// softforks format. +// +// NOTE: always true for btcd as we didn't track it before. +func (b BtcdVersion) SupportUnifiedSoftForks() bool { + return true +} + +// SupportTestMempoolAccept returns true if btcd version is 24.1.0 or above. +func (b BtcdVersion) SupportTestMempoolAccept() bool { + return b > BtcdPre2401 +} + +// SupportGetTxSpendingPrevOut returns true if btcd version is 24.1.0 or above. +func (b BtcdVersion) SupportGetTxSpendingPrevOut() bool { + return b > BtcdPre2401 +} + +// Compile-time checks to ensure that BtcdVersion satisfy the BackendVersion +// interface. +var _ BackendVersion = BtcdVersion(0) + +const ( + // btcd2401Val is the int representation of btcd v0.24.1. + btcd2401Val = 240100 +) + +// parseBtcdVersion parses the btcd version from its string representation. +func parseBtcdVersion(version int32) BtcdVersion { + switch { + case version < btcd2401Val: + return BtcdPre2401 + + default: + return BtcdPost2401 + } +} diff --git a/rpcclient/backend_version_test.go b/rpcclient/backend_version_test.go new file mode 100644 index 0000000000..3a4baec1db --- /dev/null +++ b/rpcclient/backend_version_test.go @@ -0,0 +1,148 @@ +package rpcclient + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestParseBitcoindVersion checks that the correct version from bitcoind's +// `getnetworkinfo` RPC call is parsed. +func TestParseBitcoindVersion(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + rpcVersion string + parsedVersion BitcoindVersion + }{ + { + name: "parse version 0.19 and below", + rpcVersion: "/Satoshi:0.18.0/", + parsedVersion: BitcoindPre19, + }, + { + name: "parse version 0.19", + rpcVersion: "/Satoshi:0.19.0/", + parsedVersion: BitcoindPre22, + }, + { + name: "parse version 0.19 - 22.0", + rpcVersion: "/Satoshi:0.20.1/", + parsedVersion: BitcoindPre22, + }, + { + name: "parse version 22.0", + rpcVersion: "/Satoshi:22.0.0/", + parsedVersion: BitcoindPre24, + }, + { + name: "parse version 22.0 - 24.0", + rpcVersion: "/Satoshi:23.1.0/", + parsedVersion: BitcoindPre24, + }, + { + name: "parse version 24.0", + rpcVersion: "/Satoshi:24.0.0/", + parsedVersion: BitcoindPre25, + }, + { + name: "parse version 25.0", + rpcVersion: "/Satoshi:25.0.0/", + parsedVersion: BitcoindPost25, + }, + { + name: "parse version 25.0 and above", + rpcVersion: "/Satoshi:26.0.0/", + parsedVersion: BitcoindPost25, + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + version := parseBitcoindVersion(tc.rpcVersion) + require.Equal(t, tc.parsedVersion, version) + }) + } +} + +// TestParseBtcdVersion checks that the correct version from btcd's `getinfo` +// RPC call is parsed. +func TestParseBtcdVersion(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + rpcVersion int32 + parsedVersion BtcdVersion + }{ + { + name: "parse version 0.24 and below", + rpcVersion: 230000, + parsedVersion: BtcdPre2401, + }, + { + name: "parse version 0.24.1", + rpcVersion: 240100, + parsedVersion: BtcdPost2401, + }, + { + name: "parse version 0.24.1 and above", + rpcVersion: 250000, + parsedVersion: BtcdPost2401, + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + version := parseBtcdVersion(tc.rpcVersion) + require.Equal(t, tc.parsedVersion, version) + }) + } +} + +// TestVersionSupports checks all the versions of bitcoind and btcd to ensure +// that the RPCs are supported correctly. +func TestVersionSupports(t *testing.T) { + t.Parallel() + + require := require.New(t) + + // For bitcoind, unified softforks format is supported in 19.0 and + // above. + require.False(BitcoindPre19.SupportUnifiedSoftForks()) + require.True(BitcoindPre22.SupportUnifiedSoftForks()) + require.True(BitcoindPre24.SupportUnifiedSoftForks()) + require.True(BitcoindPre25.SupportUnifiedSoftForks()) + require.True(BitcoindPost25.SupportUnifiedSoftForks()) + + // For bitcoind, `testmempoolaccept` is supported in 22.0 and above. + require.False(BitcoindPre19.SupportTestMempoolAccept()) + require.False(BitcoindPre22.SupportTestMempoolAccept()) + require.True(BitcoindPre24.SupportTestMempoolAccept()) + require.True(BitcoindPre25.SupportTestMempoolAccept()) + require.True(BitcoindPost25.SupportTestMempoolAccept()) + + // For bitcoind, `gettxspendingprevout` is supported in 24.0 and above. + require.False(BitcoindPre19.SupportGetTxSpendingPrevOut()) + require.False(BitcoindPre22.SupportGetTxSpendingPrevOut()) + require.False(BitcoindPre24.SupportGetTxSpendingPrevOut()) + require.True(BitcoindPre25.SupportGetTxSpendingPrevOut()) + require.True(BitcoindPost25.SupportGetTxSpendingPrevOut()) + + // For btcd, unified softforks format is supported in all versions. + require.True(BtcdPre2401.SupportUnifiedSoftForks()) + require.True(BtcdPost2401.SupportUnifiedSoftForks()) + + // For btcd, `testmempoolaccept` is supported in 24.1 and above. + require.False(BtcdPre2401.SupportTestMempoolAccept()) + require.True(BtcdPost2401.SupportTestMempoolAccept()) + + // For btcd, `gettxspendingprevout` is supported in 24.1 and above. + require.False(BtcdPre2401.SupportGetTxSpendingPrevOut()) + require.True(BtcdPost2401.SupportGetTxSpendingPrevOut()) +} diff --git a/rpcclient/chain.go b/rpcclient/chain.go index f2ce1ea626..e65c40b3a8 100644 --- a/rpcclient/chain.go +++ b/rpcclient/chain.go @@ -441,7 +441,7 @@ func unmarshalGetBlockChainInfoResultSoftForks(chainInfo *btcjson.GetBlockChainI version BackendVersion, res []byte) error { // Versions of bitcoind on or after v0.19.0 use the unified format. - if version > BitcoindPre19 { + if version.SupportUnifiedSoftForks() { var softForks btcjson.UnifiedSoftForks if err := json.Unmarshal(res, &softForks); err != nil { return err diff --git a/rpcclient/errors.go b/rpcclient/errors.go index 78f34bef2e..09e1cb3c22 100644 --- a/rpcclient/errors.go +++ b/rpcclient/errors.go @@ -6,9 +6,10 @@ import ( ) var ( - // ErrBitcoindVersion is returned when running against a bitcoind that - // is older than the minimum version supported by the rpcclient. - ErrBitcoindVersion = errors.New("bitcoind version too low") + // ErrBackendVersion is returned when running against a bitcoind or + // btcd that is older than the minimum version supported by the + // rpcclient. + ErrBackendVersion = errors.New("backend version too low") // ErrInvalidParam is returned when the caller provides an invalid // parameter to an RPC method. diff --git a/rpcclient/infrastructure.go b/rpcclient/infrastructure.go index 8543106b15..39cfa3f474 100644 --- a/rpcclient/infrastructure.go +++ b/rpcclient/infrastructure.go @@ -20,7 +20,6 @@ import ( "net/http" "net/url" "os" - "strings" "sync" "sync/atomic" "time" @@ -102,53 +101,6 @@ type jsonRequest struct { responseChan chan *Response } -// BackendVersion represents the version of the backend the client is currently -// connected to. -type BackendVersion uint8 - -const ( - // BitcoindPre19 represents a bitcoind version before 0.19.0. - BitcoindPre19 BackendVersion = iota - - // BitcoindPre22 represents a bitcoind version equal to or greater than - // 0.19.0 and smaller than 22.0.0. - BitcoindPre22 - - // BitcoindPre25 represents a bitcoind version equal to or greater than - // 22.0.0 and smaller than 25.0.0. - BitcoindPre25 - - // BitcoindPre25 represents a bitcoind version equal to or greater than - // 25.0.0. - BitcoindPost25 - - // Btcd represents a catch-all btcd version. - Btcd -) - -// String returns a human-readable backend version. -func (b BackendVersion) String() string { - switch b { - case BitcoindPre19: - return "bitcoind 0.19 and below" - - case BitcoindPre22: - return "bitcoind v0.19.0-v22.0.0" - - case BitcoindPre25: - return "bitcoind v22.0.0-v25.0.0" - - case BitcoindPost25: - return "bitcoind v25.0.0 and above" - - case Btcd: - return "btcd" - - default: - return "unknown" - } -} - // Client represents a Bitcoin RPC client which allows easy access to the // various RPC methods available on a Bitcoin RPC server. Each of the wrapper // functions handle the details of converting the passed and return types to and @@ -182,7 +134,7 @@ type Client struct { // backendVersion is the version of the backend the client is currently // connected to. This should be retrieved through GetVersion. backendVersionMu sync.Mutex - backendVersion *BackendVersion + backendVersion BackendVersion // mtx is a mutex to protect access to connection related fields. mtx sync.Mutex @@ -1618,49 +1570,6 @@ func (c *Client) Connect(tries int) error { return err } -const ( - // bitcoind19Str is the string representation of bitcoind v0.19.0. - bitcoind19Str = "0.19.0" - - // bitcoind22Str is the string representation of bitcoind v22.0.0. - bitcoind22Str = "22.0.0" - - // bitcoind25Str is the string representation of bitcoind v25.0.0. - bitcoind25Str = "25.0.0" - - // bitcoindVersionPrefix specifies the prefix included in every bitcoind - // version exposed through GetNetworkInfo. - bitcoindVersionPrefix = "/Satoshi:" - - // bitcoindVersionSuffix specifies the suffix included in every bitcoind - // version exposed through GetNetworkInfo. - bitcoindVersionSuffix = "/" -) - -// parseBitcoindVersion parses the bitcoind version from its string -// representation. -func parseBitcoindVersion(version string) BackendVersion { - // Trim the version of its prefix and suffix to determine the - // appropriate version number. - version = strings.TrimPrefix( - strings.TrimSuffix(version, bitcoindVersionSuffix), - bitcoindVersionPrefix, - ) - switch { - case version < bitcoind19Str: - return BitcoindPre19 - - case version < bitcoind22Str: - return BitcoindPre22 - - case version < bitcoind25Str: - return BitcoindPre25 - - default: - return BitcoindPost25 - } -} - // BackendVersion retrieves the version of the backend the client is currently // connected to. func (c *Client) BackendVersion() (BackendVersion, error) { @@ -1668,7 +1577,7 @@ func (c *Client) BackendVersion() (BackendVersion, error) { defer c.backendVersionMu.Unlock() if c.backendVersion != nil { - return *c.backendVersion, nil + return c.backendVersion, nil } // We'll start by calling GetInfo. This method doesn't exist for @@ -1680,20 +1589,20 @@ func (c *Client) BackendVersion() (BackendVersion, error) { // Parse the btcd version and cache it. case nil: log.Debugf("Detected btcd version: %v", info.Version) - version := Btcd - c.backendVersion = &version - return *c.backendVersion, nil + version := parseBtcdVersion(info.Version) + c.backendVersion = version + return c.backendVersion, nil // Inspect the RPC error to ensure the method was not found, otherwise // we actually ran into an error. case *btcjson.RPCError: if err.Code != btcjson.ErrRPCMethodNotFound.Code { - return 0, fmt.Errorf("unable to detect btcd version: "+ + return nil, fmt.Errorf("unable to detect btcd version: "+ "%v", err) } default: - return 0, fmt.Errorf("unable to detect btcd version: %v", err) + return nil, fmt.Errorf("unable to detect btcd version: %v", err) } // Since the GetInfo method was not found, we assume the client is @@ -1701,7 +1610,8 @@ func (c *Client) BackendVersion() (BackendVersion, error) { // GetNetworkInfo. networkInfo, err := c.GetNetworkInfo() if err != nil { - return 0, fmt.Errorf("unable to detect bitcoind version: %v", err) + return nil, fmt.Errorf("unable to detect bitcoind version: %v", + err) } // Parse the bitcoind version and cache it. @@ -1709,7 +1619,7 @@ func (c *Client) BackendVersion() (BackendVersion, error) { version := parseBitcoindVersion(networkInfo.SubVersion) c.backendVersion = &version - return *c.backendVersion, nil + return c.backendVersion, nil } func (c *Client) sendAsync() FutureGetBulkResult { diff --git a/rpcclient/infrastructure_test.go b/rpcclient/infrastructure_test.go deleted file mode 100644 index e97fa275c0..0000000000 --- a/rpcclient/infrastructure_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package rpcclient - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -// TestParseBitcoindVersion checks that the correct version from bitcoind's -// `getnetworkinfo` RPC call is parsed. -func TestParseBitcoindVersion(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - rpcVersion string - parsedVersion BackendVersion - }{ - { - name: "parse version 0.19 and below", - rpcVersion: "/Satoshi:0.18.0/", - parsedVersion: BitcoindPre19, - }, - { - name: "parse version 0.19", - rpcVersion: "/Satoshi:0.19.0/", - parsedVersion: BitcoindPre22, - }, - { - name: "parse version 0.19 - 22.0", - rpcVersion: "/Satoshi:0.20.1/", - parsedVersion: BitcoindPre22, - }, - { - name: "parse version 22.0", - rpcVersion: "/Satoshi:22.0.0/", - parsedVersion: BitcoindPre25, - }, - { - name: "parse version 22.0 - 25.0", - rpcVersion: "/Satoshi:23.0.0/", - parsedVersion: BitcoindPre25, - }, - { - name: "parse version 25.0", - rpcVersion: "/Satoshi:25.0.0/", - parsedVersion: BitcoindPost25, - }, - { - name: "parse version 25.0 and above", - rpcVersion: "/Satoshi:26.0.0/", - parsedVersion: BitcoindPost25, - }, - } - - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - version := parseBitcoindVersion(tc.rpcVersion) - require.Equal(t, tc.parsedVersion, version) - }) - } -} diff --git a/rpcclient/rawtransactions.go b/rpcclient/rawtransactions.go index a683021946..9c95041fd4 100644 --- a/rpcclient/rawtransactions.go +++ b/rpcclient/rawtransactions.go @@ -360,7 +360,9 @@ func (c *Client) SendRawTransactionAsync(tx *wire.MsgTx, allowHighFees bool) Fut var cmd *btcjson.SendRawTransactionCmd // Starting from bitcoind v0.19.0, the MaxFeeRate field should be used. - if version > BitcoindPre19 { + // + // When unified softforks format is supported, it's 0.19 and above. + if version.SupportUnifiedSoftForks() { // Using a 0 MaxFeeRate is interpreted as a maximum fee rate not // being enforced by bitcoind. var maxFeeRate int32 @@ -943,8 +945,8 @@ func (c *Client) TestMempoolAcceptAsync(txns []*wire.MsgTx, // // We decide to not support this call for versions below 22.0.0. as the // request/response formats are very different. - if version < BitcoindPre22 { - err := fmt.Errorf("%w: %v", ErrBitcoindVersion, version) + if !version.SupportTestMempoolAccept() { + err := fmt.Errorf("%w: %v", ErrBackendVersion, version) return newFutureError(err) } @@ -1012,3 +1014,71 @@ func (c *Client) TestMempoolAccept(txns []*wire.MsgTx, return c.TestMempoolAcceptAsync(txns, maxFeeRate).Receive() } + +// FutureGetTxSpendingPrevOut is a future promise to deliver the result of a +// GetTxSpendingPrevOut RPC invocation (or an applicable error). +type FutureGetTxSpendingPrevOut chan *Response + +// Receive waits for the Response promised by the future and returns the +// response from GetTxSpendingPrevOut. +func (r FutureGetTxSpendingPrevOut) Receive() ( + []*btcjson.GetTxSpendingPrevOutResult, error) { + + response, err := ReceiveFuture(r) + if err != nil { + return nil, err + } + + // Unmarshal as an array of GetTxSpendingPrevOutResult items. + var results []*btcjson.GetTxSpendingPrevOutResult + + err = json.Unmarshal(response, &results) + if err != nil { + return nil, err + } + + return results, nil +} + +// GetTxSpendingPrevOutAsync returns an instance of a type that can be used to +// get the result of the RPC at some future time by invoking the Receive +// function on the returned instance. +// +// See GetTxSpendingPrevOut for the blocking version and more details. +func (c *Client) GetTxSpendingPrevOutAsync( + outpoints []wire.OutPoint) FutureGetTxSpendingPrevOut { + + // Due to differences in the testmempoolaccept API for different + // backends, we'll need to inspect our version and construct the + // appropriate request. + version, err := c.BackendVersion() + if err != nil { + return newFutureError(err) + } + + log.Debugf("GetTxSpendingPrevOutAsync: backend version %s", version) + + // Exit early if the version is below 24.0.0. + if !version.SupportGetTxSpendingPrevOut() { + err := fmt.Errorf("%w: %v", ErrBackendVersion, version) + return newFutureError(err) + } + + // Exit early if an empty array of outpoints is provided. + if len(outpoints) == 0 { + err := fmt.Errorf("%w: no outpoints provided", ErrInvalidParam) + return newFutureError(err) + } + + cmd := btcjson.NewGetTxSpendingPrevOutCmd(outpoints) + + return c.SendCmd(cmd) +} + +// GetTxSpendingPrevOut returns the result from calling `gettxspendingprevout` +// RPC. +func (c *Client) GetTxSpendingPrevOut(outpoints []wire.OutPoint) ( + []*btcjson.GetTxSpendingPrevOutResult, error) { + + return c.GetTxSpendingPrevOutAsync(outpoints).Receive() +} diff --git a/rpcserver.go b/rpcserver.go index 2433286ac7..d6f3167f1e 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 6ca15766c3..0aa9391321 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 0ee8485180..0cc384db2c 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, diff --git a/version.go b/version.go index d7835910f8..999dfd95b2 100644 --- a/version.go +++ b/version.go @@ -18,7 +18,7 @@ const semanticAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr const ( appMajor uint = 0 appMinor uint = 24 - appPatch uint = 0 + appPatch uint = 1 // appPreRelease MUST only contain characters from semanticAlphabet // per the semantic versioning spec.