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

support testmempoolaccept for both bitcoind and btcd #2053

Merged
merged 11 commits into from
Jan 16, 2024
Prev Previous commit
Next Next commit
rpcclient: support testmempoolaccept for bitcoind
  • Loading branch information
yyforyongyu committed Jan 15, 2024
commit c7e40280d56eca20394e938ef0c2e82df17e54e5
60 changes: 58 additions & 2 deletions btcjson/chainsvrresults.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,8 @@ type GetBlockTemplateResult struct {
NonceRange string `json:"noncerange,omitempty"`

// Block proposal from BIP 0023.
Capabilities []string `json:"capabilities,omitempty"`
RejectReasion string `json:"reject-reason,omitempty"`
Capabilities []string `json:"capabilities,omitempty"`
RejectReason string `json:"reject-reason,omitempty"`
}

// GetMempoolEntryResult models the data returned from the getmempoolentry's
Expand Down Expand Up @@ -855,3 +855,59 @@ type LoadWalletResult struct {
type DumpWalletResult struct {
Filename string `json:"filename"`
}

// TestMempoolAcceptResult models the data from the testmempoolaccept command.
// The result of the mempool acceptance test for each raw transaction in the
// input array. Returns results for each transaction in the same order they
// were passed in. Transactions that cannot be fully validated due to failures
// in other transactions will not contain an 'allowed' result.
type TestMempoolAcceptResult struct {
// Txid is the transaction hash in hex.
Txid string `json:"txid"`
yyforyongyu marked this conversation as resolved.
Show resolved Hide resolved

// Wtxid is the transaction witness hash in hex.
Wtxid string `json:"wtxid"`

// PackageError is the package validation error, if any (only possible
// if rawtxs had more than 1 transaction).
PackageError string `json:"package-error"`
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved

// Allowed specifies whether this tx would be accepted to the mempool
// and pass client-specified maxfeerate. If not present, the tx was not
// fully validated due to a failure in another tx in the list.
Allowed bool `json:"allowed,omitempty"`

// Vsize is the virtual transaction size as defined in BIP 141. This is
// different from actual serialized size for witness transactions as
// witness data is discounted (only present when 'allowed' is true)
Vsize int32 `json:"vsize,omitempty"`

// Fees specifies the transaction fees (only present if 'allowed' is
// true).
Fees *TestMempoolAcceptFees `json:"fees,omitempty"`

// RejectReason is the rejection string (only present when 'allowed' is
// false).
RejectReason string `json:"reject-reason,omitempty"`
}

// TestMempoolAcceptFees models the `fees` section from the testmempoolaccept
// command.
type TestMempoolAcceptFees struct {
// Base is the transaction fee in BTC.
Base float64 `json:"base"`

// EffectiveFeeRate specifies the effective feerate in BTC per KvB. May
// differ from the base feerate if, for example, there are modified
// fees from prioritisetransaction or a package feerate was used.
//
// NOTE: this field only exists in bitcoind v25.0 and above.
EffectiveFeeRate float64 `json:"effective-feerate"`

// EffectiveIncludes specifies transactions whose fees and vsizes are
// included in effective-feerate. Each item is a transaction wtxid in
// hex.
//
// NOTE: this field only exists in bitcoind v25.0 and above.
EffectiveIncludes []string `json:"effective-includes"`
}
13 changes: 13 additions & 0 deletions rpcclient/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package rpcclient

import "errors"

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")

// ErrInvalidParam is returned when the caller provides an invalid
// parameter to an RPC method.
ErrInvalidParam = errors.New("invalid param")
)
132 changes: 132 additions & 0 deletions rpcclient/rawtransactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"

"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/btcutil"
Expand Down Expand Up @@ -880,3 +881,134 @@ func (c *Client) DecodeScriptAsync(serializedScript []byte) FutureDecodeScriptRe
func (c *Client) DecodeScript(serializedScript []byte) (*btcjson.DecodeScriptResult, error) {
return c.DecodeScriptAsync(serializedScript).Receive()
}

