rpcclient: add GetTxOutProof{,Async} methods

This commit is contained in:
Oli 2025-08-17 09:27:09 +02:00
parent 51e9b53c78
commit d1a428e381
No known key found for this signature in database
GPG key ID: 8E4256593F177720
2 changed files with 200 additions and 0 deletions

View file

@ -9,6 +9,8 @@ import (
"bytes"
"encoding/hex"
"encoding/json"
"io"
"strings"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg/chainhash"
@ -1102,6 +1104,49 @@ func (c *Client) GetTxOutSetInfo() (*btcjson.GetTxOutSetInfoResult, error) {
return c.GetTxOutSetInfoAsync().Receive()
}
// FutureGetTxOutProofResult is a future promise to deliver the result of a
// GetTxOutProofAsync RPC invocation (or an applicable error).
type FutureGetTxOutProofResult chan *Response
// Receive waits for the Response promised by the future and returns the
// results of GetTxOutSetInfoAsync RPC invocation.
func (r FutureGetTxOutProofResult) Receive() (*wire.MsgMerkleBlock, error) {
res, err := ReceiveFuture(r)
if err != nil {
return nil, err
}
var merkleBlock wire.MsgMerkleBlock
err = merkleBlock.BtcDecode(
hex.NewDecoder(parseJSONStringReader(res)),
wire.ProtocolVersion, wire.WitnessEncoding,
)
if err != nil {
return nil, err
}
return &merkleBlock, nil
}
// GetTxOutProofAsync 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 GetTxOutProof for the blocking version and more details.
func (c *Client) GetTxOutProofAsync(txIDs []string,
blockHash *string) FutureGetTxOutProofResult {
cmd := btcjson.NewGetTxOutProofCmd(txIDs, blockHash)
return c.SendCmd(cmd)
}
// GetTxOutProof returns the proof that a transaction was included in a block.
func (c *Client) GetTxOutProof(txIDs []string,
blockHash *string) (*wire.MsgMerkleBlock, error) {
return c.GetTxOutProofAsync(txIDs, blockHash).Receive()
}
// FutureRescanBlocksResult is a future promise to deliver the result of a
// RescanBlocksAsync RPC invocation (or an applicable error).
//
@ -1455,3 +1500,24 @@ func (c *Client) ReconsiderBlockAsync(
func (c *Client) ReconsiderBlock(blockHash *chainhash.Hash) error {
return c.ReconsiderBlockAsync(blockHash).Receive()
}
// parseJSONString parses a JSON byte slice as a JSON string by removing leading
// and trailing double quotes.
func parseJSONString(jsonWithQuotes []byte) string {
// The result is just a single hex string. So we don't need to unmarshal
// it into a string, replacing the quotes achieves the same result, just
// much faster and with fewer allocations.
return strings.TrimPrefix(
strings.TrimSuffix(string(jsonWithQuotes), "\""), "\"",
)
}
// parseJSONStringReader parses a JSON byte slice as a JSON string by removing
// leading and trailing double quotes and returning it as a reader for further
// processing.
func parseJSONStringReader(jsonWithQuotes []byte) io.Reader {
quotes := []byte("\"")
return bytes.NewReader(bytes.TrimPrefix(
bytes.TrimSuffix(jsonWithQuotes, quotes), quotes,
))
}

View file

@ -1,7 +1,11 @@
package rpcclient
import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
@ -9,7 +13,9 @@ import (
"testing"
"time"
"github.com/btcsuite/btcd/wire"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/require"
)
var upgrader = websocket.Upgrader{}
@ -254,6 +260,134 @@ func TestClientConnectedToWSServerRunner(t *testing.T) {
}
}
// TestJSONStringParsing tests the old vs. the two new methods for parsing a
// JSON string.
func TestJSONStringParsing(t *testing.T) {
testCases := []string{
"\"\"",
"\"foo\"",
"\"5152dcbe2d94dd2c66008b1281157ff44156d146c83bf3a182ba5b32ce" +
"e79f5d\"",
"\"010000004860eb18bf1b1620e37e9490fc8a427514416fd75159ab8668" +
"8e9a8300000000d5fdcc541e25de1c7a5addedf24858b8bb665c" +
"9f36ef744ee42c316022c90f9bb0bc6649ffff001d08d2bd6101" +
"0100000001000000000000000000000000000000000000000000" +
"0000000000000000000000ffffffff0704ffff001d010bffffff" +
"ff0100f2052a010000004341047211a824f55b505228e4c3d519" +
"4c1fcfaa15a456abdf37f9b9d97a4040afc073dee6c89064984f" +
"03385237d92167c13e236446b417ab79a0fcae412ae3316b77ac" +
"00000000\"",
}
oldMethod := func(res []byte) (string, error) {
// Unmarshal result as a string.
var resultStr string
err := json.Unmarshal(res, &resultStr)
if err != nil {
return "", err
}
return resultStr, nil
}
newMethod := func(res []byte) (string, error) {
return parseJSONString(res), nil
}
newMethodReader := func(res []byte) (string, error) {
allBytes, err := io.ReadAll(parseJSONStringReader(res))
if err != nil {
return "", err
}
return string(allBytes), nil
}
for _, testCase := range testCases {
t.Run(testCase, func(t *testing.T) {
oldResult, err := oldMethod([]byte(testCase))
require.NoError(t, err)
newResult, err := newMethod([]byte(testCase))
require.NoError(t, err)
require.Equal(t, oldResult, newResult)
newResult2, err := newMethodReader([]byte(testCase))
require.NoError(t, err)
require.Equal(t, oldResult, newResult2)
})
}
}
// BenchmarkParseJSONString benchmarks the old vs. the two new methods for
// parsing a JSON string.
func BenchmarkParseJSONString(b *testing.B) {
// This is block 2 from mainnet.
payload := []byte(
"\"010000004860eb18bf1b1620e37e9490fc8a427514416fd75159ab8668" +
"8e9a8300000000d5fdcc541e25de1c7a5addedf24858b8bb665c" +
"9f36ef744ee42c316022c90f9bb0bc6649ffff001d08d2bd6101" +
"0100000001000000000000000000000000000000000000000000" +
"0000000000000000000000ffffffff0704ffff001d010bffffff" +
"ff0100f2052a010000004341047211a824f55b505228e4c3d519" +
"4c1fcfaa15a456abdf37f9b9d97a4040afc073dee6c89064984f" +
"03385237d92167c13e236446b417ab79a0fcae412ae3316b77ac" +
"00000000\"",
)
b.ResetTimer()
b.Run("unmarshal", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var s string
if err := json.Unmarshal(payload, &s); err != nil {
b.Fatal(err)
}
serializedBlock, err := hex.DecodeString(s)
if err != nil {
b.Fatal(err)
}
var msgBlock wire.MsgBlock
err = msgBlock.Deserialize(bytes.NewReader(
serializedBlock,
))
if err != nil {
b.Fatal(err)
}
}
})
b.Run("parseJSONString", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
s := parseJSONString(payload)
var msgBlock wire.MsgBlock
err := msgBlock.Deserialize(hex.NewDecoder(
strings.NewReader(s)),
)
if err != nil {
b.Fatal(err)
}
}
})
b.Run("parseJSONStringReader", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
r := parseJSONStringReader(payload)
var msgBlock wire.MsgBlock
err := msgBlock.Deserialize(hex.NewDecoder(r))
if err != nil {
b.Fatal(err)
}
}
})
}
func makeClient(t *testing.T) (*Client, chan string, func()) {
serverReceivedChannel := make(chan string)
s := httptest.NewServer(http.HandlerFunc(makeUpgradeOnConnect(serverReceivedChannel)))