Add Batch JSON-RPC support (rpc client & server)

This commit is contained in:
Jake Sylvestre 2020-12-05 22:39:40 -05:00 committed by John C. Vernaleo
parent 31b66488b4
commit 2a1aa5129e
26 changed files with 1193 additions and 357 deletions

View file

@ -211,7 +211,7 @@ func TestBtcdExtCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
@ -235,7 +235,7 @@ func TestBtcdExtCmds(t *testing.T) {
// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)

View file

@ -145,7 +145,7 @@ func TestBtcWalletExtCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
@ -169,7 +169,7 @@ func TestBtcWalletExtCmds(t *testing.T) {
// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)

View file

@ -1466,7 +1466,7 @@ func TestChainSvrCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
@ -1491,7 +1491,7 @@ func TestChainSvrCmds(t *testing.T) {
// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)

View file

@ -233,7 +233,7 @@ func TestChainSvrWsCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
@ -257,7 +257,7 @@ func TestChainSvrWsCmds(t *testing.T) {
// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)

View file

@ -231,7 +231,7 @@ func TestChainSvrWsNtfns(t *testing.T) {
for i, test := range tests {
// Marshal the notification as created by the new static
// creation function. The ID is nil for notifications.
marshalled, err := btcjson.MarshalCmd(nil, test.staticNtfn())
marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, test.staticNtfn())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
@ -256,7 +256,7 @@ func TestChainSvrWsNtfns(t *testing.T) {
// Marshal the notification as created by the generic new
// notification creation function. The ID is nil for
// notifications.
marshalled, err = btcjson.MarshalCmd(nil, cmd)
marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, nil, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)

View file

@ -36,7 +36,7 @@ func makeParams(rt reflect.Type, rv reflect.Value) []interface{} {
// is suitable for transmission to an RPC server. The provided command type
// must be a registered type. All commands provided by this package are
// registered by default.
func MarshalCmd(id interface{}, cmd interface{}) ([]byte, error) {
func MarshalCmd(rpcVersion RPCVersion, id interface{}, cmd interface{}) ([]byte, error) {
// Look up the cmd type and error out if not registered.
rt := reflect.TypeOf(cmd)
registerLock.RLock()
@ -60,7 +60,7 @@ func MarshalCmd(id interface{}, cmd interface{}) ([]byte, error) {
params := makeParams(rt.Elem(), rv.Elem())
// Generate and marshal the final JSON-RPC request.
rawCmd, err := NewRequest(id, method, params)
rawCmd, err := NewRequest(rpcVersion, id, method, params)
if err != nil {
return nil, err
}

View file

@ -460,7 +460,7 @@ func TestMarshalCmd(t *testing.T) {
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
bytes, err := btcjson.MarshalCmd(test.id, test.cmd)
bytes, err := btcjson.MarshalCmd(btcjson.RpcVersion1, test.id, test.cmd)
if err != nil {
t.Errorf("Test #%d (%s) wrong error - got %T (%v)",
i, test.name, err, err)
@ -507,7 +507,7 @@ func TestMarshalCmdErrors(t *testing.T) {
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
_, err := btcjson.MarshalCmd(test.id, test.cmd)
_, err := btcjson.MarshalCmd(btcjson.RpcVersion1, test.id, test.cmd)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T (%v), "+
"want %T", i, test.name, err, err, test.err)
@ -535,7 +535,7 @@ func TestUnmarshalCmdErrors(t *testing.T) {
{
name: "unregistered type",
request: btcjson.Request{
Jsonrpc: "1.0",
Jsonrpc: btcjson.RpcVersion1,
Method: "bogusmethod",
Params: nil,
ID: nil,
@ -545,7 +545,7 @@ func TestUnmarshalCmdErrors(t *testing.T) {
{
name: "incorrect number of params",
request: btcjson.Request{
Jsonrpc: "1.0",
Jsonrpc: btcjson.RpcVersion1,
Method: "getblockcount",
Params: []json.RawMessage{[]byte(`"bogusparam"`)},
ID: nil,
@ -555,7 +555,7 @@ func TestUnmarshalCmdErrors(t *testing.T) {
{
name: "invalid type for a parameter",
request: btcjson.Request{
Jsonrpc: "1.0",
Jsonrpc: btcjson.RpcVersion1,
Method: "getblock",
Params: []json.RawMessage{[]byte("1")},
ID: nil,
@ -565,7 +565,7 @@ func TestUnmarshalCmdErrors(t *testing.T) {
{
name: "invalid JSON for a parameter",
request: btcjson.Request{
Jsonrpc: "1.0",
Jsonrpc: btcjson.RpcVersion1,
Method: "getblock",
Params: []json.RawMessage{[]byte(`"1`)},
ID: nil,

View file

@ -27,7 +27,7 @@ func ExampleMarshalCmd() {
// server. Typically the client would increment the id here which is
// request so the response can be identified.
id := 1
marshalledBytes, err := btcjson.MarshalCmd(id, gbCmd)
marshalledBytes, err := btcjson.MarshalCmd(btcjson.RpcVersion1, id, gbCmd)
if err != nil {
fmt.Println(err)
return
@ -95,7 +95,7 @@ func ExampleUnmarshalCmd() {
func ExampleMarshalResponse() {
// Marshal a new JSON-RPC response. For example, this is a response
// to a getblockheight request.
marshalledBytes, err := btcjson.MarshalResponse(1, 350001, nil)
marshalledBytes, err := btcjson.MarshalResponse(btcjson.RpcVersion1, 1, 350001, nil)
if err != nil {
fmt.Println(err)
return
@ -107,7 +107,7 @@ func ExampleMarshalResponse() {
fmt.Printf("%s\n", marshalledBytes)
// Output:
// {"result":350001,"error":null,"id":1}
// {"jsonrpc":"1.0","result":350001,"error":null,"id":1}
}
// This example demonstrates how to unmarshal a JSON-RPC response and then
@ -116,7 +116,7 @@ func Example_unmarshalResponse() {
// Ordinarily this would be read from the wire, but for this example,
// it is hard coded here for clarity. This is an example response to a
// getblockheight request.
data := []byte(`{"result":350001,"error":null,"id":1}`)
data := []byte(`{"jsonrpc":"1.0","result":350001,"error":null,"id":1}`)
// Unmarshal the raw bytes from the wire into a JSON-RPC response.
var response btcjson.Response

View file

@ -9,6 +9,33 @@ import (
"fmt"
)
// RPCVersion is a type to indicate RPC versions.
type RPCVersion string
const (
// version 1 of rpc
RpcVersion1 RPCVersion = RPCVersion("1.0")
// version 2 of rpc
RpcVersion2 RPCVersion = RPCVersion("2.0")
)
var validRpcVersions = []RPCVersion{RpcVersion1, RpcVersion2}
// check if the rpc version is a valid version
func (r RPCVersion) IsValid() bool {
for _, version := range validRpcVersions {
if version == r {
return true
}
}
return false
}
// cast rpc version to a string
func (r RPCVersion) String() string {
return string(r)
}
// RPCErrorCode represents an error code to be used as a part of an RPCError
// which is in turn used in a JSON-RPC Response object.
//
@ -67,21 +94,74 @@ func IsValidIDType(id interface{}) bool {
// requests, however this struct it being exported in case the caller wants to
// construct raw requests for some reason.
type Request struct {
Jsonrpc string `json:"jsonrpc"`
Jsonrpc RPCVersion `json:"jsonrpc"`
Method string `json:"method"`
Params []json.RawMessage `json:"params"`
ID interface{} `json:"id"`
}
// NewRequest returns a new JSON-RPC 1.0 request object given the provided id,
// method, and parameters. The parameters are marshalled into a json.RawMessage
// for the Params field of the returned request object. This function is only
// provided in case the caller wants to construct raw requests for some reason.
//
// Typically callers will instead want to create a registered concrete command
// type with the NewCmd or New<Foo>Cmd functions and call the MarshalCmd
// function with that command to generate the marshalled JSON-RPC request.
func NewRequest(id interface{}, method string, params []interface{}) (*Request, error) {
// UnmarshalJSON is a custom unmarshal func for the Request struct. The param
// field defaults to an empty json.RawMessage array it is omitted by the request
// or nil if the supplied value is invalid.
func (request *Request) UnmarshalJSON(b []byte) error {
// Step 1: Create a type alias of the original struct.
type Alias Request
// Step 2: Create an anonymous struct with raw replacements for the special
// fields.
aux := &struct {
Jsonrpc string `json:"jsonrpc"`
Params []interface{} `json:"params"`
*Alias
}{
Alias: (*Alias)(request),
}
// Step 3: Unmarshal the data into the anonymous struct.
err := json.Unmarshal(b, &aux)
if err != nil {
return err
}
// Step 4: Convert the raw fields to the desired types
version := RPCVersion(aux.Jsonrpc)
if version.IsValid() {
request.Jsonrpc = version
}
rawParams := make([]json.RawMessage, 0)
for _, param := range aux.Params {
marshalledParam, err := json.Marshal(param)
if err != nil {
return err
}
rawMessage := json.RawMessage(marshalledParam)
rawParams = append(rawParams, rawMessage)
}
request.Params = rawParams
return nil
}
// NewRequest returns a new JSON-RPC request object given the provided rpc
// version, id, method, and parameters. The parameters are marshalled into a
// json.RawMessage for the Params field of the returned request object. This
// function is only provided in case the caller wants to construct raw requests
// for some reason. Typically callers will instead want to create a registered
// concrete command type with the NewCmd or New<Foo>Cmd functions and call the
// MarshalCmd function with that command to generate the marshalled JSON-RPC
// request.
func NewRequest(rpcVersion RPCVersion, id interface{}, method string, params []interface{}) (*Request, error) {
// default to JSON-RPC 1.0 if RPC type is not specified
if !rpcVersion.IsValid() {
str := fmt.Sprintf("rpcversion '%s' is invalid", rpcVersion)
return nil, makeError(ErrInvalidType, str)
}
if !IsValidIDType(id) {
str := fmt.Sprintf("the id of type '%T' is invalid", id)
return nil, makeError(ErrInvalidType, str)
@ -98,30 +178,35 @@ func NewRequest(id interface{}, method string, params []interface{}) (*Request,
}
return &Request{
Jsonrpc: "1.0",
Jsonrpc: rpcVersion,
ID: id,
Method: method,
Params: rawParams,
}, nil
}
// Response is the general form of a JSON-RPC response. The type of the Result
// field varies from one command to the next, so it is implemented as an
// interface. The ID field has to be a pointer for Go to put a null in it when
// Response is the general form of a JSON-RPC response. The type of the
// Result field varies from one command to the next, so it is implemented as an
// interface. The ID field has to be a pointer to allow for a nil value when
// empty.
type Response struct {
Result json.RawMessage `json:"result"`
Error *RPCError `json:"error"`
ID *interface{} `json:"id"`
Jsonrpc RPCVersion `json:"jsonrpc"`
Result json.RawMessage `json:"result"`
Error *RPCError `json:"error"`
ID *interface{} `json:"id"`
}
// NewResponse returns a new JSON-RPC response object given the provided id,
// marshalled result, and RPC error. This function is only provided in case the
// caller wants to construct raw responses for some reason.
//
// NewResponse returns a new JSON-RPC response object given the provided rpc
// version, id, marshalled result, and RPC error. This function is only
// provided in case the caller wants to construct raw responses for some reason.
// Typically callers will instead want to create the fully marshalled JSON-RPC
// response to send over the wire with the MarshalResponse function.
func NewResponse(id interface{}, marshalledResult []byte, rpcErr *RPCError) (*Response, error) {
func NewResponse(rpcVersion RPCVersion, id interface{}, marshalledResult []byte, rpcErr *RPCError) (*Response, error) {
if !rpcVersion.IsValid() {
str := fmt.Sprintf("rpcversion '%s' is invalid", rpcVersion)
return nil, makeError(ErrInvalidType, str)
}
if !IsValidIDType(id) {
str := fmt.Sprintf("the id of type '%T' is invalid", id)
return nil, makeError(ErrInvalidType, str)
@ -129,20 +214,27 @@ func NewResponse(id interface{}, marshalledResult []byte, rpcErr *RPCError) (*Re
pid := &id
return &Response{
Result: marshalledResult,
Error: rpcErr,
ID: pid,
Jsonrpc: rpcVersion,
Result: marshalledResult,
Error: rpcErr,
ID: pid,
}, nil
}
// MarshalResponse marshals the passed id, result, and RPCError to a JSON-RPC
// response byte slice that is suitable for transmission to a JSON-RPC client.
func MarshalResponse(id interface{}, result interface{}, rpcErr *RPCError) ([]byte, error) {
// MarshalResponse marshals the passed rpc version, id, result, and RPCError to
// a JSON-RPC response byte slice that is suitable for transmission to a
// JSON-RPC client.
func MarshalResponse(rpcVersion RPCVersion, id interface{}, result interface{}, rpcErr *RPCError) ([]byte, error) {
if !rpcVersion.IsValid() {
str := fmt.Sprintf("rpcversion '%s' is invalid", rpcVersion)
return nil, makeError(ErrInvalidType, str)
}
marshalledResult, err := json.Marshal(result)
if err != nil {
return nil, err
}
response, err := NewResponse(id, marshalledResult, rpcErr)
response, err := NewResponse(rpcVersion, id, marshalledResult, rpcErr)
if err != nil {
return nil, err
}

View file

@ -68,7 +68,7 @@ func TestMarshalResponse(t *testing.T) {
name: "ordinary bool result with no error",
result: true,
jsonErr: nil,
expected: []byte(`{"result":true,"error":null,"id":1}`),
expected: []byte(`{"jsonrpc":"1.0","result":true,"error":null,"id":1}`),
},
{
name: "result with error",
@ -76,14 +76,14 @@ func TestMarshalResponse(t *testing.T) {
jsonErr: func() *btcjson.RPCError {
return btcjson.NewRPCError(btcjson.ErrRPCBlockNotFound, "123 not found")
}(),
expected: []byte(`{"result":null,"error":{"code":-5,"message":"123 not found"},"id":1}`),
expected: []byte(`{"jsonrpc":"1.0","result":null,"error":{"code":-5,"message":"123 not found"},"id":1}`),
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
_, _ = i, test
marshalled, err := btcjson.MarshalResponse(testID, test.result, test.jsonErr)
marshalled, err := btcjson.MarshalResponse(btcjson.RpcVersion1, testID, test.result, test.jsonErr)
if err != nil {
t.Errorf("Test #%d (%s) unexpected error: %v", i,
test.name, err)
@ -104,7 +104,7 @@ func TestMiscErrors(t *testing.T) {
// Force an error in NewRequest by giving it a parameter type that is
// not supported.
_, err := btcjson.NewRequest(nil, "test", []interface{}{make(chan int)})
_, err := btcjson.NewRequest(btcjson.RpcVersion1, nil, "test", []interface{}{make(chan int)})
if err == nil {
t.Error("NewRequest: did not receive error")
return
@ -113,7 +113,7 @@ func TestMiscErrors(t *testing.T) {
// Force an error in MarshalResponse by giving it an id type that is not
// supported.
wantErr := btcjson.Error{ErrorCode: btcjson.ErrInvalidType}
_, err = btcjson.MarshalResponse(make(chan int), nil, nil)
_, err = btcjson.MarshalResponse(btcjson.RpcVersion1, make(chan int), nil, nil)
if jerr, ok := err.(btcjson.Error); !ok || jerr.ErrorCode != wantErr.ErrorCode {
t.Errorf("MarshalResult: did not receive expected error - got "+
"%v (%[1]T), want %v (%[2]T)", err, wantErr)
@ -122,7 +122,7 @@ func TestMiscErrors(t *testing.T) {
// Force an error in MarshalResponse by giving it a result type that
// can't be marshalled.
_, err = btcjson.MarshalResponse(1, make(chan int), nil)
_, err = btcjson.MarshalResponse(btcjson.RpcVersion1, 1, make(chan int), nil)
if _, ok := err.(*json.UnsupportedTypeError); !ok {
wantErr := &json.UnsupportedTypeError{}
t.Errorf("MarshalResult: did not receive expected error - got "+

View file

@ -1800,7 +1800,7 @@ func TestWalletSvrCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
@ -1824,7 +1824,7 @@ func TestWalletSvrCmds(t *testing.T) {
// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)

View file

@ -195,7 +195,7 @@ func TestWalletSvrWsCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
@ -219,7 +219,7 @@ func TestWalletSvrWsCmds(t *testing.T) {
// Marshal the command as created by the generic new command
// creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd)
marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)

View file

@ -122,7 +122,7 @@ func TestWalletSvrWsNtfns(t *testing.T) {
for i, test := range tests {
// Marshal the notification as created by the new static
// creation function. The ID is nil for notifications.
marshalled, err := btcjson.MarshalCmd(nil, test.staticNtfn())
marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, test.staticNtfn())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
@ -147,7 +147,7 @@ func TestWalletSvrWsNtfns(t *testing.T) {
// Marshal the notification as created by the generic new
// notification creation function. The ID is nil for
// notifications.
marshalled, err = btcjson.MarshalCmd(nil, cmd)
marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, nil, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)

View file

@ -127,7 +127,7 @@ func main() {
// Marshal the command into a JSON-RPC byte slice in preparation for
// sending it to the RPC server.
marshalledJSON, err := btcjson.MarshalCmd(1, cmd)
marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, 1, cmd)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)

View file

@ -33,7 +33,7 @@ const (
// ensures its version either has the provided bit set or unset per the set
// flag.
func assertVersionBit(r *rpctest.Harness, t *testing.T, hash *chainhash.Hash, bit uint8, set bool) {
block, err := r.Node.GetBlock(hash)
block, err := r.Client.GetBlock(hash)
if err != nil {
t.Fatalf("failed to retrieve block %v: %v", hash, err)
}
@ -53,7 +53,7 @@ func assertVersionBit(r *rpctest.Harness, t *testing.T, hash *chainhash.Hash, bi
// assertChainHeight retrieves the current chain height from the given test
// harness and ensures it matches the provided expected height.
func assertChainHeight(r *rpctest.Harness, t *testing.T, expectedHeight uint32) {
height, err := r.Node.GetBlockCount()
height, err := r.Client.GetBlockCount()
if err != nil {
t.Fatalf("failed to retrieve block height: %v", err)
}
@ -96,7 +96,7 @@ func assertSoftForkStatus(r *rpctest.Harness, t *testing.T, forkKey string, stat
"threshold state %v to string", line, state)
}
info, err := r.Node.GetBlockChainInfo()
info, err := r.Client.GetBlockChainInfo()
if err != nil {
t.Fatalf("failed to retrieve chain info: %v", err)
}
@ -339,7 +339,7 @@ func TestBIP0009Mining(t *testing.T) {
// in the defined threshold state.
deployment := &r.ActiveNet.Deployments[chaincfg.DeploymentTestDummy]
testDummyBitNum := deployment.BitNumber
hashes, err := r.Node.Generate(1)
hashes, err := r.Client.Generate(1)
if err != nil {
t.Fatalf("unable to generate blocks: %v", err)
}
@ -358,7 +358,7 @@ func TestBIP0009Mining(t *testing.T) {
// dummy deployment as started.
confirmationWindow := r.ActiveNet.MinerConfirmationWindow
numNeeded := confirmationWindow - 1
hashes, err = r.Node.Generate(numNeeded)
hashes, err = r.Client.Generate(numNeeded)
if err != nil {
t.Fatalf("failed to generated %d blocks: %v", numNeeded, err)
}
@ -373,7 +373,7 @@ func TestBIP0009Mining(t *testing.T) {
// The last generated block should still have the test bit set in the
// version since the btcd mining code will have recognized the test
// dummy deployment as locked in.
hashes, err = r.Node.Generate(confirmationWindow)
hashes, err = r.Client.Generate(confirmationWindow)
if err != nil {
t.Fatalf("failed to generated %d blocks: %v", confirmationWindow,
err)
@ -392,7 +392,7 @@ func TestBIP0009Mining(t *testing.T) {
// version since the btcd mining code will have recognized the test
// dummy deployment as activated and thus there is no longer any need
// to set the bit.
hashes, err = r.Node.Generate(confirmationWindow)
hashes, err = r.Client.Generate(confirmationWindow)
if err != nil {
t.Fatalf("failed to generated %d blocks: %v", confirmationWindow,
err)

View file

@ -57,14 +57,14 @@ func makeTestOutput(r *rpctest.Harness, t *testing.T,
if err != nil {
return nil, nil, nil, err
}
txHash, err := r.Node.SendRawTransaction(fundTx, true)
txHash, err := r.Client.SendRawTransaction(fundTx, true)
if err != nil {
return nil, nil, nil, err
}
// The transaction created above should be included within the next
// generated block.
blockHash, err := r.Node.Generate(1)
blockHash, err := r.Client.Generate(1)
if err != nil {
return nil, nil, nil, err
}
@ -151,7 +151,7 @@ func TestBIP0113Activation(t *testing.T) {
// We set the lock-time of the transaction to just one minute after the
// current MTP of the chain.
chainInfo, err := r.Node.GetBlockChainInfo()
chainInfo, err := r.Client.GetBlockChainInfo()
if err != nil {
t.Fatalf("unable to query for chain info: %v", err)
}
@ -167,7 +167,7 @@ func TestBIP0113Activation(t *testing.T) {
// This transaction should be rejected from the mempool as using MTP
// for transactions finality is now a policy rule. Additionally, the
// exact error should be the rejection of a non-final transaction.
_, err = r.Node.SendRawTransaction(tx, true)
_, err = r.Client.SendRawTransaction(tx, true)
if err == nil {
t.Fatalf("transaction accepted, but should be non-final")
} else if !strings.Contains(err.Error(), "not finalized") {
@ -201,7 +201,7 @@ func TestBIP0113Activation(t *testing.T) {
// height 299. The getblockchaininfo call checks the state for the
// block AFTER the current height.
numBlocks := (r.ActiveNet.MinerConfirmationWindow * 2) - 4
if _, err := r.Node.Generate(numBlocks); err != nil {
if _, err := r.Client.Generate(numBlocks); err != nil {
t.Fatalf("unable to generate blocks: %v", err)
}
@ -220,7 +220,7 @@ func TestBIP0113Activation(t *testing.T) {
// rejected.
timeLockDeltas := []int64{-1, 0, 1}
for _, timeLockDelta := range timeLockDeltas {
chainInfo, err = r.Node.GetBlockChainInfo()
chainInfo, err = r.Client.GetBlockChainInfo()
if err != nil {
t.Fatalf("unable to query for chain info: %v", err)
}
@ -257,7 +257,7 @@ func TestBIP0113Activation(t *testing.T) {
// accepted as it has a lock-time of one
// second _before_ the current MTP.
_, err = r.Node.SendRawTransaction(tx, true)
_, err = r.Client.SendRawTransaction(tx, true)
if err == nil && timeLockDelta >= 0 {
t.Fatal("transaction was accepted into the mempool " +
"but should be rejected!")
@ -366,7 +366,7 @@ func spendCSVOutput(redeemScript []byte, csvUTXO *wire.OutPoint,
func assertTxInBlock(r *rpctest.Harness, t *testing.T, blockHash *chainhash.Hash,
txid *chainhash.Hash) {
block, err := r.Node.GetBlock(blockHash)
block, err := r.Client.GetBlock(blockHash)
if err != nil {
t.Fatalf("unable to get block: %v", err)
}
@ -449,10 +449,10 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) {
// As the transaction is p2sh it should be accepted into the
// mempool and found within the next generated block.
if _, err := r.Node.SendRawTransaction(tx, true); err != nil {
if _, err := r.Client.SendRawTransaction(tx, true); err != nil {
t.Fatalf("unable to broadcast tx: %v", err)
}
blocks, err := r.Node.Generate(1)
blocks, err := r.Client.Generate(1)
if err != nil {
t.Fatalf("unable to generate blocks: %v", err)
}
@ -469,7 +469,7 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) {
// This transaction should be rejected from the mempool since
// CSV validation is already mempool policy pre-fork.
_, err = r.Node.SendRawTransaction(spendingTx, true)
_, err = r.Client.SendRawTransaction(spendingTx, true)
if err == nil {
t.Fatalf("transaction should have been rejected, but was " +
"instead accepted")
@ -496,7 +496,7 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) {
// height 299. The getblockchaininfo call checks the state for the
// block AFTER the current height.
numBlocks := (r.ActiveNet.MinerConfirmationWindow * 2) - 8
if _, err := r.Node.Generate(numBlocks); err != nil {
if _, err := r.Client.Generate(numBlocks); err != nil {
t.Fatalf("unable to generate blocks: %v", err)
}
@ -530,7 +530,7 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) {
t.Fatalf("unable to create CSV output: %v", err)
}
if _, err := r.Node.SendRawTransaction(tx, true); err != nil {
if _, err := r.Client.SendRawTransaction(tx, true); err != nil {
t.Fatalf("unable to broadcast transaction: %v", err)
}
@ -542,17 +542,17 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) {
}
// Mine a single block including all the transactions generated above.
if _, err := r.Node.Generate(1); err != nil {
if _, err := r.Client.Generate(1); err != nil {
t.Fatalf("unable to generate block: %v", err)
}
// Now mine 10 additional blocks giving the inputs generated above a
// age of 11. Space out each block 10 minutes after the previous block.
prevBlockHash, err := r.Node.GetBestBlockHash()
prevBlockHash, err := r.Client.GetBestBlockHash()
if err != nil {
t.Fatalf("unable to get prior block hash: %v", err)
}
prevBlock, err := r.Node.GetBlock(prevBlockHash)
prevBlock, err := r.Client.GetBlock(prevBlockHash)
if err != nil {
t.Fatalf("unable to get block: %v", err)
}
@ -652,7 +652,7 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) {
}
for i, test := range tests {
txid, err := r.Node.SendRawTransaction(test.tx, true)
txid, err := r.Client.SendRawTransaction(test.tx, true)
switch {
// Test case passes, nothing further to report.
case test.accept && err == nil:
@ -686,7 +686,7 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) {
// Generate a block, the transaction should be included within
// the newly mined block.
blockHashes, err := r.Node.Generate(1)
blockHashes, err := r.Client.Generate(1)
if err != nil {
t.Fatalf("unable to mine block: %v", err)
}

View file

@ -15,22 +15,24 @@ import (
"testing"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/integration/rpctest"
"github.com/btcsuite/btcd/rpcclient"
)
func testGetBestBlock(r *rpctest.Harness, t *testing.T) {
_, prevbestHeight, err := r.Node.GetBestBlock()
_, prevbestHeight, err := r.Client.GetBestBlock()
if err != nil {
t.Fatalf("Call to `getbestblock` failed: %v", err)
}
// Create a new block connecting to the current tip.
generatedBlockHashes, err := r.Node.Generate(1)
generatedBlockHashes, err := r.Client.Generate(1)
if err != nil {
t.Fatalf("Unable to generate block: %v", err)
}
bestHash, bestHeight, err := r.Node.GetBestBlock()
bestHash, bestHeight, err := r.Client.GetBestBlock()
if err != nil {
t.Fatalf("Call to `getbestblock` failed: %v", err)
}
@ -50,17 +52,17 @@ func testGetBestBlock(r *rpctest.Harness, t *testing.T) {
func testGetBlockCount(r *rpctest.Harness, t *testing.T) {
// Save the current count.
currentCount, err := r.Node.GetBlockCount()
currentCount, err := r.Client.GetBlockCount()
if err != nil {
t.Fatalf("Unable to get block count: %v", err)
}
if _, err := r.Node.Generate(1); err != nil {
if _, err := r.Client.Generate(1); err != nil {
t.Fatalf("Unable to generate block: %v", err)
}
// Count should have increased by one.
newCount, err := r.Node.GetBlockCount()
newCount, err := r.Client.GetBlockCount()
if err != nil {
t.Fatalf("Unable to get block count: %v", err)
}
@ -72,17 +74,17 @@ func testGetBlockCount(r *rpctest.Harness, t *testing.T) {
func testGetBlockHash(r *rpctest.Harness, t *testing.T) {
// Create a new block connecting to the current tip.
generatedBlockHashes, err := r.Node.Generate(1)
generatedBlockHashes, err := r.Client.Generate(1)
if err != nil {
t.Fatalf("Unable to generate block: %v", err)
}
info, err := r.Node.GetInfo()
info, err := r.Client.GetInfo()
if err != nil {
t.Fatalf("call to getinfo cailed: %v", err)
}
blockHash, err := r.Node.GetBlockHash(int64(info.Blocks))
blockHash, err := r.Client.GetBlockHash(int64(info.Blocks))
if err != nil {
t.Fatalf("Call to `getblockhash` failed: %v", err)
}
@ -94,10 +96,50 @@ func testGetBlockHash(r *rpctest.Harness, t *testing.T) {
}
}
func testBulkClient(r *rpctest.Harness, t *testing.T) {
// Create a new block connecting to the current tip.
generatedBlockHashes, err := r.Client.Generate(20)
if err != nil {
t.Fatalf("Unable to generate block: %v", err)
}
var futureBlockResults []rpcclient.FutureGetBlockResult
for _, hash := range generatedBlockHashes {
futureBlockResults = append(futureBlockResults, r.BatchClient.GetBlockAsync(hash))
}
err = r.BatchClient.Send()
if err != nil {
t.Fatal(err)
}
isKnownBlockHash := func(blockHash chainhash.Hash) bool {
for _, hash := range generatedBlockHashes {
if blockHash.IsEqual(hash) {
return true
}
}
return false
}
for _, block := range futureBlockResults {
msgBlock, err := block.Receive()
if err != nil {
t.Fatal(err)
}
blockHash := msgBlock.Header.BlockHash()
if !isKnownBlockHash(blockHash) {
t.Fatalf("expected hash %s to be in generated hash list", blockHash)
}
}
}
var rpcTestCases = []rpctest.HarnessTestCase{
testGetBestBlock,
testGetBlockCount,
testGetBlockHash,
testBulkClient,
}
var primaryHarness *rpctest.Harness

View file

@ -92,9 +92,10 @@ type Harness struct {
// attempts.
ConnectionRetryTimeout time.Duration
Node *rpcclient.Client
node *node
handlers *rpcclient.NotificationHandlers
Client *rpcclient.Client
BatchClient *rpcclient.Client
node *node
handlers *rpcclient.NotificationHandlers
wallet *memWallet
@ -245,13 +246,13 @@ func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error {
// Filter transactions that pay to the coinbase associated with the
// wallet.
filterAddrs := []btcutil.Address{h.wallet.coinbaseAddr}
if err := h.Node.LoadTxFilter(true, filterAddrs, nil); err != nil {
if err := h.Client.LoadTxFilter(true, filterAddrs, nil); err != nil {
return err
}
// Ensure btcd properly dispatches our registered call-back for each new
// block. Otherwise, the memWallet won't function properly.
if err := h.Node.NotifyBlocks(); err != nil {
if err := h.Client.NotifyBlocks(); err != nil {
return err
}
@ -260,7 +261,7 @@ func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error {
if createTestChain && numMatureOutputs != 0 {
numToGenerate := (uint32(h.ActiveNet.CoinbaseMaturity) +
numMatureOutputs)
_, err := h.Node.Generate(numToGenerate)
_, err := h.Client.Generate(numToGenerate)
if err != nil {
return err
}
@ -268,7 +269,7 @@ func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error {
// Block until the wallet has fully synced up to the tip of the main
// chain.
_, height, err := h.Node.GetBestBlock()
_, height, err := h.Client.GetBestBlock()
if err != nil {
return err
}
@ -289,8 +290,12 @@ func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error {
//
// This function MUST be called with the harness state mutex held (for writes).
func (h *Harness) tearDown() error {
if h.Node != nil {
h.Node.Shutdown()
if h.Client != nil {
h.Client.Shutdown()
}
if h.BatchClient != nil {
h.BatchClient.Shutdown()
}
if err := h.node.shutdown(); err != nil {
@ -325,24 +330,38 @@ func (h *Harness) TearDown() error {
// we're not able to establish a connection, this function returns with an
// error.
func (h *Harness) connectRPCClient() error {
var client *rpcclient.Client
var client, batchClient *rpcclient.Client
var err error
rpcConf := h.node.config.rpcConnConfig()
batchConf := h.node.config.rpcConnConfig()
batchConf.HTTPPostMode = true
for i := 0; i < h.MaxConnRetries; i++ {
if client, err = rpcclient.New(&rpcConf, h.handlers); err != nil {
time.Sleep(time.Duration(i) * h.ConnectionRetryTimeout)
continue
fail := false
if client == nil {
if client, err = rpcclient.New(&rpcConf, h.handlers); err != nil {
time.Sleep(time.Duration(i) * h.ConnectionRetryTimeout)
fail = true
}
}
if batchClient == nil {
if batchClient, err = rpcclient.NewBatch(&batchConf); err != nil {
time.Sleep(time.Duration(i) * h.ConnectionRetryTimeout)
fail = true
}
}
if !fail {
break
}
break
}
if client == nil {
if client == nil || batchClient == nil {
return fmt.Errorf("connection timeout")
}
h.Node = client
h.Client = client
h.wallet.SetRPCClient(client)
h.BatchClient = batchClient
return nil
}
@ -464,11 +483,11 @@ func (h *Harness) GenerateAndSubmitBlockWithCustomCoinbaseOutputs(
blockVersion = BlockVersion
}
prevBlockHash, prevBlockHeight, err := h.Node.GetBestBlock()
prevBlockHash, prevBlockHeight, err := h.Client.GetBestBlock()
if err != nil {
return nil, err
}
mBlock, err := h.Node.GetBlock(prevBlockHash)
mBlock, err := h.Client.GetBlock(prevBlockHash)
if err != nil {
return nil, err
}
@ -483,7 +502,7 @@ func (h *Harness) GenerateAndSubmitBlockWithCustomCoinbaseOutputs(
}
// Submit the block to the simnet node.
if err := h.Node.SubmitBlock(newBlock, nil); err != nil {
if err := h.Client.SubmitBlock(newBlock, nil); err != nil {
return nil, err
}

View file

@ -43,7 +43,7 @@ func testSendOutputs(r *Harness, t *testing.T) {
}
assertTxMined := func(txid *chainhash.Hash, blockHash *chainhash.Hash) {
block, err := r.Node.GetBlock(blockHash)
block, err := r.Client.GetBlock(blockHash)
if err != nil {
t.Fatalf("unable to get block: %v", err)
}
@ -67,7 +67,7 @@ func testSendOutputs(r *Harness, t *testing.T) {
// Generate a single block, the transaction the wallet created should
// be found in this block.
blockHashes, err := r.Node.Generate(1)
blockHashes, err := r.Client.Generate(1)
if err != nil {
t.Fatalf("unable to generate single block: %v", err)
}
@ -76,7 +76,7 @@ func testSendOutputs(r *Harness, t *testing.T) {
// Next, generate a spend much greater than the block reward. This
// transaction should also have been mined properly.
txid = genSpend(btcutil.Amount(500 * btcutil.SatoshiPerBitcoin))
blockHashes, err = r.Node.Generate(1)
blockHashes, err = r.Client.Generate(1)
if err != nil {
t.Fatalf("unable to generate single block: %v", err)
}
@ -84,7 +84,7 @@ func testSendOutputs(r *Harness, t *testing.T) {
}
func assertConnectedTo(t *testing.T, nodeA *Harness, nodeB *Harness) {
nodeAPeers, err := nodeA.Node.GetPeerInfo()
nodeAPeers, err := nodeA.Client.GetPeerInfo()
if err != nil {
t.Fatalf("unable to get nodeA's peer info")
}
@ -170,7 +170,7 @@ func testActiveHarnesses(r *Harness, t *testing.T) {
func testJoinMempools(r *Harness, t *testing.T) {
// Assert main test harness has no transactions in its mempool.
pooledHashes, err := r.Node.GetRawMempool()
pooledHashes, err := r.Client.GetRawMempool()
if err != nil {
t.Fatalf("unable to get mempool for main test harness: %v", err)
}
@ -210,7 +210,7 @@ func testJoinMempools(r *Harness, t *testing.T) {
if err != nil {
t.Fatalf("coinbase spend failed: %v", err)
}
if _, err := r.Node.SendRawTransaction(testTx, true); err != nil {
if _, err := r.Client.SendRawTransaction(testTx, true); err != nil {
t.Fatalf("send transaction failed: %v", err)
}
@ -219,7 +219,7 @@ func testJoinMempools(r *Harness, t *testing.T) {
harnessSynced := make(chan struct{})
go func() {
for {
poolHashes, err := r.Node.GetRawMempool()
poolHashes, err := r.Client.GetRawMempool()
if err != nil {
t.Fatalf("failed to retrieve harness mempool: %v", err)
}
@ -262,7 +262,7 @@ func testJoinMempools(r *Harness, t *testing.T) {
// Send the transaction to the local harness which will result in synced
// mempools.
if _, err := harness.Node.SendRawTransaction(testTx, true); err != nil {
if _, err := harness.Client.SendRawTransaction(testTx, true); err != nil {
t.Fatalf("send transaction failed: %v", err)
}
@ -612,7 +612,7 @@ func TestHarness(t *testing.T) {
// Current tip should be at a height of numMatureOutputs plus the
// required number of blocks for coinbase maturity.
nodeInfo, err := mainHarness.Node.GetInfo()
nodeInfo, err := mainHarness.Client.GetInfo()
if err != nil {
t.Fatalf("unable to execute getinfo on node: %v", err)
}

View file

@ -49,7 +49,7 @@ func syncMempools(nodes []*Harness) error {
retry:
for !poolsMatch {
firstPool, err := nodes[0].Node.GetRawMempool()
firstPool, err := nodes[0].Client.GetRawMempool()
if err != nil {
return err
}
@ -58,7 +58,7 @@ retry:
// first node, then we're done. Otherwise, drop back to the top
// of the loop and retry after a short wait period.
for _, node := range nodes[1:] {
nodePool, err := node.Node.GetRawMempool()
nodePool, err := node.Client.GetRawMempool()
if err != nil {
return err
}
@ -84,7 +84,7 @@ retry:
var prevHash *chainhash.Hash
var prevHeight int32
for _, node := range nodes {
blockHash, blockHeight, err := node.Node.GetBestBlock()
blockHash, blockHeight, err := node.Client.GetBestBlock()
if err != nil {
return err
}
@ -108,24 +108,24 @@ retry:
// therefore in the case of disconnects, "from" will attempt to reestablish a
// connection to the "to" harness.
func ConnectNode(from *Harness, to *Harness) error {
peerInfo, err := from.Node.GetPeerInfo()
peerInfo, err := from.Client.GetPeerInfo()
if err != nil {
return err
}
numPeers := len(peerInfo)
targetAddr := to.node.config.listen
if err := from.Node.AddNode(targetAddr, rpcclient.ANAdd); err != nil {
if err := from.Client.AddNode(targetAddr, rpcclient.ANAdd); err != nil {
return err
}
// Block until a new connection has been established.
peerInfo, err = from.Node.GetPeerInfo()
peerInfo, err = from.Client.GetPeerInfo()
if err != nil {
return err
}
for len(peerInfo) <= numPeers {
peerInfo, err = from.Node.GetPeerInfo()
peerInfo, err = from.Client.GetPeerInfo()
if err != nil {
return err
}

View file

@ -0,0 +1,31 @@
Bitcoin Core Batch HTTP POST Example
==============================
This example shows how to use the rpclient package to connect to a Bitcoin Core RPC server using HTTP POST and batch JSON-RPC mode with TLS disabled.
## Running the Example
The first step is to use `go get` to download and install the rpcclient package:
```bash
$ go get github.com/btcsuite/btcd/rpcclient
```
Next, modify the `main.go` source to specify the correct RPC username and
password for the RPC server:
```Go
User: "yourrpcuser",
Pass: "yourrpcpass",
```
Finally, navigate to the example's directory and run it with:
```bash
$ cd $GOPATH/src/github.com/btcsuite/btcd/rpcclient/examples/bitcoincorehttp
$ go run *.go
```
## License
This example is licensed under the [copyfree](http://copyfree.org) ISC License.

View file

@ -0,0 +1,46 @@
// Copyright (c) 2014-2020 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"log"
"github.com/btcsuite/btcd/rpcclient"
)
func main() {
// Connect to local bitcoin core RPC server using HTTP POST mode.
connCfg := &rpcclient.ConnConfig{
Host: "localhost:8332",
User: "yourrpcuser",
Pass: "yourrpcpass",
DisableConnectOnNew: true,
HTTPPostMode: true, // Bitcoin core only supports HTTP POST mode
DisableTLS: true, // Bitcoin core does not provide TLS by default
}
batchClient, err := rpcclient.NewBatch(connCfg)
defer batchClient.Shutdown()
if err != nil {
log.Fatal(err)
}
// batch mode requires async requests
blockCount := batchClient.GetBlockCountAsync()
block1 := batchClient.GetBlockHashAsync(1)
batchClient.GetBlockHashAsync(2)
batchClient.GetBlockHashAsync(3)
block4 := batchClient.GetBlockHashAsync(4)
difficulty := batchClient.GetDifficultyAsync()
// sends all queued batch requests
batchClient.Send()
fmt.Println(blockCount.Receive())
fmt.Println(block1.Receive())
fmt.Println(block4.Receive())
fmt.Println(difficulty.Receive())
}

View file

@ -163,6 +163,10 @@ type Client struct {
// disconnected indicated whether or not the server is disconnected.
disconnected bool
// whether or not to batch requests, false unless changed by Batch()
batch bool
batchList *list.List
// retryCount holds the number of times the client has tried to
// reconnect to the RPC server.
retryCount int64
@ -220,8 +224,13 @@ func (c *Client) addRequest(jReq *jsonRequest) error {
default:
}
element := c.requestList.PushBack(jReq)
c.requestMap[jReq.id] = element
if !c.batch {
element := c.requestList.PushBack(jReq)
c.requestMap[jReq.id] = element
} else {
element := c.batchList.PushBack(jReq)
c.requestMap[jReq.id] = element
}
return nil
}
@ -289,6 +298,41 @@ func (c *Client) trackRegisteredNtfns(cmd interface{}) {
}
}
// FutureGetBulkResult waits for the responses promised by the future
// and returns them in a channel
type FutureGetBulkResult chan *response
// Receive waits for the response promised by the future and returns an map
// of results by request id
func (r FutureGetBulkResult) Receive() (BulkResult, error) {
m := make(BulkResult)
res, err := receiveFuture(r)
if err != nil {
return nil, err
}
var arr []IndividualBulkResult
err = json.Unmarshal(res, &arr)
if err != nil {
return nil, err
}
for _, results := range arr {
m[results.Id] = results
}
return m, nil
}
// IndividualBulkResult represents one result
// from a bulk json rpc api
type IndividualBulkResult struct {
Result interface{} `json:"result"`
Error *btcjson.RPCError `json:"error"`
Id uint64 `json:"id"`
}
type BulkResult = map[uint64]IndividualBulkResult
// inMessage is the first type that an incoming message is unmarshaled
// into. It supports both requests (for notification support) and
// responses. The partially-unmarshaled message is a notification if
@ -741,7 +785,12 @@ func (c *Client) handleSendPostMessage(details *sendPostDetails) {
// Try to unmarshal the response as a regular JSON-RPC response.
var resp rawResponse
err = json.Unmarshal(respBytes, &resp)
var batchResponse json.RawMessage
if c.batch {
err = json.Unmarshal(respBytes, &batchResponse)
} else {
err = json.Unmarshal(respBytes, &resp)
}
if err != nil {
// When the response itself isn't a valid JSON-RPC response
// return an error which includes the HTTP status code and raw
@ -751,8 +800,14 @@ func (c *Client) handleSendPostMessage(details *sendPostDetails) {
jReq.responseChan <- &response{err: err}
return
}
res, err := resp.result()
var res []byte
if c.batch {
// errors must be dealt with downstream since a whole request cannot
// "error out" other than through the status code error handled above
res, err = batchResponse, nil
} else {
res, err = resp.result()
}
jReq.responseChan <- &response{result: res, err: err}
}
@ -875,7 +930,13 @@ func (c *Client) sendRequest(jReq *jsonRequest) {
// POST mode, the command is issued via an HTTP client. Otherwise,
// the command is issued via the asynchronous websocket channels.
if c.config.HTTPPostMode {
c.sendPost(jReq)
if c.batch {
if err := c.addRequest(jReq); err != nil {
log.Warn(err)
}
} else {
c.sendPost(jReq)
}
return
}
@ -905,6 +966,10 @@ func (c *Client) sendRequest(jReq *jsonRequest) {
// future. It handles both websocket and HTTP POST mode depending on the
// configuration of the client.
func (c *Client) sendCmd(cmd interface{}) chan *response {
rpcVersion := btcjson.RpcVersion1
if c.batch {
rpcVersion = btcjson.RpcVersion2
}
// Get the method associated with the command.
method, err := btcjson.CmdMethod(cmd)
if err != nil {
@ -913,7 +978,7 @@ func (c *Client) sendCmd(cmd interface{}) chan *response {
// Marshal the command.
id := c.NextID()
marshalledJSON, err := btcjson.MarshalCmd(id, cmd)
marshalledJSON, err := btcjson.MarshalCmd(rpcVersion, id, cmd)
if err != nil {
return newFutureError(err)
}
@ -927,6 +992,7 @@ func (c *Client) sendCmd(cmd interface{}) chan *response {
marshalledJSON: marshalledJSON,
responseChan: responseChan,
}
c.sendRequest(jReq)
return responseChan
@ -1357,6 +1423,8 @@ func New(config *ConnConfig, ntfnHandlers *NotificationHandlers) (*Client, error
httpClient: httpClient,
requestMap: make(map[uint64]*list.Element),
requestList: list.New(),
batch: false,
batchList: list.New(),
ntfnHandlers: ntfnHandlers,
ntfnState: newNotificationState(),
sendChan: make(chan []byte, sendBufferSize),
@ -1397,6 +1465,24 @@ func New(config *ConnConfig, ntfnHandlers *NotificationHandlers) (*Client, error
return client, nil
}
// Batch is a factory that creates a client able to interact with the server using
// JSON-RPC 2.0. The client is capable of accepting an arbitrary number of requests
// and having the server process the all at the same time. It's compatible with both
// btcd and bitcoind
func NewBatch(config *ConnConfig) (*Client, error) {
if !config.HTTPPostMode {
return nil, errors.New("http post mode is required to use batch client")
}
// notification parameter is nil since notifications are not supported in POST mode.
client, err := New(config, nil)
if err != nil {
return nil, err
}
client.batch = true //copy the client with changed batch setting
client.start()
return client, nil
}
// Connect establishes the initial websocket connection. This is necessary when
// a client was created after setting the DisableConnectOnNew field of the
// Config struct.
@ -1534,3 +1620,69 @@ func (c *Client) BackendVersion() (BackendVersion, error) {
return *c.backendVersion, nil
}
func (c *Client) sendAsync() FutureGetBulkResult {
// convert the array of marshalled json requests to a single request we can send
responseChan := make(chan *response, 1)
marshalledRequest := []byte("[")
for iter := c.batchList.Front(); iter != nil; iter = iter.Next() {
request := iter.Value.(*jsonRequest)
marshalledRequest = append(marshalledRequest, request.marshalledJSON...)
marshalledRequest = append(marshalledRequest, []byte(",")...)
}
if len(marshalledRequest) > 0 {
// removes the trailing comma to process the request individually
marshalledRequest = marshalledRequest[:len(marshalledRequest)-1]
}
marshalledRequest = append(marshalledRequest, []byte("]")...)
request := jsonRequest{
id: c.NextID(),
method: "",
cmd: nil,
marshalledJSON: marshalledRequest,
responseChan: responseChan,
}
c.sendPost(&request)
return responseChan
}
// Marshall's bulk requests and sends to the server
// creates a response channel to receive the response
func (c *Client) Send() error {
// if batchlist is empty, there's nothing to send
if c.batchList.Len() == 0 {
return nil
}
// clear batchlist in case of an error
defer func() {
c.batchList = list.New()
}()
result, err := c.sendAsync().Receive()
if err != nil {
return err
}
for iter := c.batchList.Front(); iter != nil; iter = iter.Next() {
var requestError error
request := iter.Value.(*jsonRequest)
individualResult := result[request.id]
fullResult, err := json.Marshal(individualResult.Result)
if err != nil {
return err
}
if individualResult.Error != nil {
requestError = individualResult.Error
}
result := response{
result: fullResult,
err: requestError,
}
request.responseChan <- &result
}
return nil
}

View file

@ -44,7 +44,7 @@ func (c *Client) RawRequestAsync(method string, params []json.RawMessage) Future
// than custom commands.
id := c.NextID()
rawRequest := &btcjson.Request{
Jsonrpc: "1.0",
Jsonrpc: btcjson.RpcVersion1,
ID: id,
Method: method,
Params: params,

View file

@ -101,6 +101,9 @@ var (
// declared here to avoid the overhead of creating the slice on every
// invocation for constant data.
gbtCapabilities = []string{"proposal"}
// JSON 2.0 batched request prefix
batchedRequestPrefix = []byte("[")
)
// Errors
@ -3939,10 +3942,11 @@ func (s *rpcServer) checkAuth(r *http.Request, require bool) (bool, bool, error)
// a known concrete command along with any error that might have happened while
// parsing it.
type parsedRPCCmd struct {
id interface{}
method string
cmd interface{}
err *btcjson.RPCError
jsonrpc btcjson.RPCVersion
id interface{}
method string
cmd interface{}
err *btcjson.RPCError
}
// standardCmdResult checks that a parsed command is a standard Bitcoin JSON-RPC
@ -3975,9 +3979,11 @@ handled:
// is suitable for use in replies if the command is invalid in some way such as
// an unregistered command or invalid parameters.
func parseCmd(request *btcjson.Request) *parsedRPCCmd {
var parsedCmd parsedRPCCmd
parsedCmd.id = request.ID
parsedCmd.method = request.Method
parsedCmd := parsedRPCCmd{
jsonrpc: request.Jsonrpc,
id: request.ID,
method: request.Method,
}
cmd, err := btcjson.UnmarshalCmd(request)
if err != nil {
@ -4004,7 +4010,7 @@ func parseCmd(request *btcjson.Request) *parsedRPCCmd {
// createMarshalledReply returns a new marshalled JSON-RPC response given the
// passed parameters. It will automatically convert errors that are not of
// the type *btcjson.RPCError to the appropriate type as needed.
func createMarshalledReply(id, result interface{}, replyErr error) ([]byte, error) {
func createMarshalledReply(rpcVersion btcjson.RPCVersion, id interface{}, result interface{}, replyErr error) ([]byte, error) {
var jsonErr *btcjson.RPCError
if replyErr != nil {
if jErr, ok := replyErr.(*btcjson.RPCError); ok {
@ -4014,7 +4020,67 @@ func createMarshalledReply(id, result interface{}, replyErr error) ([]byte, erro
}
}
return btcjson.MarshalResponse(id, result, jsonErr)
return btcjson.MarshalResponse(rpcVersion, id, result, jsonErr)
}
// processRequest determines the incoming request type (single or batched),
// parses it and returns a marshalled response.
func (s *rpcServer) processRequest(request *btcjson.Request, isAdmin bool, closeChan <-chan struct{}) []byte {
var result interface{}
var err error
var jsonErr *btcjson.RPCError
if !isAdmin {
if _, ok := rpcLimited[request.Method]; !ok {
jsonErr = internalRPCError("limited user not "+
"authorized for this method", "")
}
}
if jsonErr == nil {
if request.Method == "" || request.Params == nil {
jsonErr = &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidRequest.Code,
Message: "Invalid request: malformed",
}
msg, err := createMarshalledReply(request.Jsonrpc, request.ID, result, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to marshal reply: %v", err)
return nil
}
return msg
}
// Valid requests with no ID (notifications) must not have a response
// per the JSON-RPC spec.
if request.ID == nil {
return nil
}
// Attempt to parse the JSON-RPC request into a known
// concrete command.
parsedCmd := parseCmd(request)
if parsedCmd.err != nil {
jsonErr = parsedCmd.err
} else {
result, err = s.standardCmdResult(parsedCmd,
closeChan)
if err != nil {
jsonErr = &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidRequest.Code,
Message: "Invalid request: malformed",
}
}
}
}
// Marshal the response.
msg, err := createMarshalledReply(request.Jsonrpc, request.ID, result, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to marshal reply: %v", err)
return nil
}
return msg
}
// jsonRPCRead handles reading and responding to RPC messages.
@ -4059,80 +4125,186 @@ func (s *rpcServer) jsonRPCRead(w http.ResponseWriter, r *http.Request, isAdmin
conn.SetReadDeadline(timeZeroVal)
// Attempt to parse the raw body into a JSON-RPC request.
var responseID interface{}
var jsonErr error
var result interface{}
var request btcjson.Request
if err := json.Unmarshal(body, &request); err != nil {
jsonErr = &btcjson.RPCError{
Code: btcjson.ErrRPCParse.Code,
Message: "Failed to parse request: " + err.Error(),
// Setup a close notifier. Since the connection is hijacked,
// the CloseNotifer on the ResponseWriter is not available.
closeChan := make(chan struct{}, 1)
go func() {
_, err = conn.Read(make([]byte, 1))
if err != nil {
close(closeChan)
}
}()
var results []json.RawMessage
var batchSize int
var batchedRequest bool
// Determine request type
if bytes.HasPrefix(body, batchedRequestPrefix) {
batchedRequest = true
}
// Process a single request
if !batchedRequest {
var req btcjson.Request
var resp json.RawMessage
err = json.Unmarshal(body, &req)
if err != nil {
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCParse.Code,
Message: fmt.Sprintf("Failed to parse request: %v",
err),
}
resp, err = btcjson.MarshalResponse(btcjson.RpcVersion1, nil, nil, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to create reply: %v", err)
}
}
if err == nil {
// The JSON-RPC 1.0 spec defines that notifications must have their "id"
// set to null and states that notifications do not have a response.
//
// A JSON-RPC 2.0 notification is a request with "json-rpc":"2.0", and
// without an "id" member. The specification states that notifications
// must not be responded to. JSON-RPC 2.0 permits the null value as a
// valid request id, therefore such requests are not notifications.
//
// Bitcoin Core serves requests with "id":null or even an absent "id",
// and responds to such requests with "id":null in the response.
//
// Btcd does not respond to any request without and "id" or "id":null,
// regardless the indicated JSON-RPC protocol version unless RPC quirks
// are enabled. With RPC quirks enabled, such requests will be responded
// to if the reqeust does not indicate JSON-RPC version.
//
// RPC quirks can be enabled by the user to avoid compatibility issues
// with software relying on Core's behavior.
if req.ID == nil && !(cfg.RPCQuirks && req.Jsonrpc == "") {
return
}
resp = s.processRequest(&req, isAdmin, closeChan)
}
if resp != nil {
results = append(results, resp)
}
}
if jsonErr == nil {
// The JSON-RPC 1.0 spec defines that notifications must have their "id"
// set to null and states that notifications do not have a response.
//
// A JSON-RPC 2.0 notification is a request with "json-rpc":"2.0", and
// without an "id" member. The specification states that notifications
// must not be responded to. JSON-RPC 2.0 permits the null value as a
// valid request id, therefore such requests are not notifications.
//
// Bitcoin Core serves requests with "id":null or even an absent "id",
// and responds to such requests with "id":null in the response.
//
// Btcd does not respond to any request without and "id" or "id":null,
// regardless the indicated JSON-RPC protocol version unless RPC quirks
// are enabled. With RPC quirks enabled, such requests will be responded
// to if the reqeust does not indicate JSON-RPC version.
//
// RPC quirks can be enabled by the user to avoid compatibility issues
// with software relying on Core's behavior.
if request.ID == nil && !(cfg.RPCQuirks && request.Jsonrpc == "") {
return
// Process a batched request
if batchedRequest {
var batchedRequests []interface{}
var resp json.RawMessage
err = json.Unmarshal(body, &batchedRequests)
if err != nil {
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCParse.Code,
Message: fmt.Sprintf("Failed to parse request: %v",
err),
}
resp, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to create reply: %v", err)
}
if resp != nil {
results = append(results, resp)
}
}
// The parse was at least successful enough to have an ID so
// set it for the response.
responseID = request.ID
if err == nil {
// Response with an empty batch error if the batch size is zero
if len(batchedRequests) == 0 {
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidRequest.Code,
Message: "Invalid request: empty batch",
}
resp, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to marshal reply: %v", err)
}
// Setup a close notifier. Since the connection is hijacked,
// the CloseNotifer on the ResponseWriter is not available.
closeChan := make(chan struct{}, 1)
go func() {
_, err := conn.Read(make([]byte, 1))
if err != nil {
close(closeChan)
if resp != nil {
results = append(results, resp)
}
}
}()
// Check if the user is limited and set error if method unauthorized
if !isAdmin {
if _, ok := rpcLimited[request.Method]; !ok {
jsonErr = &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidParams.Code,
Message: "limited user not authorized for this method",
// Process each batch entry individually
if len(batchedRequests) > 0 {
batchSize = len(batchedRequests)
for _, entry := range batchedRequests {
var reqBytes []byte
reqBytes, err = json.Marshal(entry)
if err != nil {
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidRequest.Code,
Message: fmt.Sprintf("Invalid request: %v",
err),
}
resp, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to create reply: %v", err)
}
if resp != nil {
results = append(results, resp)
}
continue
}
var req btcjson.Request
err := json.Unmarshal(reqBytes, &req)
if err != nil {
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidRequest.Code,
Message: fmt.Sprintf("Invalid request: %v",
err),
}
resp, err = btcjson.MarshalResponse("", nil, nil, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to create reply: %v", err)
}
if resp != nil {
results = append(results, resp)
}
continue
}
resp = s.processRequest(&req, isAdmin, closeChan)
if resp != nil {
results = append(results, resp)
}
}
}
}
}
if jsonErr == nil {
// Attempt to parse the JSON-RPC request into a known concrete
// command.
parsedCmd := parseCmd(&request)
if parsedCmd.err != nil {
jsonErr = parsedCmd.err
} else {
result, jsonErr = s.standardCmdResult(parsedCmd, closeChan)
var msg = []byte{}
if batchedRequest && batchSize > 0 {
if len(results) > 0 {
// Form the batched response json
var buffer bytes.Buffer
buffer.WriteByte('[')
for idx, reply := range results {
if idx == len(results)-1 {
buffer.Write(reply)
buffer.WriteByte(']')
break
}
buffer.Write(reply)
buffer.WriteByte(',')
}
msg = buffer.Bytes()
}
}
// Marshal the response.
msg, err := createMarshalledReply(responseID, result, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to marshal reply: %v", err)
return
if !batchedRequest || batchSize == 0 {
// Respond with the first results entry for single requests
if len(results) > 0 {
msg = results[0]
}
}
// Write the response.

View file

@ -695,7 +695,7 @@ func (*wsNotificationManager) notifyBlockConnected(clients map[chan struct{}]*ws
// Notify interested websocket clients about the connected block.
ntfn := btcjson.NewBlockConnectedNtfn(block.Hash().String(), block.Height(),
block.MsgBlock().Header.Timestamp.Unix())
marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn)
marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn)
if err != nil {
rpcsLog.Errorf("Failed to marshal block connected notification: "+
"%v", err)
@ -719,7 +719,7 @@ func (*wsNotificationManager) notifyBlockDisconnected(clients map[chan struct{}]
// Notify interested websocket clients about the disconnected block.
ntfn := btcjson.NewBlockDisconnectedNtfn(block.Hash().String(),
block.Height(), block.MsgBlock().Header.Timestamp.Unix())
marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn)
marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn)
if err != nil {
rpcsLog.Errorf("Failed to marshal block disconnected "+
"notification: %v", err)
@ -765,7 +765,7 @@ func (m *wsNotificationManager) notifyFilteredBlockConnected(clients map[chan st
ntfn.SubscribedTxs = subscribedTxs[quitChan]
// Marshal and queue notification.
marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn)
marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn)
if err != nil {
rpcsLog.Errorf("Failed to marshal filtered block "+
"connected notification: %v", err)
@ -796,7 +796,7 @@ func (*wsNotificationManager) notifyFilteredBlockDisconnected(clients map[chan s
}
ntfn := btcjson.NewFilteredBlockDisconnectedNtfn(block.Height(),
hex.EncodeToString(w.Bytes()))
marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn)
marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn)
if err != nil {
rpcsLog.Errorf("Failed to marshal filtered block disconnected "+
"notification: %v", err)
@ -831,7 +831,7 @@ func (m *wsNotificationManager) notifyForNewTx(clients map[chan struct{}]*wsClie
}
ntfn := btcjson.NewTxAcceptedNtfn(txHashStr, btcutil.Amount(amount).ToBTC())
marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn)
marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn)
if err != nil {
rpcsLog.Errorf("Failed to marshal tx notification: %s", err.Error())
return
@ -854,7 +854,7 @@ func (m *wsNotificationManager) notifyForNewTx(clients map[chan struct{}]*wsClie
}
verboseNtfn = btcjson.NewTxAcceptedVerboseNtfn(*rawTx)
marshalledJSONVerbose, err = btcjson.MarshalCmd(nil,
marshalledJSONVerbose, err = btcjson.MarshalCmd(btcjson.RpcVersion1, nil,
verboseNtfn)
if err != nil {
rpcsLog.Errorf("Failed to marshal verbose tx "+
@ -980,7 +980,7 @@ func blockDetails(block *btcutil.Block, txIndex int) *btcjson.BlockDetails {
func newRedeemingTxNotification(txHex string, index int, block *btcutil.Block) ([]byte, error) {
// Create and marshal the notification.
ntfn := btcjson.NewRedeemingTxNtfn(txHex, blockDetails(block, index))
return btcjson.MarshalCmd(nil, ntfn)
return btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn)
}
// notifyForTxOuts examines each transaction output, notifying interested
@ -1016,7 +1016,7 @@ func (m *wsNotificationManager) notifyForTxOuts(ops map[wire.OutPoint]map[chan s
ntfn := btcjson.NewRecvTxNtfn(txHex, blockDetails(block,
tx.Index()))
marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn)
marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn)
if err != nil {
rpcsLog.Errorf("Failed to marshal processedtx notification: %v", err)
continue
@ -1047,7 +1047,7 @@ func (m *wsNotificationManager) notifyRelevantTxAccepted(tx *btcutil.Tx,
if len(clientsToNotify) != 0 {
n := btcjson.NewRelevantTxAcceptedNtfn(txHexString(tx.MsgTx()))
marshalled, err := btcjson.MarshalCmd(nil, n)
marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, n)
if err != nil {
rpcsLog.Errorf("Failed to marshal notification: %v", err)
return
@ -1323,153 +1323,435 @@ out:
break out
}
var request btcjson.Request
err = json.Unmarshal(msg, &request)
if err != nil {
if !c.authenticated {
break out
}
var batchedRequest bool
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCParse.Code,
Message: "Failed to parse request: " + err.Error(),
}
reply, err := createMarshalledReply(nil, nil, jsonErr)
// Determine request type
if bytes.HasPrefix(msg, batchedRequestPrefix) {
batchedRequest = true
}
if !batchedRequest {
var req btcjson.Request
var reply json.RawMessage
err = json.Unmarshal(msg, &req)
if err != nil {
rpcsLog.Errorf("Failed to marshal parse failure "+
"reply: %v", err)
continue
}
c.SendMessage(reply, nil)
continue
}
// The JSON-RPC 1.0 spec defines that notifications must have their "id"
// set to null and states that notifications do not have a response.
//
// A JSON-RPC 2.0 notification is a request with "json-rpc":"2.0", and
// without an "id" member. The specification states that notifications
// must not be responded to. JSON-RPC 2.0 permits the null value as a
// valid request id, therefore such requests are not notifications.
//
// Bitcoin Core serves requests with "id":null or even an absent "id",
// and responds to such requests with "id":null in the response.
//
// Btcd does not respond to any request without and "id" or "id":null,
// regardless the indicated JSON-RPC protocol version unless RPC quirks
// are enabled. With RPC quirks enabled, such requests will be responded
// to if the reqeust does not indicate JSON-RPC version.
//
// RPC quirks can be enabled by the user to avoid compatibility issues
// with software relying on Core's behavior.
if request.ID == nil && !(cfg.RPCQuirks && request.Jsonrpc == "") {
if !c.authenticated {
break out
}
continue
}
cmd := parseCmd(&request)
if cmd.err != nil {
if !c.authenticated {
break out
}
reply, err := createMarshalledReply(cmd.id, nil, cmd.err)
if err != nil {
rpcsLog.Errorf("Failed to marshal parse failure "+
"reply: %v", err)
continue
}
c.SendMessage(reply, nil)
continue
}
rpcsLog.Debugf("Received command <%s> from %s", cmd.method, c.addr)
// Check auth. The client is immediately disconnected if the
// first request of an unauthentiated websocket client is not
// the authenticate request, an authenticate request is received
// when the client is already authenticated, or incorrect
// authentication credentials are provided in the request.
switch authCmd, ok := cmd.cmd.(*btcjson.AuthenticateCmd); {
case c.authenticated && ok:
rpcsLog.Warnf("Websocket client %s is already authenticated",
c.addr)
break out
case !c.authenticated && !ok:
rpcsLog.Warnf("Unauthenticated websocket message " +
"received")
break out
case !c.authenticated:
// Check credentials.
login := authCmd.Username + ":" + authCmd.Passphrase
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login))
authSha := sha256.Sum256([]byte(auth))
cmp := subtle.ConstantTimeCompare(authSha[:], c.server.authsha[:])
limitcmp := subtle.ConstantTimeCompare(authSha[:], c.server.limitauthsha[:])
if cmp != 1 && limitcmp != 1 {
rpcsLog.Warnf("Auth failure.")
break out
}
c.authenticated = true
c.isAdmin = cmp == 1
// Marshal and send response.
reply, err := createMarshalledReply(cmd.id, nil, nil)
if err != nil {
rpcsLog.Errorf("Failed to marshal authenticate reply: "+
"%v", err.Error())
continue
}
c.SendMessage(reply, nil)
continue
}
// Check if the client is using limited RPC credentials and
// error when not authorized to call this RPC.
if !c.isAdmin {
if _, ok := rpcLimited[request.Method]; !ok {
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidParams.Code,
Message: "limited user not authorized for this method",
// only process requests from authenticated clients
if !c.authenticated {
break out
}
// Marshal and send response.
reply, err := createMarshalledReply(request.ID, nil, jsonErr)
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCParse.Code,
Message: "Failed to parse request: " + err.Error(),
}
reply, err = createMarshalledReply(btcjson.RpcVersion1, nil, nil, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to marshal parse failure "+
"reply: %v", err)
rpcsLog.Errorf("Failed to marshal reply: %v", err)
continue
}
c.SendMessage(reply, nil)
continue
}
if req.Method == "" || req.Params == nil {
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidRequest.Code,
Message: "Invalid request: malformed",
}
reply, err := createMarshalledReply(req.Jsonrpc, req.ID, nil, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to marshal reply: %v", err)
continue
}
c.SendMessage(reply, nil)
continue
}
// Valid requests with no ID (notifications) must not have a response
// per the JSON-RPC spec.
if req.ID == nil {
if !c.authenticated {
break out
}
continue
}
cmd := parseCmd(&req)
if cmd.err != nil {
// Only process requests from authenticated clients
if !c.authenticated {
break out
}
reply, err = createMarshalledReply(cmd.jsonrpc, cmd.id, nil, cmd.err)
if err != nil {
rpcsLog.Errorf("Failed to marshal reply: %v", err)
continue
}
c.SendMessage(reply, nil)
continue
}
rpcsLog.Debugf("Received command <%s> from %s", cmd.method, c.addr)
// Check auth. The client is immediately disconnected if the
// first request of an unauthentiated websocket client is not
// the authenticate request, an authenticate request is received
// when the client is already authenticated, or incorrect
// authentication credentials are provided in the request.
switch authCmd, ok := cmd.cmd.(*btcjson.AuthenticateCmd); {
case c.authenticated && ok:
rpcsLog.Warnf("Websocket client %s is already authenticated",
c.addr)
break out
case !c.authenticated && !ok:
rpcsLog.Warnf("Unauthenticated websocket message " +
"received")
break out
case !c.authenticated:
// Check credentials.
login := authCmd.Username + ":" + authCmd.Passphrase
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login))
authSha := sha256.Sum256([]byte(auth))
cmp := subtle.ConstantTimeCompare(authSha[:], c.server.authsha[:])
limitcmp := subtle.ConstantTimeCompare(authSha[:], c.server.limitauthsha[:])
if cmp != 1 && limitcmp != 1 {
rpcsLog.Warnf("Auth failure.")
break out
}
c.authenticated = true
c.isAdmin = cmp == 1
// Marshal and send response.
reply, err = createMarshalledReply(cmd.jsonrpc, cmd.id, nil, nil)
if err != nil {
rpcsLog.Errorf("Failed to marshal authenticate reply: "+
"%v", err.Error())
continue
}
c.SendMessage(reply, nil)
continue
}
// Check if the client is using limited RPC credentials and
// error when not authorized to call the supplied RPC.
if !c.isAdmin {
if _, ok := rpcLimited[req.Method]; !ok {
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidParams.Code,
Message: "limited user not authorized for this method",
}
// Marshal and send response.
reply, err = createMarshalledReply("", req.ID, nil, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to marshal parse failure "+
"reply: %v", err)
continue
}
c.SendMessage(reply, nil)
continue
}
}
// Asynchronously handle the request. A semaphore is used to
// limit the number of concurrent requests currently being
// serviced. If the semaphore can not be acquired, simply wait
// until a request finished before reading the next RPC request
// from the websocket client.
//
// This could be a little fancier by timing out and erroring
// when it takes too long to service the request, but if that is
// done, the read of the next request should not be blocked by
// this semaphore, otherwise the next request will be read and
// will probably sit here for another few seconds before timing
// out as well. This will cause the total timeout duration for
// later requests to be much longer than the check here would
// imply.
//
// If a timeout is added, the semaphore acquiring should be
// moved inside of the new goroutine with a select statement
// that also reads a time.After channel. This will unblock the
// read of the next request from the websocket client and allow
// many requests to be waited on concurrently.
c.serviceRequestSem.acquire()
go func() {
c.serviceRequest(cmd)
c.serviceRequestSem.release()
}()
}
// Asynchronously handle the request. A semaphore is used to
// limit the number of concurrent requests currently being
// serviced. If the semaphore can not be acquired, simply wait
// until a request finished before reading the next RPC request
// from the websocket client.
//
// This could be a little fancier by timing out and erroring
// when it takes too long to service the request, but if that is
// done, the read of the next request should not be blocked by
// this semaphore, otherwise the next request will be read and
// will probably sit here for another few seconds before timing
// out as well. This will cause the total timeout duration for
// later requests to be much longer than the check here would
// imply.
//
// If a timeout is added, the semaphore acquiring should be
// moved inside of the new goroutine with a select statement
// that also reads a time.After channel. This will unblock the
// read of the next request from the websocket client and allow
// many requests to be waited on concurrently.
c.serviceRequestSem.acquire()
go func() {
c.serviceRequest(cmd)
// Process a batched request
if batchedRequest {
var batchedRequests []interface{}
var results []json.RawMessage
var batchSize int
var reply json.RawMessage
c.serviceRequestSem.acquire()
err = json.Unmarshal(msg, &batchedRequests)
if err != nil {
// Only process requests from authenticated clients
if !c.authenticated {
break out
}
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCParse.Code,
Message: fmt.Sprintf("Failed to parse request: %v",
err),
}
reply, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to create reply: %v", err)
}
if reply != nil {
results = append(results, reply)
}
}
if err == nil {
// Response with an empty batch error if the batch size is zero
if len(batchedRequests) == 0 {
if !c.authenticated {
break out
}
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidRequest.Code,
Message: "Invalid request: empty batch",
}
reply, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to marshal reply: %v", err)
}
if reply != nil {
results = append(results, reply)
}
}
// Process each batch entry individually
if len(batchedRequests) > 0 {
batchSize = len(batchedRequests)
for _, entry := range batchedRequests {
var reqBytes []byte
reqBytes, err = json.Marshal(entry)
if err != nil {
// Only process requests from authenticated clients
if !c.authenticated {
break out
}
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidRequest.Code,
Message: fmt.Sprintf("Invalid request: %v",
err),
}
reply, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to create reply: %v", err)
continue
}
if reply != nil {
results = append(results, reply)
}
continue
}
var req btcjson.Request
err := json.Unmarshal(reqBytes, &req)
if err != nil {
// Only process requests from authenticated clients
if !c.authenticated {
break out
}
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidRequest.Code,
Message: fmt.Sprintf("Invalid request: %v",
err),
}
reply, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to create reply: %v", err)
continue
}
if reply != nil {
results = append(results, reply)
}
continue
}
if req.Method == "" || req.Params == nil {
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidRequest.Code,
Message: "Invalid request: malformed",
}
reply, err := createMarshalledReply(req.Jsonrpc, req.ID, nil, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to marshal reply: %v", err)
continue
}
if reply != nil {
results = append(results, reply)
}
continue
}
// Valid requests with no ID (notifications) must not have a response
// per the JSON-RPC spec.
if req.ID == nil {
if !c.authenticated {
break out
}
continue
}
cmd := parseCmd(&req)
if cmd.err != nil {
// Only process requests from authenticated clients
if !c.authenticated {
break out
}
reply, err = createMarshalledReply(cmd.jsonrpc, cmd.id, nil, cmd.err)
if err != nil {
rpcsLog.Errorf("Failed to marshal reply: %v", err)
continue
}
if reply != nil {
results = append(results, reply)
}
continue
}
rpcsLog.Debugf("Received command <%s> from %s", cmd.method, c.addr)
// Check auth. The client is immediately disconnected if the
// first request of an unauthentiated websocket client is not
// the authenticate request, an authenticate request is received
// when the client is already authenticated, or incorrect
// authentication credentials are provided in the request.
switch authCmd, ok := cmd.cmd.(*btcjson.AuthenticateCmd); {
case c.authenticated && ok:
rpcsLog.Warnf("Websocket client %s is already authenticated",
c.addr)
break out
case !c.authenticated && !ok:
rpcsLog.Warnf("Unauthenticated websocket message " +
"received")
break out
case !c.authenticated:
// Check credentials.
login := authCmd.Username + ":" + authCmd.Passphrase
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login))
authSha := sha256.Sum256([]byte(auth))
cmp := subtle.ConstantTimeCompare(authSha[:], c.server.authsha[:])
limitcmp := subtle.ConstantTimeCompare(authSha[:], c.server.limitauthsha[:])
if cmp != 1 && limitcmp != 1 {
rpcsLog.Warnf("Auth failure.")
break out
}
c.authenticated = true
c.isAdmin = cmp == 1
// Marshal and send response.
reply, err = createMarshalledReply(cmd.jsonrpc, cmd.id, nil, nil)
if err != nil {
rpcsLog.Errorf("Failed to marshal authenticate reply: "+
"%v", err.Error())
continue
}
if reply != nil {
results = append(results, reply)
}
continue
}
// Check if the client is using limited RPC credentials and
// error when not authorized to call the supplied RPC.
if !c.isAdmin {
if _, ok := rpcLimited[req.Method]; !ok {
jsonErr := &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidParams.Code,
Message: "limited user not authorized for this method",
}
// Marshal and send response.
reply, err = createMarshalledReply(req.Jsonrpc, req.ID, nil, jsonErr)
if err != nil {
rpcsLog.Errorf("Failed to marshal parse failure "+
"reply: %v", err)
continue
}
if reply != nil {
results = append(results, reply)
}
continue
}
}
// Lookup the websocket extension for the command, if it doesn't
// exist fallback to handling the command as a standard command.
var resp interface{}
wsHandler, ok := wsHandlers[cmd.method]
if ok {
resp, err = wsHandler(c, cmd.cmd)
} else {
resp, err = c.server.standardCmdResult(cmd, nil)
}
// Marshal request output.
reply, err := createMarshalledReply(cmd.jsonrpc, cmd.id, resp, err)
if err != nil {
rpcsLog.Errorf("Failed to marshal reply for <%s> "+
"command: %v", cmd.method, err)
return
}
if reply != nil {
results = append(results, reply)
}
}
}
}
// generate reply
var payload = []byte{}
if batchedRequest && batchSize > 0 {
if len(results) > 0 {
// Form the batched response json
var buffer bytes.Buffer
buffer.WriteByte('[')
for idx, marshalledReply := range results {
if idx == len(results)-1 {
buffer.Write(marshalledReply)
buffer.WriteByte(']')
break
}
buffer.Write(marshalledReply)
buffer.WriteByte(',')
}
payload = buffer.Bytes()
}
}
if !batchedRequest || batchSize == 0 {
// Respond with the first results entry for single requests
if len(results) > 0 {
payload = results[0]
}
}
c.SendMessage(payload, nil)
c.serviceRequestSem.release()
}()
}
}
// Ensure the connection is closed.
@ -1495,7 +1777,7 @@ func (c *wsClient) serviceRequest(r *parsedRPCCmd) {
} else {
result, err = c.server.standardCmdResult(r, nil)
}
reply, err := createMarshalledReply(r.id, result, err)
reply, err := createMarshalledReply(r.jsonrpc, r.id, result, err)
if err != nil {
rpcsLog.Errorf("Failed to marshal reply for <%s> "+
"command: %v", r.method, err)
@ -2125,7 +2407,7 @@ func rescanBlock(wsc *wsClient, lookups *rescanKeys, blk *btcutil.Block) {
ntfn := btcjson.NewRecvTxNtfn(txHex,
blockDetails(blk, tx.Index()))
marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn)
marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn)
if err != nil {
rpcsLog.Errorf("Failed to marshal recvtx notification: %v", err)
return
@ -2492,7 +2774,7 @@ fetchRange:
hashList[i].String(), blk.Height(),
blk.MsgBlock().Header.Timestamp.Unix(),
)
mn, err := btcjson.MarshalCmd(nil, n)
mn, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, n)
if err != nil {
rpcsLog.Errorf("Failed to marshal rescan "+
"progress notification: %v", err)
@ -2637,7 +2919,7 @@ func handleRescan(wsc *wsClient, icmd interface{}) (interface{}, error) {
lastBlockHash.String(), lastBlock.Height(),
lastBlock.MsgBlock().Header.Timestamp.Unix(),
)
if mn, err := btcjson.MarshalCmd(nil, n); err != nil {
if mn, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, n); err != nil {
rpcsLog.Errorf("Failed to marshal rescan finished "+
"notification: %v", err)
} else {