Skip to content
Open
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
129 changes: 72 additions & 57 deletions rpcclient/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"bytes"
"encoding/hex"
"encoding/json"
"io"
"strings"

"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg/chainhash"
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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).
//
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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,
))
}
134 changes: 134 additions & 0 deletions rpcclient/chain_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package rpcclient

import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"

"github.com/btcsuite/btcd/wire"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/require"
)

var upgrader = websocket.Upgrader{}
Expand Down Expand Up @@ -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)))
Expand Down
Loading