Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chain: add mempool lookup for outpoints #910

Merged
merged 4 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved
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})
yyforyongyu marked this conversation as resolved.
Show resolved Hide resolved
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
Loading