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

View file

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

View file

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

View file

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

View file

@ -231,7 +231,7 @@ func TestChainSvrWsNtfns(t *testing.T) {
for i, test := range tests { for i, test := range tests {
// Marshal the notification as created by the new static // Marshal the notification as created by the new static
// creation function. The ID is nil for notifications. // 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 { if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err) test.name, err)
@ -256,7 +256,7 @@ func TestChainSvrWsNtfns(t *testing.T) {
// Marshal the notification as created by the generic new // Marshal the notification as created by the generic new
// notification creation function. The ID is nil for // notification creation function. The ID is nil for
// notifications. // notifications.
marshalled, err = btcjson.MarshalCmd(nil, cmd) marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, nil, cmd)
if err != nil { if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err) 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 // is suitable for transmission to an RPC server. The provided command type
// must be a registered type. All commands provided by this package are // must be a registered type. All commands provided by this package are
// registered by default. // 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. // Look up the cmd type and error out if not registered.
rt := reflect.TypeOf(cmd) rt := reflect.TypeOf(cmd)
registerLock.RLock() registerLock.RLock()
@ -60,7 +60,7 @@ func MarshalCmd(id interface{}, cmd interface{}) ([]byte, error) {
params := makeParams(rt.Elem(), rv.Elem()) params := makeParams(rt.Elem(), rv.Elem())
// Generate and marshal the final JSON-RPC request. // Generate and marshal the final JSON-RPC request.
rawCmd, err := NewRequest(id, method, params) rawCmd, err := NewRequest(rpcVersion, id, method, params)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

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

View file

@ -27,7 +27,7 @@ func ExampleMarshalCmd() {
// server. Typically the client would increment the id here which is // server. Typically the client would increment the id here which is
// request so the response can be identified. // request so the response can be identified.
id := 1 id := 1
marshalledBytes, err := btcjson.MarshalCmd(id, gbCmd) marshalledBytes, err := btcjson.MarshalCmd(btcjson.RpcVersion1, id, gbCmd)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return return
@ -95,7 +95,7 @@ func ExampleUnmarshalCmd() {
func ExampleMarshalResponse() { func ExampleMarshalResponse() {
// Marshal a new JSON-RPC response. For example, this is a response // Marshal a new JSON-RPC response. For example, this is a response
// to a getblockheight request. // to a getblockheight request.
marshalledBytes, err := btcjson.MarshalResponse(1, 350001, nil) marshalledBytes, err := btcjson.MarshalResponse(btcjson.RpcVersion1, 1, 350001, nil)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return return
@ -107,7 +107,7 @@ func ExampleMarshalResponse() {
fmt.Printf("%s\n", marshalledBytes) fmt.Printf("%s\n", marshalledBytes)
// Output: // 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 // 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, // 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 // it is hard coded here for clarity. This is an example response to a
// getblockheight request. // 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. // Unmarshal the raw bytes from the wire into a JSON-RPC response.
var response btcjson.Response var response btcjson.Response

View file

@ -9,6 +9,33 @@ import (
"fmt" "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 // 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. // 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 // requests, however this struct it being exported in case the caller wants to
// construct raw requests for some reason. // construct raw requests for some reason.
type Request struct { type Request struct {
Jsonrpc string `json:"jsonrpc"` Jsonrpc RPCVersion `json:"jsonrpc"`
Method string `json:"method"` Method string `json:"method"`
Params []json.RawMessage `json:"params"` Params []json.RawMessage `json:"params"`
ID interface{} `json:"id"` ID interface{} `json:"id"`
} }
// NewRequest returns a new JSON-RPC 1.0 request object given the provided id, // UnmarshalJSON is a custom unmarshal func for the Request struct. The param
// method, and parameters. The parameters are marshalled into a json.RawMessage // field defaults to an empty json.RawMessage array it is omitted by the request
// for the Params field of the returned request object. This function is only // or nil if the supplied value is invalid.
// provided in case the caller wants to construct raw requests for some reason. func (request *Request) UnmarshalJSON(b []byte) error {
// // Step 1: Create a type alias of the original struct.
// Typically callers will instead want to create a registered concrete command type Alias Request
// type with the NewCmd or New<Foo>Cmd functions and call the MarshalCmd
// function with that command to generate the marshalled JSON-RPC request. // Step 2: Create an anonymous struct with raw replacements for the special
func NewRequest(id interface{}, method string, params []interface{}) (*Request, error) { // 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) { if !IsValidIDType(id) {
str := fmt.Sprintf("the id of type '%T' is invalid", id) str := fmt.Sprintf("the id of type '%T' is invalid", id)
return nil, makeError(ErrInvalidType, str) return nil, makeError(ErrInvalidType, str)
@ -98,30 +178,35 @@ func NewRequest(id interface{}, method string, params []interface{}) (*Request,
} }
return &Request{ return &Request{
Jsonrpc: "1.0", Jsonrpc: rpcVersion,
ID: id, ID: id,
Method: method, Method: method,
Params: rawParams, Params: rawParams,
}, nil }, nil
} }
// Response is the general form of a JSON-RPC response. The type of the Result // Response is the general form of a JSON-RPC response. The type of the
// field varies from one command to the next, so it is implemented as an // 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 // interface. The ID field has to be a pointer to allow for a nil value when
// empty. // empty.
type Response struct { type Response struct {
Result json.RawMessage `json:"result"` Jsonrpc RPCVersion `json:"jsonrpc"`
Error *RPCError `json:"error"` Result json.RawMessage `json:"result"`
ID *interface{} `json:"id"` Error *RPCError `json:"error"`
ID *interface{} `json:"id"`
} }
// NewResponse returns a new JSON-RPC response object given the provided id, // NewResponse returns a new JSON-RPC response object given the provided rpc
// marshalled result, and RPC error. This function is only provided in case the // version, id, marshalled result, and RPC error. This function is only
// caller wants to construct raw responses for some reason. // 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 // Typically callers will instead want to create the fully marshalled JSON-RPC
// response to send over the wire with the MarshalResponse function. // 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) { if !IsValidIDType(id) {
str := fmt.Sprintf("the id of type '%T' is invalid", id) str := fmt.Sprintf("the id of type '%T' is invalid", id)
return nil, makeError(ErrInvalidType, str) return nil, makeError(ErrInvalidType, str)
@ -129,20 +214,27 @@ func NewResponse(id interface{}, marshalledResult []byte, rpcErr *RPCError) (*Re
pid := &id pid := &id
return &Response{ return &Response{
Result: marshalledResult, Jsonrpc: rpcVersion,
Error: rpcErr, Result: marshalledResult,
ID: pid, Error: rpcErr,
ID: pid,
}, nil }, nil
} }
// MarshalResponse marshals the passed id, result, and RPCError to a JSON-RPC // MarshalResponse marshals the passed rpc version, id, result, and RPCError to
// response byte slice that is suitable for transmission to a JSON-RPC client. // a JSON-RPC response byte slice that is suitable for transmission to a
func MarshalResponse(id interface{}, result interface{}, rpcErr *RPCError) ([]byte, error) { // 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) marshalledResult, err := json.Marshal(result)
if err != nil { if err != nil {
return nil, err return nil, err
} }
response, err := NewResponse(id, marshalledResult, rpcErr) response, err := NewResponse(rpcVersion, id, marshalledResult, rpcErr)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -68,7 +68,7 @@ func TestMarshalResponse(t *testing.T) {
name: "ordinary bool result with no error", name: "ordinary bool result with no error",
result: true, result: true,
jsonErr: nil, 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", name: "result with error",
@ -76,14 +76,14 @@ func TestMarshalResponse(t *testing.T) {
jsonErr: func() *btcjson.RPCError { jsonErr: func() *btcjson.RPCError {
return btcjson.NewRPCError(btcjson.ErrRPCBlockNotFound, "123 not found") 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)) t.Logf("Running %d tests", len(tests))
for i, test := range tests { for i, test := range tests {
_, _ = i, test _, _ = 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 { if err != nil {
t.Errorf("Test #%d (%s) unexpected error: %v", i, t.Errorf("Test #%d (%s) unexpected error: %v", i,
test.name, err) 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 // Force an error in NewRequest by giving it a parameter type that is
// not supported. // 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 { if err == nil {
t.Error("NewRequest: did not receive error") t.Error("NewRequest: did not receive error")
return return
@ -113,7 +113,7 @@ func TestMiscErrors(t *testing.T) {
// Force an error in MarshalResponse by giving it an id type that is not // Force an error in MarshalResponse by giving it an id type that is not
// supported. // supported.
wantErr := btcjson.Error{ErrorCode: btcjson.ErrInvalidType} 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 { if jerr, ok := err.(btcjson.Error); !ok || jerr.ErrorCode != wantErr.ErrorCode {
t.Errorf("MarshalResult: did not receive expected error - got "+ t.Errorf("MarshalResult: did not receive expected error - got "+
"%v (%[1]T), want %v (%[2]T)", err, wantErr) "%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 // Force an error in MarshalResponse by giving it a result type that
// can't be marshalled. // 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 { if _, ok := err.(*json.UnsupportedTypeError); !ok {
wantErr := &json.UnsupportedTypeError{} wantErr := &json.UnsupportedTypeError{}
t.Errorf("MarshalResult: did not receive expected error - got "+ 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 { for i, test := range tests {
// Marshal the command as created by the new static command // Marshal the command as created by the new static command
// creation function. // creation function.
marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd()) marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd())
if err != nil { if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err) test.name, err)
@ -1824,7 +1824,7 @@ func TestWalletSvrCmds(t *testing.T) {
// Marshal the command as created by the generic new command // Marshal the command as created by the generic new command
// creation function. // creation function.
marshalled, err = btcjson.MarshalCmd(testID, cmd) marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd)
if err != nil { if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err) test.name, err)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,22 +15,24 @@ import (
"testing" "testing"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/integration/rpctest" "github.com/btcsuite/btcd/integration/rpctest"
"github.com/btcsuite/btcd/rpcclient"
) )
func testGetBestBlock(r *rpctest.Harness, t *testing.T) { func testGetBestBlock(r *rpctest.Harness, t *testing.T) {
_, prevbestHeight, err := r.Node.GetBestBlock() _, prevbestHeight, err := r.Client.GetBestBlock()
if err != nil { if err != nil {
t.Fatalf("Call to `getbestblock` failed: %v", err) t.Fatalf("Call to `getbestblock` failed: %v", err)
} }
// Create a new block connecting to the current tip. // Create a new block connecting to the current tip.
generatedBlockHashes, err := r.Node.Generate(1) generatedBlockHashes, err := r.Client.Generate(1)
if err != nil { if err != nil {
t.Fatalf("Unable to generate block: %v", err) t.Fatalf("Unable to generate block: %v", err)
} }
bestHash, bestHeight, err := r.Node.GetBestBlock() bestHash, bestHeight, err := r.Client.GetBestBlock()
if err != nil { if err != nil {
t.Fatalf("Call to `getbestblock` failed: %v", err) 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) { func testGetBlockCount(r *rpctest.Harness, t *testing.T) {
// Save the current count. // Save the current count.
currentCount, err := r.Node.GetBlockCount() currentCount, err := r.Client.GetBlockCount()
if err != nil { if err != nil {
t.Fatalf("Unable to get block count: %v", err) 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) t.Fatalf("Unable to generate block: %v", err)
} }
// Count should have increased by one. // Count should have increased by one.
newCount, err := r.Node.GetBlockCount() newCount, err := r.Client.GetBlockCount()
if err != nil { if err != nil {
t.Fatalf("Unable to get block count: %v", err) 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) { func testGetBlockHash(r *rpctest.Harness, t *testing.T) {
// Create a new block connecting to the current tip. // Create a new block connecting to the current tip.
generatedBlockHashes, err := r.Node.Generate(1) generatedBlockHashes, err := r.Client.Generate(1)
if err != nil { if err != nil {
t.Fatalf("Unable to generate block: %v", err) t.Fatalf("Unable to generate block: %v", err)
} }
info, err := r.Node.GetInfo() info, err := r.Client.GetInfo()
if err != nil { if err != nil {
t.Fatalf("call to getinfo cailed: %v", err) 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 { if err != nil {
t.Fatalf("Call to `getblockhash` failed: %v", err) 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{ var rpcTestCases = []rpctest.HarnessTestCase{
testGetBestBlock, testGetBestBlock,
testGetBlockCount, testGetBlockCount,
testGetBlockHash, testGetBlockHash,
testBulkClient,
} }
var primaryHarness *rpctest.Harness var primaryHarness *rpctest.Harness

View file

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

View file

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

View file

@ -49,7 +49,7 @@ func syncMempools(nodes []*Harness) error {
retry: retry:
for !poolsMatch { for !poolsMatch {
firstPool, err := nodes[0].Node.GetRawMempool() firstPool, err := nodes[0].Client.GetRawMempool()
if err != nil { if err != nil {
return err return err
} }
@ -58,7 +58,7 @@ retry:
// first node, then we're done. Otherwise, drop back to the top // first node, then we're done. Otherwise, drop back to the top
// of the loop and retry after a short wait period. // of the loop and retry after a short wait period.
for _, node := range nodes[1:] { for _, node := range nodes[1:] {
nodePool, err := node.Node.GetRawMempool() nodePool, err := node.Client.GetRawMempool()
if err != nil { if err != nil {
return err return err
} }
@ -84,7 +84,7 @@ retry:
var prevHash *chainhash.Hash var prevHash *chainhash.Hash
var prevHeight int32 var prevHeight int32
for _, node := range nodes { for _, node := range nodes {
blockHash, blockHeight, err := node.Node.GetBestBlock() blockHash, blockHeight, err := node.Client.GetBestBlock()
if err != nil { if err != nil {
return err return err
} }
@ -108,24 +108,24 @@ retry:
// therefore in the case of disconnects, "from" will attempt to reestablish a // therefore in the case of disconnects, "from" will attempt to reestablish a
// connection to the "to" harness. // connection to the "to" harness.
func ConnectNode(from *Harness, to *Harness) error { func ConnectNode(from *Harness, to *Harness) error {
peerInfo, err := from.Node.GetPeerInfo() peerInfo, err := from.Client.GetPeerInfo()
if err != nil { if err != nil {
return err return err
} }
numPeers := len(peerInfo) numPeers := len(peerInfo)
targetAddr := to.node.config.listen 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 return err
} }
// Block until a new connection has been established. // Block until a new connection has been established.
peerInfo, err = from.Node.GetPeerInfo() peerInfo, err = from.Client.GetPeerInfo()
if err != nil { if err != nil {
return err return err
} }
for len(peerInfo) <= numPeers { for len(peerInfo) <= numPeers {
peerInfo, err = from.Node.GetPeerInfo() peerInfo, err = from.Client.GetPeerInfo()
if err != nil { if err != nil {
return err 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 indicated whether or not the server is disconnected.
disconnected bool 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 // retryCount holds the number of times the client has tried to
// reconnect to the RPC server. // reconnect to the RPC server.
retryCount int64 retryCount int64
@ -220,8 +224,13 @@ func (c *Client) addRequest(jReq *jsonRequest) error {
default: default:
} }
element := c.requestList.PushBack(jReq) if !c.batch {
c.requestMap[jReq.id] = element element := c.requestList.PushBack(jReq)
c.requestMap[jReq.id] = element
} else {
element := c.batchList.PushBack(jReq)
c.requestMap[jReq.id] = element
}
return nil 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 // inMessage is the first type that an incoming message is unmarshaled
// into. It supports both requests (for notification support) and // into. It supports both requests (for notification support) and
// responses. The partially-unmarshaled message is a notification if // 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. // Try to unmarshal the response as a regular JSON-RPC response.
var resp rawResponse 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 { if err != nil {
// When the response itself isn't a valid JSON-RPC response // When the response itself isn't a valid JSON-RPC response
// return an error which includes the HTTP status code and raw // 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} jReq.responseChan <- &response{err: err}
return return
} }
var res []byte
res, err := resp.result() 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} 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, // POST mode, the command is issued via an HTTP client. Otherwise,
// the command is issued via the asynchronous websocket channels. // the command is issued via the asynchronous websocket channels.
if c.config.HTTPPostMode { 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 return
} }
@ -905,6 +966,10 @@ func (c *Client) sendRequest(jReq *jsonRequest) {
// future. It handles both websocket and HTTP POST mode depending on the // future. It handles both websocket and HTTP POST mode depending on the
// configuration of the client. // configuration of the client.
func (c *Client) sendCmd(cmd interface{}) chan *response { func (c *Client) sendCmd(cmd interface{}) chan *response {
rpcVersion := btcjson.RpcVersion1
if c.batch {
rpcVersion = btcjson.RpcVersion2
}
// Get the method associated with the command. // Get the method associated with the command.
method, err := btcjson.CmdMethod(cmd) method, err := btcjson.CmdMethod(cmd)
if err != nil { if err != nil {
@ -913,7 +978,7 @@ func (c *Client) sendCmd(cmd interface{}) chan *response {
// Marshal the command. // Marshal the command.
id := c.NextID() id := c.NextID()
marshalledJSON, err := btcjson.MarshalCmd(id, cmd) marshalledJSON, err := btcjson.MarshalCmd(rpcVersion, id, cmd)
if err != nil { if err != nil {
return newFutureError(err) return newFutureError(err)
} }
@ -927,6 +992,7 @@ func (c *Client) sendCmd(cmd interface{}) chan *response {
marshalledJSON: marshalledJSON, marshalledJSON: marshalledJSON,
responseChan: responseChan, responseChan: responseChan,
} }
c.sendRequest(jReq) c.sendRequest(jReq)
return responseChan return responseChan
@ -1357,6 +1423,8 @@ func New(config *ConnConfig, ntfnHandlers *NotificationHandlers) (*Client, error
httpClient: httpClient, httpClient: httpClient,
requestMap: make(map[uint64]*list.Element), requestMap: make(map[uint64]*list.Element),
requestList: list.New(), requestList: list.New(),
batch: false,
batchList: list.New(),
ntfnHandlers: ntfnHandlers, ntfnHandlers: ntfnHandlers,
ntfnState: newNotificationState(), ntfnState: newNotificationState(),
sendChan: make(chan []byte, sendBufferSize), sendChan: make(chan []byte, sendBufferSize),
@ -1397,6 +1465,24 @@ func New(config *ConnConfig, ntfnHandlers *NotificationHandlers) (*Client, error
return client, nil 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 // Connect establishes the initial websocket connection. This is necessary when
// a client was created after setting the DisableConnectOnNew field of the // a client was created after setting the DisableConnectOnNew field of the
// Config struct. // Config struct.
@ -1534,3 +1620,69 @@ func (c *Client) BackendVersion() (BackendVersion, error) {
return *c.backendVersion, nil 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. // than custom commands.
id := c.NextID() id := c.NextID()
rawRequest := &btcjson.Request{ rawRequest := &btcjson.Request{
Jsonrpc: "1.0", Jsonrpc: btcjson.RpcVersion1,
ID: id, ID: id,
Method: method, Method: method,
Params: params, Params: params,

View file

@ -101,6 +101,9 @@ var (
// declared here to avoid the overhead of creating the slice on every // declared here to avoid the overhead of creating the slice on every
// invocation for constant data. // invocation for constant data.
gbtCapabilities = []string{"proposal"} gbtCapabilities = []string{"proposal"}
// JSON 2.0 batched request prefix
batchedRequestPrefix = []byte("[")
) )
// Errors // 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 // a known concrete command along with any error that might have happened while
// parsing it. // parsing it.
type parsedRPCCmd struct { type parsedRPCCmd struct {
id interface{} jsonrpc btcjson.RPCVersion
method string id interface{}
cmd interface{} method string
err *btcjson.RPCError cmd interface{}
err *btcjson.RPCError
} }
// standardCmdResult checks that a parsed command is a standard Bitcoin JSON-RPC // 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 // is suitable for use in replies if the command is invalid in some way such as
// an unregistered command or invalid parameters. // an unregistered command or invalid parameters.
func parseCmd(request *btcjson.Request) *parsedRPCCmd { func parseCmd(request *btcjson.Request) *parsedRPCCmd {
var parsedCmd parsedRPCCmd parsedCmd := parsedRPCCmd{
parsedCmd.id = request.ID jsonrpc: request.Jsonrpc,
parsedCmd.method = request.Method id: request.ID,
method: request.Method,
}
cmd, err := btcjson.UnmarshalCmd(request) cmd, err := btcjson.UnmarshalCmd(request)
if err != nil { if err != nil {
@ -4004,7 +4010,7 @@ func parseCmd(request *btcjson.Request) *parsedRPCCmd {
// createMarshalledReply returns a new marshalled JSON-RPC response given the // createMarshalledReply returns a new marshalled JSON-RPC response given the
// passed parameters. It will automatically convert errors that are not of // passed parameters. It will automatically convert errors that are not of
// the type *btcjson.RPCError to the appropriate type as needed. // 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 var jsonErr *btcjson.RPCError
if replyErr != nil { if replyErr != nil {
if jErr, ok := replyErr.(*btcjson.RPCError); ok { 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. // 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) conn.SetReadDeadline(timeZeroVal)
// Attempt to parse the raw body into a JSON-RPC request. // Attempt to parse the raw body into a JSON-RPC request.
var responseID interface{} // Setup a close notifier. Since the connection is hijacked,
var jsonErr error // the CloseNotifer on the ResponseWriter is not available.
var result interface{} closeChan := make(chan struct{}, 1)
var request btcjson.Request go func() {
if err := json.Unmarshal(body, &request); err != nil { _, err = conn.Read(make([]byte, 1))
jsonErr = &btcjson.RPCError{ if err != nil {
Code: btcjson.ErrRPCParse.Code, close(closeChan)
Message: "Failed to parse request: " + err.Error(), }
}()
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" // Process a batched request
// set to null and states that notifications do not have a response. if batchedRequest {
// var batchedRequests []interface{}
// A JSON-RPC 2.0 notification is a request with "json-rpc":"2.0", and var resp json.RawMessage
// without an "id" member. The specification states that notifications err = json.Unmarshal(body, &batchedRequests)
// must not be responded to. JSON-RPC 2.0 permits the null value as a if err != nil {
// valid request id, therefore such requests are not notifications. jsonErr := &btcjson.RPCError{
// Code: btcjson.ErrRPCParse.Code,
// Bitcoin Core serves requests with "id":null or even an absent "id", Message: fmt.Sprintf("Failed to parse request: %v",
// and responds to such requests with "id":null in the response. err),
// }
// Btcd does not respond to any request without and "id" or "id":null, resp, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr)
// regardless the indicated JSON-RPC protocol version unless RPC quirks if err != nil {
// are enabled. With RPC quirks enabled, such requests will be responded rpcsLog.Errorf("Failed to create reply: %v", err)
// to if the reqeust does not indicate JSON-RPC version. }
//
// RPC quirks can be enabled by the user to avoid compatibility issues if resp != nil {
// with software relying on Core's behavior. results = append(results, resp)
if request.ID == nil && !(cfg.RPCQuirks && request.Jsonrpc == "") { }
return
} }
// The parse was at least successful enough to have an ID so if err == nil {
// set it for the response. // Response with an empty batch error if the batch size is zero
responseID = request.ID 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, if resp != nil {
// the CloseNotifer on the ResponseWriter is not available. results = append(results, resp)
closeChan := make(chan struct{}, 1) }
go func() {
_, err := conn.Read(make([]byte, 1))
if err != nil {
close(closeChan)
} }
}()
// Check if the user is limited and set error if method unauthorized // Process each batch entry individually
if !isAdmin { if len(batchedRequests) > 0 {
if _, ok := rpcLimited[request.Method]; !ok { batchSize = len(batchedRequests)
jsonErr = &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidParams.Code, for _, entry := range batchedRequests {
Message: "limited user not authorized for this method", 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 { var msg = []byte{}
// Attempt to parse the JSON-RPC request into a known concrete if batchedRequest && batchSize > 0 {
// command. if len(results) > 0 {
parsedCmd := parseCmd(&request) // Form the batched response json
if parsedCmd.err != nil { var buffer bytes.Buffer
jsonErr = parsedCmd.err buffer.WriteByte('[')
} else { for idx, reply := range results {
result, jsonErr = s.standardCmdResult(parsedCmd, closeChan) if idx == len(results)-1 {
buffer.Write(reply)
buffer.WriteByte(']')
break
}
buffer.Write(reply)
buffer.WriteByte(',')
} }
msg = buffer.Bytes()
} }
} }
// Marshal the response. if !batchedRequest || batchSize == 0 {
msg, err := createMarshalledReply(responseID, result, jsonErr) // Respond with the first results entry for single requests
if err != nil { if len(results) > 0 {
rpcsLog.Errorf("Failed to marshal reply: %v", err) msg = results[0]
return }
} }
// Write the response. // 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. // Notify interested websocket clients about the connected block.
ntfn := btcjson.NewBlockConnectedNtfn(block.Hash().String(), block.Height(), ntfn := btcjson.NewBlockConnectedNtfn(block.Hash().String(), block.Height(),
block.MsgBlock().Header.Timestamp.Unix()) block.MsgBlock().Header.Timestamp.Unix())
marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn)
if err != nil { if err != nil {
rpcsLog.Errorf("Failed to marshal block connected notification: "+ rpcsLog.Errorf("Failed to marshal block connected notification: "+
"%v", err) "%v", err)
@ -719,7 +719,7 @@ func (*wsNotificationManager) notifyBlockDisconnected(clients map[chan struct{}]
// Notify interested websocket clients about the disconnected block. // Notify interested websocket clients about the disconnected block.
ntfn := btcjson.NewBlockDisconnectedNtfn(block.Hash().String(), ntfn := btcjson.NewBlockDisconnectedNtfn(block.Hash().String(),
block.Height(), block.MsgBlock().Header.Timestamp.Unix()) block.Height(), block.MsgBlock().Header.Timestamp.Unix())
marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn)
if err != nil { if err != nil {
rpcsLog.Errorf("Failed to marshal block disconnected "+ rpcsLog.Errorf("Failed to marshal block disconnected "+
"notification: %v", err) "notification: %v", err)
@ -765,7 +765,7 @@ func (m *wsNotificationManager) notifyFilteredBlockConnected(clients map[chan st
ntfn.SubscribedTxs = subscribedTxs[quitChan] ntfn.SubscribedTxs = subscribedTxs[quitChan]
// Marshal and queue notification. // Marshal and queue notification.
marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn)
if err != nil { if err != nil {
rpcsLog.Errorf("Failed to marshal filtered block "+ rpcsLog.Errorf("Failed to marshal filtered block "+
"connected notification: %v", err) "connected notification: %v", err)
@ -796,7 +796,7 @@ func (*wsNotificationManager) notifyFilteredBlockDisconnected(clients map[chan s
} }
ntfn := btcjson.NewFilteredBlockDisconnectedNtfn(block.Height(), ntfn := btcjson.NewFilteredBlockDisconnectedNtfn(block.Height(),
hex.EncodeToString(w.Bytes())) hex.EncodeToString(w.Bytes()))
marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn)
if err != nil { if err != nil {
rpcsLog.Errorf("Failed to marshal filtered block disconnected "+ rpcsLog.Errorf("Failed to marshal filtered block disconnected "+
"notification: %v", err) "notification: %v", err)
@ -831,7 +831,7 @@ func (m *wsNotificationManager) notifyForNewTx(clients map[chan struct{}]*wsClie
} }
ntfn := btcjson.NewTxAcceptedNtfn(txHashStr, btcutil.Amount(amount).ToBTC()) 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 { if err != nil {
rpcsLog.Errorf("Failed to marshal tx notification: %s", err.Error()) rpcsLog.Errorf("Failed to marshal tx notification: %s", err.Error())
return return
@ -854,7 +854,7 @@ func (m *wsNotificationManager) notifyForNewTx(clients map[chan struct{}]*wsClie
} }
verboseNtfn = btcjson.NewTxAcceptedVerboseNtfn(*rawTx) verboseNtfn = btcjson.NewTxAcceptedVerboseNtfn(*rawTx)
marshalledJSONVerbose, err = btcjson.MarshalCmd(nil, marshalledJSONVerbose, err = btcjson.MarshalCmd(btcjson.RpcVersion1, nil,
verboseNtfn) verboseNtfn)
if err != nil { if err != nil {
rpcsLog.Errorf("Failed to marshal verbose tx "+ 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) { func newRedeemingTxNotification(txHex string, index int, block *btcutil.Block) ([]byte, error) {
// Create and marshal the notification. // Create and marshal the notification.
ntfn := btcjson.NewRedeemingTxNtfn(txHex, blockDetails(block, index)) 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 // 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, ntfn := btcjson.NewRecvTxNtfn(txHex, blockDetails(block,
tx.Index())) tx.Index()))
marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn)
if err != nil { if err != nil {
rpcsLog.Errorf("Failed to marshal processedtx notification: %v", err) rpcsLog.Errorf("Failed to marshal processedtx notification: %v", err)
continue continue
@ -1047,7 +1047,7 @@ func (m *wsNotificationManager) notifyRelevantTxAccepted(tx *btcutil.Tx,
if len(clientsToNotify) != 0 { if len(clientsToNotify) != 0 {
n := btcjson.NewRelevantTxAcceptedNtfn(txHexString(tx.MsgTx())) n := btcjson.NewRelevantTxAcceptedNtfn(txHexString(tx.MsgTx()))
marshalled, err := btcjson.MarshalCmd(nil, n) marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, n)
if err != nil { if err != nil {
rpcsLog.Errorf("Failed to marshal notification: %v", err) rpcsLog.Errorf("Failed to marshal notification: %v", err)
return return
@ -1323,153 +1323,435 @@ out:
break out break out
} }
var request btcjson.Request var batchedRequest bool
err = json.Unmarshal(msg, &request)
if err != nil {
if !c.authenticated {
break out
}
jsonErr := &btcjson.RPCError{ // Determine request type
Code: btcjson.ErrRPCParse.Code, if bytes.HasPrefix(msg, batchedRequestPrefix) {
Message: "Failed to parse request: " + err.Error(), batchedRequest = true
} }
reply, err := createMarshalledReply(nil, nil, jsonErr)
if !batchedRequest {
var req btcjson.Request
var reply json.RawMessage
err = json.Unmarshal(msg, &req)
if err != nil { if err != nil {
rpcsLog.Errorf("Failed to marshal parse failure "+ // only process requests from authenticated clients
"reply: %v", err) if !c.authenticated {
continue break out
}
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",
} }
// 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 { if err != nil {
rpcsLog.Errorf("Failed to marshal parse failure "+ rpcsLog.Errorf("Failed to marshal reply: %v", err)
"reply: %v", err)
continue continue
} }
c.SendMessage(reply, nil) c.SendMessage(reply, nil)
continue 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 // Process a batched request
// limit the number of concurrent requests currently being if batchedRequest {
// serviced. If the semaphore can not be acquired, simply wait var batchedRequests []interface{}
// until a request finished before reading the next RPC request var results []json.RawMessage
// from the websocket client. var batchSize int
// var reply json.RawMessage
// This could be a little fancier by timing out and erroring c.serviceRequestSem.acquire()
// when it takes too long to service the request, but if that is err = json.Unmarshal(msg, &batchedRequests)
// done, the read of the next request should not be blocked by if err != nil {
// this semaphore, otherwise the next request will be read and // Only process requests from authenticated clients
// will probably sit here for another few seconds before timing if !c.authenticated {
// out as well. This will cause the total timeout duration for break out
// later requests to be much longer than the check here would }
// imply.
// jsonErr := &btcjson.RPCError{
// If a timeout is added, the semaphore acquiring should be Code: btcjson.ErrRPCParse.Code,
// moved inside of the new goroutine with a select statement Message: fmt.Sprintf("Failed to parse request: %v",
// that also reads a time.After channel. This will unblock the err),
// read of the next request from the websocket client and allow }
// many requests to be waited on concurrently. reply, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr)
c.serviceRequestSem.acquire() if err != nil {
go func() { rpcsLog.Errorf("Failed to create reply: %v", err)
c.serviceRequest(cmd) }
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() c.serviceRequestSem.release()
}() }
} }
// Ensure the connection is closed. // Ensure the connection is closed.
@ -1495,7 +1777,7 @@ func (c *wsClient) serviceRequest(r *parsedRPCCmd) {
} else { } else {
result, err = c.server.standardCmdResult(r, nil) 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 { if err != nil {
rpcsLog.Errorf("Failed to marshal reply for <%s> "+ rpcsLog.Errorf("Failed to marshal reply for <%s> "+
"command: %v", r.method, err) "command: %v", r.method, err)
@ -2125,7 +2407,7 @@ func rescanBlock(wsc *wsClient, lookups *rescanKeys, blk *btcutil.Block) {
ntfn := btcjson.NewRecvTxNtfn(txHex, ntfn := btcjson.NewRecvTxNtfn(txHex,
blockDetails(blk, tx.Index())) blockDetails(blk, tx.Index()))
marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn)
if err != nil { if err != nil {
rpcsLog.Errorf("Failed to marshal recvtx notification: %v", err) rpcsLog.Errorf("Failed to marshal recvtx notification: %v", err)
return return
@ -2492,7 +2774,7 @@ fetchRange:
hashList[i].String(), blk.Height(), hashList[i].String(), blk.Height(),
blk.MsgBlock().Header.Timestamp.Unix(), blk.MsgBlock().Header.Timestamp.Unix(),
) )
mn, err := btcjson.MarshalCmd(nil, n) mn, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, n)
if err != nil { if err != nil {
rpcsLog.Errorf("Failed to marshal rescan "+ rpcsLog.Errorf("Failed to marshal rescan "+
"progress notification: %v", err) "progress notification: %v", err)
@ -2637,7 +2919,7 @@ func handleRescan(wsc *wsClient, icmd interface{}) (interface{}, error) {
lastBlockHash.String(), lastBlock.Height(), lastBlockHash.String(), lastBlock.Height(),
lastBlock.MsgBlock().Header.Timestamp.Unix(), 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 "+ rpcsLog.Errorf("Failed to marshal rescan finished "+
"notification: %v", err) "notification: %v", err)
} else { } else {