// FutureTestMempoolAcceptResult is a future promise to deliver the result
// of a TestMempoolAccept RPC invocation (or an applicable error).
type FutureTestMempoolAcceptResult chan *Response

// Receive waits for the Response promised by the future and returns the
// response from TestMempoolAccept.
func (r FutureTestMempoolAcceptResult) Receive() (
[]*btcjson.TestMempoolAcceptResult, error) {

response, err := ReceiveFuture(r)
if err != nil {
return nil, err
}

// Unmarshal as an array of TestMempoolAcceptResult items.
var results []*btcjson.TestMempoolAcceptResult

err = json.Unmarshal(response, &results)
if err != nil {
return nil, err
}

return results, nil
}

// TestMempoolAcceptAsync 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 TestMempoolAccept for the blocking version and more details.
func (c *Client) TestMempoolAcceptAsync(txns []*wire.MsgTx,
maxFeeRate float64) FutureTestMempoolAcceptResult {

// 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("TestMempoolAcceptAsync: backend version %s", version)

// Exit early if the version is below 22.0.0.
//
// Based on the history of `testmempoolaccept` in bitcoind,
// - introduced in 0.17.0
// - unchanged in 0.18.0
// - allowhighfees(bool) param is changed to maxfeerate(numeric) in
// 0.19.0
// - unchanged in 0.20.0
// - added fees and vsize fields in its response in 0.21.0
// - allow more than one txes in param rawtx and added package-error
// and wtxid fields in its response in 0.22.0
// - unchanged in 0.23.0
// - unchanged in 0.24.0
// - added effective-feerate and effective-includes fields in its
// response in 0.25.0
//
// 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)
return newFutureError(err)
}

// The maximum number of transactions allowed is 25.
if len(txns) > 25 {
err := fmt.Errorf("%w: too many transactions provided",
ErrInvalidParam)
return newFutureError(err)
}

// Exit early if an empty array of transactions is provided.
if len(txns) == 0 {
err := fmt.Errorf("%w: no transactions provided",
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved
ErrInvalidParam)
return newFutureError(err)
}

// Iterate all the transactions and turn them into hex strings.
rawTxns := make([]string, 0, len(txns))
for _, tx := range txns {
// Serialize the transaction and convert to hex string.
buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize()))

// TODO(yy): add similar checks found in `BtcDecode` to
// `BtcEncode` - atm it just serializes bytes without any
// bitcoin-specific checks.
if err := tx.Serialize(buf); err != nil {
err = fmt.Errorf("%w: %v", ErrInvalidParam, err)
return newFutureError(err)
}

rawTx := hex.EncodeToString(buf.Bytes())
rawTxns = append(rawTxns, rawTx)

// Sanity check the provided tx is valid, which can be removed
// once we have similar checks added in `BtcEncode`.
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved
//
// NOTE: must be performed after buf.Bytes is copied above.
//
// TODO(yy): remove it once the above TODO is addressed.
if err := tx.Deserialize(buf); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm missing something here: we just serialized it above, why wouldn't it deserialize here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is because tx.Serialize doesn't do any check to make sure it's a valid tx, one can pass an empty *wire.MsgTx to it and it will decode [0 0 0 0 0 0 0 0 0 0] into the buffer.

err = fmt.Errorf("%w: %v", ErrInvalidParam, err)
return newFutureError(err)
}
}

cmd := btcjson.NewTestMempoolAcceptCmd(rawTxns, maxFeeRate)

return c.SendCmd(cmd)
}

// TestMempoolAccept returns result of mempool acceptance tests indicating if
// raw transaction(s) would be accepted by mempool.
//
// If multiple transactions are passed in, parents must come before children
// and package policies apply: the transactions cannot conflict with any
// mempool transactions or each other.
//
// If one transaction fails, other transactions may not be fully validated (the
// 'allowed' key will be blank).
//
// The maximum number of transactions allowed is 25.
func (c *Client) TestMempoolAccept(txns []*wire.MsgTx,
maxFeeRate float64) ([]*btcjson.TestMempoolAcceptResult, error) {

return c.TestMempoolAcceptAsync(txns, maxFeeRate).Receive()
}