Skip to content

Commit

Permalink
Merge pull request btcsuite#910 from yyforyongyu/add-mempool-lookup
Browse files Browse the repository at this point in the history
chain: add mempool lookup for outpoints
  • Loading branch information
Roasbeef authored Mar 5, 2024
2 parents 1f3534b + 524b1eb commit f7c216e
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 42 deletions.
8 changes: 8 additions & 0 deletions chain/bitcoind_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1400,3 +1400,11 @@ func (c *BitcoindClient) filterTx(txDetails *btcutil.Tx,

return true, rec, nil
}

// LookupInputMempoolSpend returns the transaction hash and true if the given
// input is found being spent in mempool, otherwise it returns nil and false.
func (c *BitcoindClient) LookupInputMempoolSpend(op wire.OutPoint) (
chainhash.Hash, bool) {

return c.chainConn.events.LookupInputSpend(op)
}
43 changes: 43 additions & 0 deletions chain/bitcoind_events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ func TestBitcoindEvents(t *testing.T) {
// mempool.
btcClient = setupBitcoind(t, addr, test.rpcPolling)
testNotifySpentMempool(t, miner1, btcClient)

// Test looking up mempool for input spent.
testLookupInputMempoolSpend(t, miner1, btcClient)
})
}
}
Expand Down Expand Up @@ -214,6 +217,46 @@ func testNotifySpentMempool(t *testing.T, miner *rpctest.Harness,
}
}

// testLookupInputMempoolSpend tests that LookupInputMempoolSpend returns the
// correct tx hash and whether the input has been spent in the mempool.
func testLookupInputMempoolSpend(t *testing.T, miner *rpctest.Harness,
client *BitcoindClient) {

rt := require.New(t)

script, _, err := randPubKeyHashScript()
rt.NoError(err)

// Create a test tx.
tx, err := miner.CreateTransaction(
[]*wire.TxOut{{Value: 1000, PkScript: script}}, 5, false,
)
rt.NoError(err)

// Lookup the input in mempool.
op := tx.TxIn[0].PreviousOutPoint
txid, found := client.LookupInputMempoolSpend(op)

// Expect that the input has not been spent in the mempool.
rt.False(found)
rt.Zero(txid)

// Send the tx which will put it in the mempool.
_, err = client.SendRawTransaction(tx, true)
rt.NoError(err)

// Lookup the input again should return the spending tx.
//
// NOTE: We need to wait for the tx to propagate to the mempool.
rt.Eventually(func() bool {
txid, found = client.LookupInputMempoolSpend(op)
return found
}, 5*time.Second, 100*time.Millisecond)

// Check the expected txid is returned.
rt.Equal(tx.TxHash(), txid)
}

