diff --git a/rpcclient/chain.go b/rpcclient/chain.go index dcac9af6a6..878a4007a1 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" @@ -27,13 +29,7 @@ func (r FutureGetBestBlockHashResult) Receive() (*chainhash.Hash, error) { return nil, err } - // Unmarshal result as a string. - var txHashStr string - err = json.Unmarshal(res, &txHashStr) - if err != nil { - return nil, err - } - return chainhash.NewHashFromStr(txHashStr) + return chainhash.NewHashFromStr(parseJSONString(res)) } // GetBestBlockHashAsync returns an instance of a type that can be used to get @@ -111,22 +107,9 @@ func (r FutureGetBlockResult) Receive() (*wire.MsgBlock, error) { return nil, err } - // Unmarshal result as a string. - var blockHex string - err = json.Unmarshal(res, &blockHex) - if err != nil { - return nil, err - } - - // Decode the serialized block hex to raw bytes. - serializedBlock, err := hex.DecodeString(blockHex) - if err != nil { - return nil, err - } - // Deserialize the block and return it. var msgBlock wire.MsgBlock - err = msgBlock.Deserialize(bytes.NewReader(serializedBlock)) + err = msgBlock.Deserialize(hex.NewDecoder(parseJSONStringReader(res))) if err != nil { return nil, err } @@ -558,13 +541,7 @@ func (r FutureGetBlockHashResult) Receive() (*chainhash.Hash, error) { return nil, err } - // Unmarshal the result as a string-encoded sha. - var txHashStr string - err = json.Unmarshal(res, &txHashStr) - if err != nil { - return nil, err - } - return chainhash.NewHashFromStr(txHashStr) + return chainhash.NewHashFromStr(parseJSONString(res)) } // GetBlockHashAsync returns an instance of a type that can be used to get the @@ -595,21 +572,9 @@ func (r FutureGetBlockHeaderResult) Receive() (*wire.BlockHeader, error) { return nil, err } - // Unmarshal result as a string. - var bhHex string - err = json.Unmarshal(res, &bhHex) - if err != nil { - return nil, err - } - - serializedBH, err := hex.DecodeString(bhHex) - if err != nil { - return nil, err - } - // Deserialize the blockheader and return it. var bh wire.BlockHeader - err = bh.Deserialize(bytes.NewReader(serializedBH)) + err = bh.Deserialize(hex.NewDecoder(parseJSONStringReader(res))) if err != nil { return nil, err } @@ -1102,6 +1067,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). // @@ -1201,15 +1209,8 @@ func (r FutureGetCFilterResult) Receive() (*wire.MsgCFilter, error) { return nil, err } - // Unmarshal result as a string. - var filterHex string - err = json.Unmarshal(res, &filterHex) - if err != nil { - return nil, err - } - // Decode the serialized cf hex to raw bytes. - serializedFilter, err := hex.DecodeString(filterHex) + serializedFilter, err := hex.DecodeString(parseJSONString(res)) if err != nil { return nil, err } @@ -1256,15 +1257,8 @@ func (r FutureGetCFilterHeaderResult) Receive() (*wire.MsgCFHeaders, error) { return nil, err } - // Unmarshal result as a string. - var headerHex string - err = json.Unmarshal(res, &headerHex) - if err != nil { - return nil, err - } - // Assign the decoded header into a hash - headerHash, err := chainhash.NewHashFromStr(headerHex) + headerHash, err := chainhash.NewHashFromStr(parseJSONString(res)) if err != nil { return nil, err } @@ -1455,3 +1449,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 464506d0bb..a946bf80e8 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)))