From d1a428e381cd2bb36b8719553b9f2ce67f683bcd Mon Sep 17 00:00:00 2001 From: Oli Date: Sun, 17 Aug 2025 09:27:09 +0200 Subject: [PATCH] rpcclient: add GetTxOutProof{,Async} methods --- rpcclient/chain.go | 66 ++++++++++++++++++++ rpcclient/chain_test.go | 134 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+) diff --git a/rpcclient/chain.go b/rpcclient/chain.go index dcac9af6..1721c929 100644 --- a/rpcclient/chain.go +++ b/rpcclient/chain.go @@ -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, + )) +} diff --git a/rpcclient/chain_test.go b/rpcclient/chain_test.go index 464506d0..a946bf80 100644 --- a/rpcclient/chain_test.go +++ b/rpcclient/chain_test.go @@ -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)))