// testReorg tests that the given BitcoindClient correctly responds to a chain
// re-org.
func testReorg(t *testing.T, miner1, miner2 *rpctest.Harness,
Expand Down
40 changes: 1 addition & 39 deletions chain/bitcoind_zmq_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package chain

import (
"bytes"
"encoding/json"
"fmt"
"io"
"math/rand"
Expand Down Expand Up @@ -219,22 +218,6 @@ func (b *bitcoindZMQEvents) BlockNotifications() <-chan *wire.MsgBlock {
return b.blockNtfns
}

// getTxSpendingPrevOutReq is the rpc request format for bitcoind's
// gettxspendingprevout call.
type getTxSpendingPrevOutReq struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
}

// getTxSpendingPrevOutResp is the rpc response format for bitcoind's
// gettxspendingprevout call. It returns the "spendingtxid" if one exists in
// the mempool.
type getTxSpendingPrevOutResp struct {
Txid string `json:"txid"`
Vout float64 `json:"vout"`
SpendingTxid string `json:"spendingtxid"`
}

// LookupInputSpend returns the transaction that spends the given outpoint
// found in the mempool.
func (b *bitcoindZMQEvents) LookupInputSpend(
Expand Down Expand Up @@ -501,28 +484,7 @@ func (b *bitcoindZMQEvents) mempoolPoller() {
func getTxSpendingPrevOut(op wire.OutPoint,
client *rpcclient.Client) (chainhash.Hash, bool) {

prevoutReq := &getTxSpendingPrevOutReq{
Txid: op.Hash.String(), Vout: op.Index,
}

// The RPC takes an array of prevouts so we have an array with a single
// item since we don't yet batch calls to LookupInputSpend.
prevoutArr := []*getTxSpendingPrevOutReq{prevoutReq}

req, err := json.Marshal(prevoutArr)
if err != nil {
return chainhash.Hash{}, false
}

resp, err := client.RawRequest(
"gettxspendingprevout", []json.RawMessage{req},
)
if err != nil {
return chainhash.Hash{}, false
}

var prevoutResps []getTxSpendingPrevOutResp
err = json.Unmarshal(resp, &prevoutResps)
prevoutResps, err := client.GetTxSpendingPrevOut([]wire.OutPoint{op})
if err != nil {
return chainhash.Hash{}, false
}
Expand Down
113 changes: 113 additions & 0 deletions chain/btcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ var _ Interface = (*RPCClient)(nil)
// but must be done using the Start method. If the remote server does not
// operate on the same bitcoin network as described by the passed chain
// parameters, the connection will be disconnected.
//
// TODO(yy): deprecate it in favor of NewRPCClientWithConfig.
func NewRPCClient(chainParams *chaincfg.Params, connect, user, pass string, certs []byte,
disableTLS bool, reconnectAttempts int) (*RPCClient, error) {

Expand Down Expand Up @@ -91,6 +93,109 @@ func NewRPCClient(chainParams *chaincfg.Params, connect, user, pass string, cert
return client, nil
}

// RPCClientConfig defines the config options used when initializing the RPC
// Client.
type RPCClientConfig struct {
// Conn describes the connection configuration parameters for the
// client.
Conn *rpcclient.ConnConfig

// Params defines a Bitcoin network by its parameters.
Chain *chaincfg.Params

// NotificationHandlers defines callback function pointers to invoke
// with notifications. If not set, the default handlers defined in this
// client will be used.
NotificationHandlers *rpcclient.NotificationHandlers

// ReconnectAttempts defines the number to reties (each after an
// increasing backoff) if the connection can not be established.
ReconnectAttempts int
}

// validate checks the required config options are set.
func (r *RPCClientConfig) validate() error {
if r == nil {
return errors.New("missing rpc config")
}

// Make sure retry attempts is positive.
if r.ReconnectAttempts < 0 {
return errors.New("reconnectAttempts must be positive")
}

// Make sure the chain params are configed.
if r.Chain == nil {
return errors.New("missing chain params config")
}

// Make sure connection config is supplied.
if r.Conn == nil {
return errors.New("missing conn config")
}

// If disableTLS is false, the remote RPC certificate must be provided
// in the certs slice.
if !r.Conn.DisableTLS && r.Conn.Certificates == nil {
return errors.New("must provide certs when TLS is enabled")
}

return nil
}

// NewRPCClientWithConfig creates a client connection to the server based on
// the config options supplised.
//
// The connection is not established immediately, but must be done using the
// Start method. If the remote server does not operate on the same bitcoin
// network as described by the passed chain parameters, the connection will be
// disconnected.
func NewRPCClientWithConfig(cfg *RPCClientConfig) (*RPCClient, error) {
// Make sure the config is valid.
if err := cfg.validate(); err != nil {
return nil, err
}

// Mimic the old behavior defined in `NewRPCClient`. We will remove
// these hard-codings once this package is more properly refactored.
cfg.Conn.DisableAutoReconnect = false
cfg.Conn.DisableConnectOnNew = true

client := &RPCClient{
connConfig: cfg.Conn,
chainParams: cfg.Chain,
reconnectAttempts: cfg.ReconnectAttempts,
enqueueNotification: make(chan interface{}),
dequeueNotification: make(chan interface{}),
currentBlock: make(chan *waddrmgr.BlockStamp),
quit: make(chan struct{}),
}

// Use the configed notification callbacks, if not set, default to the
// callbacks defined in this package.
ntfnCallbacks := cfg.NotificationHandlers
if ntfnCallbacks == nil {
ntfnCallbacks = &rpcclient.NotificationHandlers{
OnClientConnected: client.onClientConnect,
OnBlockConnected: client.onBlockConnected,
OnBlockDisconnected: client.onBlockDisconnected,
OnRecvTx: client.onRecvTx,
OnRedeemingTx: client.onRedeemingTx,
OnRescanFinished: client.onRescanFinished,
OnRescanProgress: client.onRescanProgress,
}
}

// Create the RPC client using the above config.
rpcClient, err := rpcclient.New(client.connConfig, ntfnCallbacks)
if err != nil {
return nil, err
}

client.Client = rpcClient
return client, nil
}

// BackEnd returns the name of the driver.
func (c *RPCClient) BackEnd() string {
return "btcd"
Expand Down Expand Up @@ -465,3 +570,11 @@ func (c *RPCClient) POSTClient() (*rpcclient.Client, error) {
configCopy.HTTPPostMode = true
return rpcclient.New(&configCopy, nil)
}

// LookupInputMempoolSpend returns the transaction hash and true if the given
// input is found being spent in mempool, otherwise it returns nil and false.
func (c *RPCClient) LookupInputMempoolSpend(op wire.OutPoint) (
chainhash.Hash, bool) {

return getTxSpendingPrevOut(op, c.Client)
}
58 changes: 58 additions & 0 deletions chain/btcd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package chain

import (
"testing"

"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/rpcclient"
"github.com/stretchr/testify/require"
)

// TestValidateConfig checks the `validate` method on the RPCClientConfig
// behaves as expected.
func TestValidateConfig(t *testing.T) {
t.Parallel()

rt := require.New(t)

// ReconnectAttempts must be positive.
cfg := &RPCClientConfig{
ReconnectAttempts: -1,
}
rt.ErrorContains(cfg.validate(), "reconnectAttempts")

// Must specify a chain params.
cfg = &RPCClientConfig{
ReconnectAttempts: 1,
}
rt.ErrorContains(cfg.validate(), "chain params")

// Must specify a connection config.
cfg = &RPCClientConfig{
ReconnectAttempts: 1,
Chain: &chaincfg.Params{},
}
rt.ErrorContains(cfg.validate(), "conn config")

// Must specify a certificate when using TLS.
cfg = &RPCClientConfig{
ReconnectAttempts: 1,
Chain: &chaincfg.Params{},
Conn: &rpcclient.ConnConfig{},
}
rt.ErrorContains(cfg.validate(), "certs")

// Validate config.
cfg = &RPCClientConfig{
ReconnectAttempts: 1,
Chain: &chaincfg.Params{},
Conn: &rpcclient.ConnConfig{
DisableTLS: true,
},
}
rt.NoError(cfg.validate())

// When a nil config is provided, it should return an error.
_, err := NewRPCClientWithConfig(nil)
rt.ErrorContains(err, "missing rpc config")
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module github.com/btcsuite/btcwallet

require (
github.com/btcsuite/btcd v0.24.1-0.20240116200649-17fdc5219b36
github.com/btcsuite/btcd v0.24.1-0.20240301210420-1a2b599bf1af
github.com/btcsuite/btcd/btcec/v2 v2.2.2
github.com/btcsuite/btcd/btcutil v1.1.5
github.com/btcsuite/btcd/btcutil/psbt v1.1.8
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13P
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4/go.mod h1:7alexyj/lHlOtr2PJK7L/+HDJZpcGDn/pAU98r7DY08=
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
github.com/btcsuite/btcd v0.24.1-0.20240116200649-17fdc5219b36 h1:Us/FoCuHjjn1OfE278h9QTGuuydc0n+SA+NlycvfNsM=
github.com/btcsuite/btcd v0.24.1-0.20240116200649-17fdc5219b36/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
github.com/btcsuite/btcd v0.24.1-0.20240301210420-1a2b599bf1af h1:F60A3wst4/fy9Yr1Vn8MYmFlfn7DNLxp8o8UTvhqgBE=
github.com/btcsuite/btcd v0.24.1-0.20240301210420-1a2b599bf1af/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/btcec/v2 v2.2.2 h1:5uxe5YjoCq+JeOpg0gZSNHuFgeogrocBYxvg6w9sAgc=
Expand Down

0 comments on commit f7c216e

Please sign in to comment.