diff --git a/btcjson/btcdextcmds_test.go b/btcjson/btcdextcmds_test.go index 143ec5224fb..aaa44144e44 100644 --- a/btcjson/btcdextcmds_test.go +++ b/btcjson/btcdextcmds_test.go @@ -211,7 +211,7 @@ func TestBtcdExtCmds(t *testing.T) { for i, test := range tests { // Marshal the command as created by the new static command // creation function. - marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd()) + marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd()) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) @@ -235,7 +235,7 @@ func TestBtcdExtCmds(t *testing.T) { // Marshal the command as created by the generic new command // creation function. - marshalled, err = btcjson.MarshalCmd(testID, cmd) + marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) diff --git a/btcjson/btcwalletextcmds_test.go b/btcjson/btcwalletextcmds_test.go index 58de1c81d0d..dea1c614657 100644 --- a/btcjson/btcwalletextcmds_test.go +++ b/btcjson/btcwalletextcmds_test.go @@ -145,7 +145,7 @@ func TestBtcWalletExtCmds(t *testing.T) { for i, test := range tests { // Marshal the command as created by the new static command // creation function. - marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd()) + marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd()) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) @@ -169,7 +169,7 @@ func TestBtcWalletExtCmds(t *testing.T) { // Marshal the command as created by the generic new command // creation function. - marshalled, err = btcjson.MarshalCmd(testID, cmd) + marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) diff --git a/btcjson/chainsvrcmds_test.go b/btcjson/chainsvrcmds_test.go index 263d6d7e5dd..7d3a68dc41d 100644 --- a/btcjson/chainsvrcmds_test.go +++ b/btcjson/chainsvrcmds_test.go @@ -1466,7 +1466,7 @@ func TestChainSvrCmds(t *testing.T) { for i, test := range tests { // Marshal the command as created by the new static command // creation function. - marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd()) + marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd()) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) @@ -1491,7 +1491,7 @@ func TestChainSvrCmds(t *testing.T) { // Marshal the command as created by the generic new command // creation function. - marshalled, err = btcjson.MarshalCmd(testID, cmd) + marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) diff --git a/btcjson/chainsvrwscmds_test.go b/btcjson/chainsvrwscmds_test.go index b0cd63cc615..03fb22c8e0b 100644 --- a/btcjson/chainsvrwscmds_test.go +++ b/btcjson/chainsvrwscmds_test.go @@ -233,7 +233,7 @@ func TestChainSvrWsCmds(t *testing.T) { for i, test := range tests { // Marshal the command as created by the new static command // creation function. - marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd()) + marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd()) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) @@ -257,7 +257,7 @@ func TestChainSvrWsCmds(t *testing.T) { // Marshal the command as created by the generic new command // creation function. - marshalled, err = btcjson.MarshalCmd(testID, cmd) + marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) diff --git a/btcjson/chainsvrwsntfns_test.go b/btcjson/chainsvrwsntfns_test.go index 2da1e7ad2fd..e2b234c2a03 100644 --- a/btcjson/chainsvrwsntfns_test.go +++ b/btcjson/chainsvrwsntfns_test.go @@ -231,7 +231,7 @@ func TestChainSvrWsNtfns(t *testing.T) { for i, test := range tests { // Marshal the notification as created by the new static // creation function. The ID is nil for notifications. - marshalled, err := btcjson.MarshalCmd(nil, test.staticNtfn()) + marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, test.staticNtfn()) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) @@ -256,7 +256,7 @@ func TestChainSvrWsNtfns(t *testing.T) { // Marshal the notification as created by the generic new // notification creation function. The ID is nil for // notifications. - marshalled, err = btcjson.MarshalCmd(nil, cmd) + marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, nil, cmd) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) diff --git a/btcjson/cmdparse.go b/btcjson/cmdparse.go index 117f72f1879..4fb8dd62601 100644 --- a/btcjson/cmdparse.go +++ b/btcjson/cmdparse.go @@ -36,7 +36,7 @@ func makeParams(rt reflect.Type, rv reflect.Value) []interface{} { // is suitable for transmission to an RPC server. The provided command type // must be a registered type. All commands provided by this package are // registered by default. -func MarshalCmd(id interface{}, cmd interface{}) ([]byte, error) { +func MarshalCmd(rpcVersion RPCVersion, id interface{}, cmd interface{}) ([]byte, error) { // Look up the cmd type and error out if not registered. rt := reflect.TypeOf(cmd) registerLock.RLock() @@ -60,7 +60,7 @@ func MarshalCmd(id interface{}, cmd interface{}) ([]byte, error) { params := makeParams(rt.Elem(), rv.Elem()) // Generate and marshal the final JSON-RPC request. - rawCmd, err := NewRequest(id, method, params) + rawCmd, err := NewRequest(rpcVersion, id, method, params) if err != nil { return nil, err } diff --git a/btcjson/cmdparse_test.go b/btcjson/cmdparse_test.go index 71547271df5..f2585edf5cd 100644 --- a/btcjson/cmdparse_test.go +++ b/btcjson/cmdparse_test.go @@ -460,7 +460,7 @@ func TestMarshalCmd(t *testing.T) { t.Logf("Running %d tests", len(tests)) for i, test := range tests { - bytes, err := btcjson.MarshalCmd(test.id, test.cmd) + bytes, err := btcjson.MarshalCmd(btcjson.RpcVersion1, test.id, test.cmd) if err != nil { t.Errorf("Test #%d (%s) wrong error - got %T (%v)", i, test.name, err, err) @@ -507,7 +507,7 @@ func TestMarshalCmdErrors(t *testing.T) { t.Logf("Running %d tests", len(tests)) for i, test := range tests { - _, err := btcjson.MarshalCmd(test.id, test.cmd) + _, err := btcjson.MarshalCmd(btcjson.RpcVersion1, test.id, test.cmd) if reflect.TypeOf(err) != reflect.TypeOf(test.err) { t.Errorf("Test #%d (%s) wrong error - got %T (%v), "+ "want %T", i, test.name, err, err, test.err) @@ -535,7 +535,7 @@ func TestUnmarshalCmdErrors(t *testing.T) { { name: "unregistered type", request: btcjson.Request{ - Jsonrpc: "1.0", + Jsonrpc: btcjson.RpcVersion1, Method: "bogusmethod", Params: nil, ID: nil, @@ -545,7 +545,7 @@ func TestUnmarshalCmdErrors(t *testing.T) { { name: "incorrect number of params", request: btcjson.Request{ - Jsonrpc: "1.0", + Jsonrpc: btcjson.RpcVersion1, Method: "getblockcount", Params: []json.RawMessage{[]byte(`"bogusparam"`)}, ID: nil, @@ -555,7 +555,7 @@ func TestUnmarshalCmdErrors(t *testing.T) { { name: "invalid type for a parameter", request: btcjson.Request{ - Jsonrpc: "1.0", + Jsonrpc: btcjson.RpcVersion1, Method: "getblock", Params: []json.RawMessage{[]byte("1")}, ID: nil, @@ -565,7 +565,7 @@ func TestUnmarshalCmdErrors(t *testing.T) { { name: "invalid JSON for a parameter", request: btcjson.Request{ - Jsonrpc: "1.0", + Jsonrpc: btcjson.RpcVersion1, Method: "getblock", Params: []json.RawMessage{[]byte(`"1`)}, ID: nil, diff --git a/btcjson/example_test.go b/btcjson/example_test.go index 73dd804073e..74478e7482c 100644 --- a/btcjson/example_test.go +++ b/btcjson/example_test.go @@ -27,7 +27,7 @@ func ExampleMarshalCmd() { // server. Typically the client would increment the id here which is // request so the response can be identified. id := 1 - marshalledBytes, err := btcjson.MarshalCmd(id, gbCmd) + marshalledBytes, err := btcjson.MarshalCmd(btcjson.RpcVersion1, id, gbCmd) if err != nil { fmt.Println(err) return @@ -95,7 +95,7 @@ func ExampleUnmarshalCmd() { func ExampleMarshalResponse() { // Marshal a new JSON-RPC response. For example, this is a response // to a getblockheight request. - marshalledBytes, err := btcjson.MarshalResponse(1, 350001, nil) + marshalledBytes, err := btcjson.MarshalResponse(btcjson.RpcVersion1, 1, 350001, nil) if err != nil { fmt.Println(err) return @@ -107,7 +107,7 @@ func ExampleMarshalResponse() { fmt.Printf("%s\n", marshalledBytes) // Output: - // {"result":350001,"error":null,"id":1} + // {"jsonrpc":"1.0","result":350001,"error":null,"id":1} } // This example demonstrates how to unmarshal a JSON-RPC response and then @@ -116,7 +116,7 @@ func Example_unmarshalResponse() { // Ordinarily this would be read from the wire, but for this example, // it is hard coded here for clarity. This is an example response to a // getblockheight request. - data := []byte(`{"result":350001,"error":null,"id":1}`) + data := []byte(`{"jsonrpc":"1.0","result":350001,"error":null,"id":1}`) // Unmarshal the raw bytes from the wire into a JSON-RPC response. var response btcjson.Response diff --git a/btcjson/jsonrpc.go b/btcjson/jsonrpc.go index 0ead85e5ee1..553a7bc37f3 100644 --- a/btcjson/jsonrpc.go +++ b/btcjson/jsonrpc.go @@ -9,6 +9,33 @@ import ( "fmt" ) +// RPCVersion is a type to indicate RPC versions. +type RPCVersion string + +const ( + // version 1 of rpc + RpcVersion1 RPCVersion = RPCVersion("1.0") + // version 2 of rpc + RpcVersion2 RPCVersion = RPCVersion("2.0") +) + +var validRpcVersions = []RPCVersion{RpcVersion1, RpcVersion2} + +// check if the rpc version is a valid version +func (r RPCVersion) IsValid() bool { + for _, version := range validRpcVersions { + if version == r { + return true + } + } + return false +} + +// cast rpc version to a string +func (r RPCVersion) String() string { + return string(r) +} + // RPCErrorCode represents an error code to be used as a part of an RPCError // which is in turn used in a JSON-RPC Response object. // @@ -67,21 +94,74 @@ func IsValidIDType(id interface{}) bool { // requests, however this struct it being exported in case the caller wants to // construct raw requests for some reason. type Request struct { - Jsonrpc string `json:"jsonrpc"` + Jsonrpc RPCVersion `json:"jsonrpc"` Method string `json:"method"` Params []json.RawMessage `json:"params"` ID interface{} `json:"id"` } -// NewRequest returns a new JSON-RPC 1.0 request object given the provided id, -// method, and parameters. The parameters are marshalled into a json.RawMessage -// for the Params field of the returned request object. This function is only -// provided in case the caller wants to construct raw requests for some reason. -// -// Typically callers will instead want to create a registered concrete command -// type with the NewCmd or NewCmd functions and call the MarshalCmd -// function with that command to generate the marshalled JSON-RPC request. -func NewRequest(id interface{}, method string, params []interface{}) (*Request, error) { +// UnmarshalJSON is a custom unmarshal func for the Request struct. The param +// field defaults to an empty json.RawMessage array it is omitted by the request +// or nil if the supplied value is invalid. +func (request *Request) UnmarshalJSON(b []byte) error { + // Step 1: Create a type alias of the original struct. + type Alias Request + + // Step 2: Create an anonymous struct with raw replacements for the special + // fields. + aux := &struct { + Jsonrpc string `json:"jsonrpc"` + Params []interface{} `json:"params"` + *Alias + }{ + Alias: (*Alias)(request), + } + + // Step 3: Unmarshal the data into the anonymous struct. + err := json.Unmarshal(b, &aux) + if err != nil { + return err + } + + // Step 4: Convert the raw fields to the desired types + + version := RPCVersion(aux.Jsonrpc) + if version.IsValid() { + request.Jsonrpc = version + } + + rawParams := make([]json.RawMessage, 0) + + for _, param := range aux.Params { + marshalledParam, err := json.Marshal(param) + if err != nil { + return err + } + + rawMessage := json.RawMessage(marshalledParam) + rawParams = append(rawParams, rawMessage) + } + + request.Params = rawParams + + return nil +} + +// NewRequest returns a new JSON-RPC request object given the provided rpc +// version, id, method, and parameters. The parameters are marshalled into a +// json.RawMessage for the Params field of the returned request object. This +// function is only provided in case the caller wants to construct raw requests +// for some reason. Typically callers will instead want to create a registered +// concrete command type with the NewCmd or NewCmd functions and call the +// MarshalCmd function with that command to generate the marshalled JSON-RPC +// request. +func NewRequest(rpcVersion RPCVersion, id interface{}, method string, params []interface{}) (*Request, error) { + // default to JSON-RPC 1.0 if RPC type is not specified + if !rpcVersion.IsValid() { + str := fmt.Sprintf("rpcversion '%s' is invalid", rpcVersion) + return nil, makeError(ErrInvalidType, str) + } + if !IsValidIDType(id) { str := fmt.Sprintf("the id of type '%T' is invalid", id) return nil, makeError(ErrInvalidType, str) @@ -98,30 +178,35 @@ func NewRequest(id interface{}, method string, params []interface{}) (*Request, } return &Request{ - Jsonrpc: "1.0", + Jsonrpc: rpcVersion, ID: id, Method: method, Params: rawParams, }, nil } -// Response is the general form of a JSON-RPC response. The type of the Result -// field varies from one command to the next, so it is implemented as an -// interface. The ID field has to be a pointer for Go to put a null in it when +// Response is the general form of a JSON-RPC response. The type of the +// Result field varies from one command to the next, so it is implemented as an +// interface. The ID field has to be a pointer to allow for a nil value when // empty. type Response struct { - Result json.RawMessage `json:"result"` - Error *RPCError `json:"error"` - ID *interface{} `json:"id"` + Jsonrpc RPCVersion `json:"jsonrpc"` + Result json.RawMessage `json:"result"` + Error *RPCError `json:"error"` + ID *interface{} `json:"id"` } -// NewResponse returns a new JSON-RPC response object given the provided id, -// marshalled result, and RPC error. This function is only provided in case the -// caller wants to construct raw responses for some reason. -// +// NewResponse returns a new JSON-RPC response object given the provided rpc +// version, id, marshalled result, and RPC error. This function is only +// provided in case the caller wants to construct raw responses for some reason. // Typically callers will instead want to create the fully marshalled JSON-RPC // response to send over the wire with the MarshalResponse function. -func NewResponse(id interface{}, marshalledResult []byte, rpcErr *RPCError) (*Response, error) { +func NewResponse(rpcVersion RPCVersion, id interface{}, marshalledResult []byte, rpcErr *RPCError) (*Response, error) { + if !rpcVersion.IsValid() { + str := fmt.Sprintf("rpcversion '%s' is invalid", rpcVersion) + return nil, makeError(ErrInvalidType, str) + } + if !IsValidIDType(id) { str := fmt.Sprintf("the id of type '%T' is invalid", id) return nil, makeError(ErrInvalidType, str) @@ -129,20 +214,27 @@ func NewResponse(id interface{}, marshalledResult []byte, rpcErr *RPCError) (*Re pid := &id return &Response{ - Result: marshalledResult, - Error: rpcErr, - ID: pid, + Jsonrpc: rpcVersion, + Result: marshalledResult, + Error: rpcErr, + ID: pid, }, nil } -// MarshalResponse marshals the passed id, result, and RPCError to a JSON-RPC -// response byte slice that is suitable for transmission to a JSON-RPC client. -func MarshalResponse(id interface{}, result interface{}, rpcErr *RPCError) ([]byte, error) { +// MarshalResponse marshals the passed rpc version, id, result, and RPCError to +// a JSON-RPC response byte slice that is suitable for transmission to a +// JSON-RPC client. +func MarshalResponse(rpcVersion RPCVersion, id interface{}, result interface{}, rpcErr *RPCError) ([]byte, error) { + if !rpcVersion.IsValid() { + str := fmt.Sprintf("rpcversion '%s' is invalid", rpcVersion) + return nil, makeError(ErrInvalidType, str) + } + marshalledResult, err := json.Marshal(result) if err != nil { return nil, err } - response, err := NewResponse(id, marshalledResult, rpcErr) + response, err := NewResponse(rpcVersion, id, marshalledResult, rpcErr) if err != nil { return nil, err } diff --git a/btcjson/jsonrpc_test.go b/btcjson/jsonrpc_test.go index 7a5d75618ca..13d98e89f52 100644 --- a/btcjson/jsonrpc_test.go +++ b/btcjson/jsonrpc_test.go @@ -68,7 +68,7 @@ func TestMarshalResponse(t *testing.T) { name: "ordinary bool result with no error", result: true, jsonErr: nil, - expected: []byte(`{"result":true,"error":null,"id":1}`), + expected: []byte(`{"jsonrpc":"1.0","result":true,"error":null,"id":1}`), }, { name: "result with error", @@ -76,14 +76,14 @@ func TestMarshalResponse(t *testing.T) { jsonErr: func() *btcjson.RPCError { return btcjson.NewRPCError(btcjson.ErrRPCBlockNotFound, "123 not found") }(), - expected: []byte(`{"result":null,"error":{"code":-5,"message":"123 not found"},"id":1}`), + expected: []byte(`{"jsonrpc":"1.0","result":null,"error":{"code":-5,"message":"123 not found"},"id":1}`), }, } t.Logf("Running %d tests", len(tests)) for i, test := range tests { _, _ = i, test - marshalled, err := btcjson.MarshalResponse(testID, test.result, test.jsonErr) + marshalled, err := btcjson.MarshalResponse(btcjson.RpcVersion1, testID, test.result, test.jsonErr) if err != nil { t.Errorf("Test #%d (%s) unexpected error: %v", i, test.name, err) @@ -104,7 +104,7 @@ func TestMiscErrors(t *testing.T) { // Force an error in NewRequest by giving it a parameter type that is // not supported. - _, err := btcjson.NewRequest(nil, "test", []interface{}{make(chan int)}) + _, err := btcjson.NewRequest(btcjson.RpcVersion1, nil, "test", []interface{}{make(chan int)}) if err == nil { t.Error("NewRequest: did not receive error") return @@ -113,7 +113,7 @@ func TestMiscErrors(t *testing.T) { // Force an error in MarshalResponse by giving it an id type that is not // supported. wantErr := btcjson.Error{ErrorCode: btcjson.ErrInvalidType} - _, err = btcjson.MarshalResponse(make(chan int), nil, nil) + _, err = btcjson.MarshalResponse(btcjson.RpcVersion1, make(chan int), nil, nil) if jerr, ok := err.(btcjson.Error); !ok || jerr.ErrorCode != wantErr.ErrorCode { t.Errorf("MarshalResult: did not receive expected error - got "+ "%v (%[1]T), want %v (%[2]T)", err, wantErr) @@ -122,7 +122,7 @@ func TestMiscErrors(t *testing.T) { // Force an error in MarshalResponse by giving it a result type that // can't be marshalled. - _, err = btcjson.MarshalResponse(1, make(chan int), nil) + _, err = btcjson.MarshalResponse(btcjson.RpcVersion1, 1, make(chan int), nil) if _, ok := err.(*json.UnsupportedTypeError); !ok { wantErr := &json.UnsupportedTypeError{} t.Errorf("MarshalResult: did not receive expected error - got "+ diff --git a/btcjson/walletsvrcmds_test.go b/btcjson/walletsvrcmds_test.go index 9ee6f0c749a..9c68d260f6a 100644 --- a/btcjson/walletsvrcmds_test.go +++ b/btcjson/walletsvrcmds_test.go @@ -1800,7 +1800,7 @@ func TestWalletSvrCmds(t *testing.T) { for i, test := range tests { // Marshal the command as created by the new static command // creation function. - marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd()) + marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd()) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) @@ -1824,7 +1824,7 @@ func TestWalletSvrCmds(t *testing.T) { // Marshal the command as created by the generic new command // creation function. - marshalled, err = btcjson.MarshalCmd(testID, cmd) + marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) diff --git a/btcjson/walletsvrwscmds_test.go b/btcjson/walletsvrwscmds_test.go index 17144b6ea7d..110a893b23c 100644 --- a/btcjson/walletsvrwscmds_test.go +++ b/btcjson/walletsvrwscmds_test.go @@ -195,7 +195,7 @@ func TestWalletSvrWsCmds(t *testing.T) { for i, test := range tests { // Marshal the command as created by the new static command // creation function. - marshalled, err := btcjson.MarshalCmd(testID, test.staticCmd()) + marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, testID, test.staticCmd()) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) @@ -219,7 +219,7 @@ func TestWalletSvrWsCmds(t *testing.T) { // Marshal the command as created by the generic new command // creation function. - marshalled, err = btcjson.MarshalCmd(testID, cmd) + marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, testID, cmd) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) diff --git a/btcjson/walletsvrwsntfns_test.go b/btcjson/walletsvrwsntfns_test.go index 7662b3c2a10..111916627e3 100644 --- a/btcjson/walletsvrwsntfns_test.go +++ b/btcjson/walletsvrwsntfns_test.go @@ -122,7 +122,7 @@ func TestWalletSvrWsNtfns(t *testing.T) { for i, test := range tests { // Marshal the notification as created by the new static // creation function. The ID is nil for notifications. - marshalled, err := btcjson.MarshalCmd(nil, test.staticNtfn()) + marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, test.staticNtfn()) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) @@ -147,7 +147,7 @@ func TestWalletSvrWsNtfns(t *testing.T) { // Marshal the notification as created by the generic new // notification creation function. The ID is nil for // notifications. - marshalled, err = btcjson.MarshalCmd(nil, cmd) + marshalled, err = btcjson.MarshalCmd(btcjson.RpcVersion1, nil, cmd) if err != nil { t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i, test.name, err) diff --git a/cmd/btcctl/btcctl.go b/cmd/btcctl/btcctl.go index 5c412f867fa..771d5f7ed78 100644 --- a/cmd/btcctl/btcctl.go +++ b/cmd/btcctl/btcctl.go @@ -127,7 +127,7 @@ func main() { // Marshal the command into a JSON-RPC byte slice in preparation for // sending it to the RPC server. - marshalledJSON, err := btcjson.MarshalCmd(1, cmd) + marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, 1, cmd) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) diff --git a/integration/bip0009_test.go b/integration/bip0009_test.go index b72065594cf..9bdec34fbbf 100644 --- a/integration/bip0009_test.go +++ b/integration/bip0009_test.go @@ -33,7 +33,7 @@ const ( // ensures its version either has the provided bit set or unset per the set // flag. func assertVersionBit(r *rpctest.Harness, t *testing.T, hash *chainhash.Hash, bit uint8, set bool) { - block, err := r.Node.GetBlock(hash) + block, err := r.Client.GetBlock(hash) if err != nil { t.Fatalf("failed to retrieve block %v: %v", hash, err) } @@ -53,7 +53,7 @@ func assertVersionBit(r *rpctest.Harness, t *testing.T, hash *chainhash.Hash, bi // assertChainHeight retrieves the current chain height from the given test // harness and ensures it matches the provided expected height. func assertChainHeight(r *rpctest.Harness, t *testing.T, expectedHeight uint32) { - height, err := r.Node.GetBlockCount() + height, err := r.Client.GetBlockCount() if err != nil { t.Fatalf("failed to retrieve block height: %v", err) } @@ -96,7 +96,7 @@ func assertSoftForkStatus(r *rpctest.Harness, t *testing.T, forkKey string, stat "threshold state %v to string", line, state) } - info, err := r.Node.GetBlockChainInfo() + info, err := r.Client.GetBlockChainInfo() if err != nil { t.Fatalf("failed to retrieve chain info: %v", err) } @@ -339,7 +339,7 @@ func TestBIP0009Mining(t *testing.T) { // in the defined threshold state. deployment := &r.ActiveNet.Deployments[chaincfg.DeploymentTestDummy] testDummyBitNum := deployment.BitNumber - hashes, err := r.Node.Generate(1) + hashes, err := r.Client.Generate(1) if err != nil { t.Fatalf("unable to generate blocks: %v", err) } @@ -358,7 +358,7 @@ func TestBIP0009Mining(t *testing.T) { // dummy deployment as started. confirmationWindow := r.ActiveNet.MinerConfirmationWindow numNeeded := confirmationWindow - 1 - hashes, err = r.Node.Generate(numNeeded) + hashes, err = r.Client.Generate(numNeeded) if err != nil { t.Fatalf("failed to generated %d blocks: %v", numNeeded, err) } @@ -373,7 +373,7 @@ func TestBIP0009Mining(t *testing.T) { // The last generated block should still have the test bit set in the // version since the btcd mining code will have recognized the test // dummy deployment as locked in. - hashes, err = r.Node.Generate(confirmationWindow) + hashes, err = r.Client.Generate(confirmationWindow) if err != nil { t.Fatalf("failed to generated %d blocks: %v", confirmationWindow, err) @@ -392,7 +392,7 @@ func TestBIP0009Mining(t *testing.T) { // version since the btcd mining code will have recognized the test // dummy deployment as activated and thus there is no longer any need // to set the bit. - hashes, err = r.Node.Generate(confirmationWindow) + hashes, err = r.Client.Generate(confirmationWindow) if err != nil { t.Fatalf("failed to generated %d blocks: %v", confirmationWindow, err) diff --git a/integration/csv_fork_test.go b/integration/csv_fork_test.go index b623487e595..31466349669 100644 --- a/integration/csv_fork_test.go +++ b/integration/csv_fork_test.go @@ -57,14 +57,14 @@ func makeTestOutput(r *rpctest.Harness, t *testing.T, if err != nil { return nil, nil, nil, err } - txHash, err := r.Node.SendRawTransaction(fundTx, true) + txHash, err := r.Client.SendRawTransaction(fundTx, true) if err != nil { return nil, nil, nil, err } // The transaction created above should be included within the next // generated block. - blockHash, err := r.Node.Generate(1) + blockHash, err := r.Client.Generate(1) if err != nil { return nil, nil, nil, err } @@ -151,7 +151,7 @@ func TestBIP0113Activation(t *testing.T) { // We set the lock-time of the transaction to just one minute after the // current MTP of the chain. - chainInfo, err := r.Node.GetBlockChainInfo() + chainInfo, err := r.Client.GetBlockChainInfo() if err != nil { t.Fatalf("unable to query for chain info: %v", err) } @@ -167,7 +167,7 @@ func TestBIP0113Activation(t *testing.T) { // This transaction should be rejected from the mempool as using MTP // for transactions finality is now a policy rule. Additionally, the // exact error should be the rejection of a non-final transaction. - _, err = r.Node.SendRawTransaction(tx, true) + _, err = r.Client.SendRawTransaction(tx, true) if err == nil { t.Fatalf("transaction accepted, but should be non-final") } else if !strings.Contains(err.Error(), "not finalized") { @@ -201,7 +201,7 @@ func TestBIP0113Activation(t *testing.T) { // height 299. The getblockchaininfo call checks the state for the // block AFTER the current height. numBlocks := (r.ActiveNet.MinerConfirmationWindow * 2) - 4 - if _, err := r.Node.Generate(numBlocks); err != nil { + if _, err := r.Client.Generate(numBlocks); err != nil { t.Fatalf("unable to generate blocks: %v", err) } @@ -220,7 +220,7 @@ func TestBIP0113Activation(t *testing.T) { // rejected. timeLockDeltas := []int64{-1, 0, 1} for _, timeLockDelta := range timeLockDeltas { - chainInfo, err = r.Node.GetBlockChainInfo() + chainInfo, err = r.Client.GetBlockChainInfo() if err != nil { t.Fatalf("unable to query for chain info: %v", err) } @@ -257,7 +257,7 @@ func TestBIP0113Activation(t *testing.T) { // accepted as it has a lock-time of one // second _before_ the current MTP. - _, err = r.Node.SendRawTransaction(tx, true) + _, err = r.Client.SendRawTransaction(tx, true) if err == nil && timeLockDelta >= 0 { t.Fatal("transaction was accepted into the mempool " + "but should be rejected!") @@ -366,7 +366,7 @@ func spendCSVOutput(redeemScript []byte, csvUTXO *wire.OutPoint, func assertTxInBlock(r *rpctest.Harness, t *testing.T, blockHash *chainhash.Hash, txid *chainhash.Hash) { - block, err := r.Node.GetBlock(blockHash) + block, err := r.Client.GetBlock(blockHash) if err != nil { t.Fatalf("unable to get block: %v", err) } @@ -449,10 +449,10 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) { // As the transaction is p2sh it should be accepted into the // mempool and found within the next generated block. - if _, err := r.Node.SendRawTransaction(tx, true); err != nil { + if _, err := r.Client.SendRawTransaction(tx, true); err != nil { t.Fatalf("unable to broadcast tx: %v", err) } - blocks, err := r.Node.Generate(1) + blocks, err := r.Client.Generate(1) if err != nil { t.Fatalf("unable to generate blocks: %v", err) } @@ -469,7 +469,7 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) { // This transaction should be rejected from the mempool since // CSV validation is already mempool policy pre-fork. - _, err = r.Node.SendRawTransaction(spendingTx, true) + _, err = r.Client.SendRawTransaction(spendingTx, true) if err == nil { t.Fatalf("transaction should have been rejected, but was " + "instead accepted") @@ -496,7 +496,7 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) { // height 299. The getblockchaininfo call checks the state for the // block AFTER the current height. numBlocks := (r.ActiveNet.MinerConfirmationWindow * 2) - 8 - if _, err := r.Node.Generate(numBlocks); err != nil { + if _, err := r.Client.Generate(numBlocks); err != nil { t.Fatalf("unable to generate blocks: %v", err) } @@ -530,7 +530,7 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) { t.Fatalf("unable to create CSV output: %v", err) } - if _, err := r.Node.SendRawTransaction(tx, true); err != nil { + if _, err := r.Client.SendRawTransaction(tx, true); err != nil { t.Fatalf("unable to broadcast transaction: %v", err) } @@ -542,17 +542,17 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) { } // Mine a single block including all the transactions generated above. - if _, err := r.Node.Generate(1); err != nil { + if _, err := r.Client.Generate(1); err != nil { t.Fatalf("unable to generate block: %v", err) } // Now mine 10 additional blocks giving the inputs generated above a // age of 11. Space out each block 10 minutes after the previous block. - prevBlockHash, err := r.Node.GetBestBlockHash() + prevBlockHash, err := r.Client.GetBestBlockHash() if err != nil { t.Fatalf("unable to get prior block hash: %v", err) } - prevBlock, err := r.Node.GetBlock(prevBlockHash) + prevBlock, err := r.Client.GetBlock(prevBlockHash) if err != nil { t.Fatalf("unable to get block: %v", err) } @@ -652,7 +652,7 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) { } for i, test := range tests { - txid, err := r.Node.SendRawTransaction(test.tx, true) + txid, err := r.Client.SendRawTransaction(test.tx, true) switch { // Test case passes, nothing further to report. case test.accept && err == nil: @@ -686,7 +686,7 @@ func TestBIP0068AndBIP0112Activation(t *testing.T) { // Generate a block, the transaction should be included within // the newly mined block. - blockHashes, err := r.Node.Generate(1) + blockHashes, err := r.Client.Generate(1) if err != nil { t.Fatalf("unable to mine block: %v", err) } diff --git a/integration/rpcserver_test.go b/integration/rpcserver_test.go index e5528453d45..5875b35353a 100644 --- a/integration/rpcserver_test.go +++ b/integration/rpcserver_test.go @@ -15,22 +15,24 @@ import ( "testing" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/integration/rpctest" + "github.com/btcsuite/btcd/rpcclient" ) func testGetBestBlock(r *rpctest.Harness, t *testing.T) { - _, prevbestHeight, err := r.Node.GetBestBlock() + _, prevbestHeight, err := r.Client.GetBestBlock() if err != nil { t.Fatalf("Call to `getbestblock` failed: %v", err) } // Create a new block connecting to the current tip. - generatedBlockHashes, err := r.Node.Generate(1) + generatedBlockHashes, err := r.Client.Generate(1) if err != nil { t.Fatalf("Unable to generate block: %v", err) } - bestHash, bestHeight, err := r.Node.GetBestBlock() + bestHash, bestHeight, err := r.Client.GetBestBlock() if err != nil { t.Fatalf("Call to `getbestblock` failed: %v", err) } @@ -50,17 +52,17 @@ func testGetBestBlock(r *rpctest.Harness, t *testing.T) { func testGetBlockCount(r *rpctest.Harness, t *testing.T) { // Save the current count. - currentCount, err := r.Node.GetBlockCount() + currentCount, err := r.Client.GetBlockCount() if err != nil { t.Fatalf("Unable to get block count: %v", err) } - if _, err := r.Node.Generate(1); err != nil { + if _, err := r.Client.Generate(1); err != nil { t.Fatalf("Unable to generate block: %v", err) } // Count should have increased by one. - newCount, err := r.Node.GetBlockCount() + newCount, err := r.Client.GetBlockCount() if err != nil { t.Fatalf("Unable to get block count: %v", err) } @@ -72,17 +74,17 @@ func testGetBlockCount(r *rpctest.Harness, t *testing.T) { func testGetBlockHash(r *rpctest.Harness, t *testing.T) { // Create a new block connecting to the current tip. - generatedBlockHashes, err := r.Node.Generate(1) + generatedBlockHashes, err := r.Client.Generate(1) if err != nil { t.Fatalf("Unable to generate block: %v", err) } - info, err := r.Node.GetInfo() + info, err := r.Client.GetInfo() if err != nil { t.Fatalf("call to getinfo cailed: %v", err) } - blockHash, err := r.Node.GetBlockHash(int64(info.Blocks)) + blockHash, err := r.Client.GetBlockHash(int64(info.Blocks)) if err != nil { t.Fatalf("Call to `getblockhash` failed: %v", err) } @@ -94,10 +96,50 @@ func testGetBlockHash(r *rpctest.Harness, t *testing.T) { } } +func testBulkClient(r *rpctest.Harness, t *testing.T) { + // Create a new block connecting to the current tip. + generatedBlockHashes, err := r.Client.Generate(20) + if err != nil { + t.Fatalf("Unable to generate block: %v", err) + } + + var futureBlockResults []rpcclient.FutureGetBlockResult + for _, hash := range generatedBlockHashes { + futureBlockResults = append(futureBlockResults, r.BatchClient.GetBlockAsync(hash)) + } + + err = r.BatchClient.Send() + if err != nil { + t.Fatal(err) + } + + isKnownBlockHash := func(blockHash chainhash.Hash) bool { + for _, hash := range generatedBlockHashes { + if blockHash.IsEqual(hash) { + return true + } + } + return false + } + + for _, block := range futureBlockResults { + msgBlock, err := block.Receive() + if err != nil { + t.Fatal(err) + } + blockHash := msgBlock.Header.BlockHash() + if !isKnownBlockHash(blockHash) { + t.Fatalf("expected hash %s to be in generated hash list", blockHash) + } + } + +} + var rpcTestCases = []rpctest.HarnessTestCase{ testGetBestBlock, testGetBlockCount, testGetBlockHash, + testBulkClient, } var primaryHarness *rpctest.Harness diff --git a/integration/rpctest/rpc_harness.go b/integration/rpctest/rpc_harness.go index ff8c0216b1b..679ae4e4783 100644 --- a/integration/rpctest/rpc_harness.go +++ b/integration/rpctest/rpc_harness.go @@ -92,9 +92,10 @@ type Harness struct { // attempts. ConnectionRetryTimeout time.Duration - Node *rpcclient.Client - node *node - handlers *rpcclient.NotificationHandlers + Client *rpcclient.Client + BatchClient *rpcclient.Client + node *node + handlers *rpcclient.NotificationHandlers wallet *memWallet @@ -245,13 +246,13 @@ func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error { // Filter transactions that pay to the coinbase associated with the // wallet. filterAddrs := []btcutil.Address{h.wallet.coinbaseAddr} - if err := h.Node.LoadTxFilter(true, filterAddrs, nil); err != nil { + if err := h.Client.LoadTxFilter(true, filterAddrs, nil); err != nil { return err } // Ensure btcd properly dispatches our registered call-back for each new // block. Otherwise, the memWallet won't function properly. - if err := h.Node.NotifyBlocks(); err != nil { + if err := h.Client.NotifyBlocks(); err != nil { return err } @@ -260,7 +261,7 @@ func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error { if createTestChain && numMatureOutputs != 0 { numToGenerate := (uint32(h.ActiveNet.CoinbaseMaturity) + numMatureOutputs) - _, err := h.Node.Generate(numToGenerate) + _, err := h.Client.Generate(numToGenerate) if err != nil { return err } @@ -268,7 +269,7 @@ func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error { // Block until the wallet has fully synced up to the tip of the main // chain. - _, height, err := h.Node.GetBestBlock() + _, height, err := h.Client.GetBestBlock() if err != nil { return err } @@ -289,8 +290,12 @@ func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error { // // This function MUST be called with the harness state mutex held (for writes). func (h *Harness) tearDown() error { - if h.Node != nil { - h.Node.Shutdown() + if h.Client != nil { + h.Client.Shutdown() + } + + if h.BatchClient != nil { + h.BatchClient.Shutdown() } if err := h.node.shutdown(); err != nil { @@ -325,24 +330,38 @@ func (h *Harness) TearDown() error { // we're not able to establish a connection, this function returns with an // error. func (h *Harness) connectRPCClient() error { - var client *rpcclient.Client + var client, batchClient *rpcclient.Client var err error rpcConf := h.node.config.rpcConnConfig() + batchConf := h.node.config.rpcConnConfig() + batchConf.HTTPPostMode = true for i := 0; i < h.MaxConnRetries; i++ { - if client, err = rpcclient.New(&rpcConf, h.handlers); err != nil { - time.Sleep(time.Duration(i) * h.ConnectionRetryTimeout) - continue + fail := false + if client == nil { + if client, err = rpcclient.New(&rpcConf, h.handlers); err != nil { + time.Sleep(time.Duration(i) * h.ConnectionRetryTimeout) + fail = true + } + } + if batchClient == nil { + if batchClient, err = rpcclient.NewBatch(&batchConf); err != nil { + time.Sleep(time.Duration(i) * h.ConnectionRetryTimeout) + fail = true + } + } + if !fail { + break } - break } - if client == nil { + if client == nil || batchClient == nil { return fmt.Errorf("connection timeout") } - h.Node = client + h.Client = client h.wallet.SetRPCClient(client) + h.BatchClient = batchClient return nil } @@ -464,11 +483,11 @@ func (h *Harness) GenerateAndSubmitBlockWithCustomCoinbaseOutputs( blockVersion = BlockVersion } - prevBlockHash, prevBlockHeight, err := h.Node.GetBestBlock() + prevBlockHash, prevBlockHeight, err := h.Client.GetBestBlock() if err != nil { return nil, err } - mBlock, err := h.Node.GetBlock(prevBlockHash) + mBlock, err := h.Client.GetBlock(prevBlockHash) if err != nil { return nil, err } @@ -483,7 +502,7 @@ func (h *Harness) GenerateAndSubmitBlockWithCustomCoinbaseOutputs( } // Submit the block to the simnet node. - if err := h.Node.SubmitBlock(newBlock, nil); err != nil { + if err := h.Client.SubmitBlock(newBlock, nil); err != nil { return nil, err } diff --git a/integration/rpctest/rpc_harness_test.go b/integration/rpctest/rpc_harness_test.go index 25faf969012..df753e3126a 100644 --- a/integration/rpctest/rpc_harness_test.go +++ b/integration/rpctest/rpc_harness_test.go @@ -43,7 +43,7 @@ func testSendOutputs(r *Harness, t *testing.T) { } assertTxMined := func(txid *chainhash.Hash, blockHash *chainhash.Hash) { - block, err := r.Node.GetBlock(blockHash) + block, err := r.Client.GetBlock(blockHash) if err != nil { t.Fatalf("unable to get block: %v", err) } @@ -67,7 +67,7 @@ func testSendOutputs(r *Harness, t *testing.T) { // Generate a single block, the transaction the wallet created should // be found in this block. - blockHashes, err := r.Node.Generate(1) + blockHashes, err := r.Client.Generate(1) if err != nil { t.Fatalf("unable to generate single block: %v", err) } @@ -76,7 +76,7 @@ func testSendOutputs(r *Harness, t *testing.T) { // Next, generate a spend much greater than the block reward. This // transaction should also have been mined properly. txid = genSpend(btcutil.Amount(500 * btcutil.SatoshiPerBitcoin)) - blockHashes, err = r.Node.Generate(1) + blockHashes, err = r.Client.Generate(1) if err != nil { t.Fatalf("unable to generate single block: %v", err) } @@ -84,7 +84,7 @@ func testSendOutputs(r *Harness, t *testing.T) { } func assertConnectedTo(t *testing.T, nodeA *Harness, nodeB *Harness) { - nodeAPeers, err := nodeA.Node.GetPeerInfo() + nodeAPeers, err := nodeA.Client.GetPeerInfo() if err != nil { t.Fatalf("unable to get nodeA's peer info") } @@ -170,7 +170,7 @@ func testActiveHarnesses(r *Harness, t *testing.T) { func testJoinMempools(r *Harness, t *testing.T) { // Assert main test harness has no transactions in its mempool. - pooledHashes, err := r.Node.GetRawMempool() + pooledHashes, err := r.Client.GetRawMempool() if err != nil { t.Fatalf("unable to get mempool for main test harness: %v", err) } @@ -210,7 +210,7 @@ func testJoinMempools(r *Harness, t *testing.T) { if err != nil { t.Fatalf("coinbase spend failed: %v", err) } - if _, err := r.Node.SendRawTransaction(testTx, true); err != nil { + if _, err := r.Client.SendRawTransaction(testTx, true); err != nil { t.Fatalf("send transaction failed: %v", err) } @@ -219,7 +219,7 @@ func testJoinMempools(r *Harness, t *testing.T) { harnessSynced := make(chan struct{}) go func() { for { - poolHashes, err := r.Node.GetRawMempool() + poolHashes, err := r.Client.GetRawMempool() if err != nil { t.Fatalf("failed to retrieve harness mempool: %v", err) } @@ -262,7 +262,7 @@ func testJoinMempools(r *Harness, t *testing.T) { // Send the transaction to the local harness which will result in synced // mempools. - if _, err := harness.Node.SendRawTransaction(testTx, true); err != nil { + if _, err := harness.Client.SendRawTransaction(testTx, true); err != nil { t.Fatalf("send transaction failed: %v", err) } @@ -612,7 +612,7 @@ func TestHarness(t *testing.T) { // Current tip should be at a height of numMatureOutputs plus the // required number of blocks for coinbase maturity. - nodeInfo, err := mainHarness.Node.GetInfo() + nodeInfo, err := mainHarness.Client.GetInfo() if err != nil { t.Fatalf("unable to execute getinfo on node: %v", err) } diff --git a/integration/rpctest/utils.go b/integration/rpctest/utils.go index fc7d938dcfd..d4d76f2ee6e 100644 --- a/integration/rpctest/utils.go +++ b/integration/rpctest/utils.go @@ -49,7 +49,7 @@ func syncMempools(nodes []*Harness) error { retry: for !poolsMatch { - firstPool, err := nodes[0].Node.GetRawMempool() + firstPool, err := nodes[0].Client.GetRawMempool() if err != nil { return err } @@ -58,7 +58,7 @@ retry: // first node, then we're done. Otherwise, drop back to the top // of the loop and retry after a short wait period. for _, node := range nodes[1:] { - nodePool, err := node.Node.GetRawMempool() + nodePool, err := node.Client.GetRawMempool() if err != nil { return err } @@ -84,7 +84,7 @@ retry: var prevHash *chainhash.Hash var prevHeight int32 for _, node := range nodes { - blockHash, blockHeight, err := node.Node.GetBestBlock() + blockHash, blockHeight, err := node.Client.GetBestBlock() if err != nil { return err } @@ -108,24 +108,24 @@ retry: // therefore in the case of disconnects, "from" will attempt to reestablish a // connection to the "to" harness. func ConnectNode(from *Harness, to *Harness) error { - peerInfo, err := from.Node.GetPeerInfo() + peerInfo, err := from.Client.GetPeerInfo() if err != nil { return err } numPeers := len(peerInfo) targetAddr := to.node.config.listen - if err := from.Node.AddNode(targetAddr, rpcclient.ANAdd); err != nil { + if err := from.Client.AddNode(targetAddr, rpcclient.ANAdd); err != nil { return err } // Block until a new connection has been established. - peerInfo, err = from.Node.GetPeerInfo() + peerInfo, err = from.Client.GetPeerInfo() if err != nil { return err } for len(peerInfo) <= numPeers { - peerInfo, err = from.Node.GetPeerInfo() + peerInfo, err = from.Client.GetPeerInfo() if err != nil { return err } diff --git a/rpcclient/examples/bitcoincorehttpbulk/README.md b/rpcclient/examples/bitcoincorehttpbulk/README.md new file mode 100644 index 00000000000..ca900b6e797 --- /dev/null +++ b/rpcclient/examples/bitcoincorehttpbulk/README.md @@ -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. diff --git a/rpcclient/examples/bitcoincorehttpbulk/main.go b/rpcclient/examples/bitcoincorehttpbulk/main.go new file mode 100644 index 00000000000..3dce058da03 --- /dev/null +++ b/rpcclient/examples/bitcoincorehttpbulk/main.go @@ -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()) +} diff --git a/rpcclient/infrastructure.go b/rpcclient/infrastructure.go index 4f15ed14307..22b530807f4 100644 --- a/rpcclient/infrastructure.go +++ b/rpcclient/infrastructure.go @@ -163,6 +163,10 @@ type Client struct { // disconnected indicated whether or not the server is disconnected. disconnected bool + // whether or not to batch requests, false unless changed by Batch() + batch bool + batchList *list.List + // retryCount holds the number of times the client has tried to // reconnect to the RPC server. retryCount int64 @@ -220,8 +224,13 @@ func (c *Client) addRequest(jReq *jsonRequest) error { default: } - element := c.requestList.PushBack(jReq) - c.requestMap[jReq.id] = element + if !c.batch { + element := c.requestList.PushBack(jReq) + c.requestMap[jReq.id] = element + } else { + element := c.batchList.PushBack(jReq) + c.requestMap[jReq.id] = element + } return nil } @@ -289,6 +298,41 @@ func (c *Client) trackRegisteredNtfns(cmd interface{}) { } } +// FutureGetBulkResult waits for the responses promised by the future +// and returns them in a channel +type FutureGetBulkResult chan *response + +// Receive waits for the response promised by the future and returns an map +// of results by request id +func (r FutureGetBulkResult) Receive() (BulkResult, error) { + m := make(BulkResult) + res, err := receiveFuture(r) + if err != nil { + return nil, err + } + var arr []IndividualBulkResult + err = json.Unmarshal(res, &arr) + if err != nil { + return nil, err + } + + for _, results := range arr { + m[results.Id] = results + } + + return m, nil +} + +// IndividualBulkResult represents one result +// from a bulk json rpc api +type IndividualBulkResult struct { + Result interface{} `json:"result"` + Error *btcjson.RPCError `json:"error"` + Id uint64 `json:"id"` +} + +type BulkResult = map[uint64]IndividualBulkResult + // inMessage is the first type that an incoming message is unmarshaled // into. It supports both requests (for notification support) and // responses. The partially-unmarshaled message is a notification if @@ -741,7 +785,12 @@ func (c *Client) handleSendPostMessage(details *sendPostDetails) { // Try to unmarshal the response as a regular JSON-RPC response. var resp rawResponse - err = json.Unmarshal(respBytes, &resp) + var batchResponse json.RawMessage + if c.batch { + err = json.Unmarshal(respBytes, &batchResponse) + } else { + err = json.Unmarshal(respBytes, &resp) + } if err != nil { // When the response itself isn't a valid JSON-RPC response // return an error which includes the HTTP status code and raw @@ -751,8 +800,14 @@ func (c *Client) handleSendPostMessage(details *sendPostDetails) { jReq.responseChan <- &response{err: err} return } - - res, err := resp.result() + var res []byte + if c.batch { + // errors must be dealt with downstream since a whole request cannot + // "error out" other than through the status code error handled above + res, err = batchResponse, nil + } else { + res, err = resp.result() + } jReq.responseChan <- &response{result: res, err: err} } @@ -875,7 +930,13 @@ func (c *Client) sendRequest(jReq *jsonRequest) { // POST mode, the command is issued via an HTTP client. Otherwise, // the command is issued via the asynchronous websocket channels. if c.config.HTTPPostMode { - c.sendPost(jReq) + if c.batch { + if err := c.addRequest(jReq); err != nil { + log.Warn(err) + } + } else { + c.sendPost(jReq) + } return } @@ -905,6 +966,10 @@ func (c *Client) sendRequest(jReq *jsonRequest) { // future. It handles both websocket and HTTP POST mode depending on the // configuration of the client. func (c *Client) sendCmd(cmd interface{}) chan *response { + rpcVersion := btcjson.RpcVersion1 + if c.batch { + rpcVersion = btcjson.RpcVersion2 + } // Get the method associated with the command. method, err := btcjson.CmdMethod(cmd) if err != nil { @@ -913,7 +978,7 @@ func (c *Client) sendCmd(cmd interface{}) chan *response { // Marshal the command. id := c.NextID() - marshalledJSON, err := btcjson.MarshalCmd(id, cmd) + marshalledJSON, err := btcjson.MarshalCmd(rpcVersion, id, cmd) if err != nil { return newFutureError(err) } @@ -927,6 +992,7 @@ func (c *Client) sendCmd(cmd interface{}) chan *response { marshalledJSON: marshalledJSON, responseChan: responseChan, } + c.sendRequest(jReq) return responseChan @@ -1357,6 +1423,8 @@ func New(config *ConnConfig, ntfnHandlers *NotificationHandlers) (*Client, error httpClient: httpClient, requestMap: make(map[uint64]*list.Element), requestList: list.New(), + batch: false, + batchList: list.New(), ntfnHandlers: ntfnHandlers, ntfnState: newNotificationState(), sendChan: make(chan []byte, sendBufferSize), @@ -1397,6 +1465,24 @@ func New(config *ConnConfig, ntfnHandlers *NotificationHandlers) (*Client, error return client, nil } +// Batch is a factory that creates a client able to interact with the server using +// JSON-RPC 2.0. The client is capable of accepting an arbitrary number of requests +// and having the server process the all at the same time. It's compatible with both +// btcd and bitcoind +func NewBatch(config *ConnConfig) (*Client, error) { + if !config.HTTPPostMode { + return nil, errors.New("http post mode is required to use batch client") + } + // notification parameter is nil since notifications are not supported in POST mode. + client, err := New(config, nil) + if err != nil { + return nil, err + } + client.batch = true //copy the client with changed batch setting + client.start() + return client, nil +} + // Connect establishes the initial websocket connection. This is necessary when // a client was created after setting the DisableConnectOnNew field of the // Config struct. @@ -1534,3 +1620,69 @@ func (c *Client) BackendVersion() (BackendVersion, error) { return *c.backendVersion, nil } + +func (c *Client) sendAsync() FutureGetBulkResult { + // convert the array of marshalled json requests to a single request we can send + responseChan := make(chan *response, 1) + marshalledRequest := []byte("[") + for iter := c.batchList.Front(); iter != nil; iter = iter.Next() { + request := iter.Value.(*jsonRequest) + marshalledRequest = append(marshalledRequest, request.marshalledJSON...) + marshalledRequest = append(marshalledRequest, []byte(",")...) + } + if len(marshalledRequest) > 0 { + // removes the trailing comma to process the request individually + marshalledRequest = marshalledRequest[:len(marshalledRequest)-1] + } + marshalledRequest = append(marshalledRequest, []byte("]")...) + request := jsonRequest{ + id: c.NextID(), + method: "", + cmd: nil, + marshalledJSON: marshalledRequest, + responseChan: responseChan, + } + c.sendPost(&request) + return responseChan +} + +// Marshall's bulk requests and sends to the server +// creates a response channel to receive the response +func (c *Client) Send() error { + // if batchlist is empty, there's nothing to send + if c.batchList.Len() == 0{ + return nil + } + + // clear batchlist in case of an error + defer func() { + c.batchList = list.New() + }() + + result, err := c.sendAsync().Receive() + + if err != nil { + return err + } + + for iter := c.batchList.Front(); iter != nil; iter = iter.Next() { + var requestError error + request := iter.Value.(*jsonRequest) + individualResult := result[request.id] + fullResult, err := json.Marshal(individualResult.Result) + if err != nil { + return err + } + + if individualResult.Error != nil { + requestError = individualResult.Error + } + + result := response{ + result: fullResult, + err: requestError, + } + request.responseChan <- &result + } + return nil +} diff --git a/rpcclient/rawrequest.go b/rpcclient/rawrequest.go index dd16fd049ed..baead8bb908 100644 --- a/rpcclient/rawrequest.go +++ b/rpcclient/rawrequest.go @@ -44,7 +44,7 @@ func (c *Client) RawRequestAsync(method string, params []json.RawMessage) Future // than custom commands. id := c.NextID() rawRequest := &btcjson.Request{ - Jsonrpc: "1.0", + Jsonrpc: btcjson.RpcVersion1, ID: id, Method: method, Params: params, diff --git a/rpcserver.go b/rpcserver.go index f159f2397fa..8e351bd27c3 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -101,6 +101,9 @@ var ( // declared here to avoid the overhead of creating the slice on every // invocation for constant data. gbtCapabilities = []string{"proposal"} + + // JSON 2.0 batched request prefix + batchedRequestPrefix = []byte("[") ) // Errors @@ -3939,10 +3942,11 @@ func (s *rpcServer) checkAuth(r *http.Request, require bool) (bool, bool, error) // a known concrete command along with any error that might have happened while // parsing it. type parsedRPCCmd struct { - id interface{} - method string - cmd interface{} - err *btcjson.RPCError + jsonrpc btcjson.RPCVersion + id interface{} + method string + cmd interface{} + err *btcjson.RPCError } // standardCmdResult checks that a parsed command is a standard Bitcoin JSON-RPC @@ -3975,9 +3979,11 @@ handled: // is suitable for use in replies if the command is invalid in some way such as // an unregistered command or invalid parameters. func parseCmd(request *btcjson.Request) *parsedRPCCmd { - var parsedCmd parsedRPCCmd - parsedCmd.id = request.ID - parsedCmd.method = request.Method + parsedCmd := parsedRPCCmd{ + jsonrpc: request.Jsonrpc, + id: request.ID, + method: request.Method, + } cmd, err := btcjson.UnmarshalCmd(request) if err != nil { @@ -4004,7 +4010,7 @@ func parseCmd(request *btcjson.Request) *parsedRPCCmd { // createMarshalledReply returns a new marshalled JSON-RPC response given the // passed parameters. It will automatically convert errors that are not of // the type *btcjson.RPCError to the appropriate type as needed. -func createMarshalledReply(id, result interface{}, replyErr error) ([]byte, error) { +func createMarshalledReply(rpcVersion btcjson.RPCVersion, id interface{}, result interface{}, replyErr error) ([]byte, error) { var jsonErr *btcjson.RPCError if replyErr != nil { if jErr, ok := replyErr.(*btcjson.RPCError); ok { @@ -4014,7 +4020,67 @@ func createMarshalledReply(id, result interface{}, replyErr error) ([]byte, erro } } - return btcjson.MarshalResponse(id, result, jsonErr) + return btcjson.MarshalResponse(rpcVersion, id, result, jsonErr) +} + +// processRequest determines the incoming request type (single or batched), +// parses it and returns a marshalled response. +func (s *rpcServer) processRequest(request *btcjson.Request, isAdmin bool, closeChan <-chan struct{}) []byte { + var result interface{} + var err error + var jsonErr *btcjson.RPCError + + if !isAdmin { + if _, ok := rpcLimited[request.Method]; !ok { + jsonErr = internalRPCError("limited user not "+ + "authorized for this method", "") + } + } + + if jsonErr == nil { + if request.Method == "" || request.Params == nil { + jsonErr = &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidRequest.Code, + Message: "Invalid request: malformed", + } + msg, err := createMarshalledReply(request.Jsonrpc, request.ID, result, jsonErr) + if err != nil { + rpcsLog.Errorf("Failed to marshal reply: %v", err) + return nil + } + return msg + } + + // Valid requests with no ID (notifications) must not have a response + // per the JSON-RPC spec. + if request.ID == nil { + return nil + } + + // Attempt to parse the JSON-RPC request into a known + // concrete command. + parsedCmd := parseCmd(request) + if parsedCmd.err != nil { + jsonErr = parsedCmd.err + } else { + result, err = s.standardCmdResult(parsedCmd, + closeChan) + if err != nil { + jsonErr = &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidRequest.Code, + Message: "Invalid request: malformed", + } + } + } + } + + // Marshal the response. + msg, err := createMarshalledReply(request.Jsonrpc, request.ID, result, jsonErr) + if err != nil { + rpcsLog.Errorf("Failed to marshal reply: %v", err) + return nil + } + return msg } // jsonRPCRead handles reading and responding to RPC messages. @@ -4059,80 +4125,186 @@ func (s *rpcServer) jsonRPCRead(w http.ResponseWriter, r *http.Request, isAdmin conn.SetReadDeadline(timeZeroVal) // Attempt to parse the raw body into a JSON-RPC request. - var responseID interface{} - var jsonErr error - var result interface{} - var request btcjson.Request - if err := json.Unmarshal(body, &request); err != nil { - jsonErr = &btcjson.RPCError{ - Code: btcjson.ErrRPCParse.Code, - Message: "Failed to parse request: " + err.Error(), + // Setup a close notifier. Since the connection is hijacked, + // the CloseNotifer on the ResponseWriter is not available. + closeChan := make(chan struct{}, 1) + go func() { + _, err = conn.Read(make([]byte, 1)) + if err != nil { + close(closeChan) } + }() + + var results []json.RawMessage + var batchSize int + var batchedRequest bool + + // Determine request type + if bytes.HasPrefix(body, batchedRequestPrefix) { + batchedRequest = true } - if jsonErr == nil { - // The JSON-RPC 1.0 spec defines that notifications must have their "id" - // set to null and states that notifications do not have a response. - // - // A JSON-RPC 2.0 notification is a request with "json-rpc":"2.0", and - // without an "id" member. The specification states that notifications - // must not be responded to. JSON-RPC 2.0 permits the null value as a - // valid request id, therefore such requests are not notifications. - // - // Bitcoin Core serves requests with "id":null or even an absent "id", - // and responds to such requests with "id":null in the response. - // - // Btcd does not respond to any request without and "id" or "id":null, - // regardless the indicated JSON-RPC protocol version unless RPC quirks - // are enabled. With RPC quirks enabled, such requests will be responded - // to if the reqeust does not indicate JSON-RPC version. - // - // RPC quirks can be enabled by the user to avoid compatibility issues - // with software relying on Core's behavior. - if request.ID == nil && !(cfg.RPCQuirks && request.Jsonrpc == "") { - return + + // Process a 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) + } } - // The parse was at least successful enough to have an ID so - // set it for the response. - responseID = request.ID + 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) + } - // Setup a close notifier. Since the connection is hijacked, - // the CloseNotifer on the ResponseWriter is not available. - closeChan := make(chan struct{}, 1) - go func() { - _, err := conn.Read(make([]byte, 1)) + if resp != nil { + results = append(results, resp) + } + } + + // Process a batched request + if batchedRequest { + var batchedRequests []interface{} + var resp json.RawMessage + err = json.Unmarshal(body, &batchedRequests) + if err != nil { + jsonErr := &btcjson.RPCError{ + Code: btcjson.ErrRPCParse.Code, + Message: fmt.Sprintf("Failed to parse request: %v", + err), + } + resp, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr) if err != nil { - close(closeChan) + rpcsLog.Errorf("Failed to create reply: %v", err) } - }() - // Check if the user is limited and set error if method unauthorized - if !isAdmin { - if _, ok := rpcLimited[request.Method]; !ok { - jsonErr = &btcjson.RPCError{ - Code: btcjson.ErrRPCInvalidParams.Code, - Message: "limited user not authorized for this method", + if resp != nil { + results = append(results, resp) + } + } + + if err == nil { + // Response with an empty batch error if the batch size is zero + if len(batchedRequests) == 0 { + jsonErr := &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidRequest.Code, + Message: "Invalid request: empty batch", + } + resp, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr) + if err != nil { + rpcsLog.Errorf("Failed to marshal reply: %v", err) + } + + if resp != nil { + results = append(results, resp) + } + } + + // Process each batch entry individually + if len(batchedRequests) > 0 { + batchSize = len(batchedRequests) + + for _, entry := range batchedRequests { + var reqBytes []byte + reqBytes, err = json.Marshal(entry) + if err != nil { + jsonErr := &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidRequest.Code, + Message: fmt.Sprintf("Invalid request: %v", + err), + } + resp, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr) + if err != nil { + rpcsLog.Errorf("Failed to create reply: %v", err) + } + + if resp != nil { + results = append(results, resp) + } + continue + } + + var req btcjson.Request + err := json.Unmarshal(reqBytes, &req) + if err != nil { + jsonErr := &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidRequest.Code, + Message: fmt.Sprintf("Invalid request: %v", + err), + } + resp, err = btcjson.MarshalResponse("", nil, nil, jsonErr) + if err != nil { + rpcsLog.Errorf("Failed to create reply: %v", err) + } + + if resp != nil { + results = append(results, resp) + } + continue + } + + resp = s.processRequest(&req, isAdmin, closeChan) + if resp != nil { + results = append(results, resp) + } } } } + } - if jsonErr == nil { - // Attempt to parse the JSON-RPC request into a known concrete - // command. - parsedCmd := parseCmd(&request) - if parsedCmd.err != nil { - jsonErr = parsedCmd.err - } else { - result, jsonErr = s.standardCmdResult(parsedCmd, closeChan) + var msg = []byte{} + if batchedRequest && batchSize > 0 { + if len(results) > 0 { + // Form the batched response json + var buffer bytes.Buffer + buffer.WriteByte('[') + for idx, reply := range results { + if idx == len(results)-1 { + buffer.Write(reply) + buffer.WriteByte(']') + break + } + buffer.Write(reply) + buffer.WriteByte(',') } + msg = buffer.Bytes() } } - // Marshal the response. - msg, err := createMarshalledReply(responseID, result, jsonErr) - if err != nil { - rpcsLog.Errorf("Failed to marshal reply: %v", err) - return + if !batchedRequest || batchSize == 0 { + // Respond with the first results entry for single requests + if len(results) > 0 { + msg = results[0] + } } // Write the response. diff --git a/rpcwebsocket.go b/rpcwebsocket.go index 32e466d115a..356a897455a 100644 --- a/rpcwebsocket.go +++ b/rpcwebsocket.go @@ -695,7 +695,7 @@ func (*wsNotificationManager) notifyBlockConnected(clients map[chan struct{}]*ws // Notify interested websocket clients about the connected block. ntfn := btcjson.NewBlockConnectedNtfn(block.Hash().String(), block.Height(), block.MsgBlock().Header.Timestamp.Unix()) - marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) + marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn) if err != nil { rpcsLog.Errorf("Failed to marshal block connected notification: "+ "%v", err) @@ -719,7 +719,7 @@ func (*wsNotificationManager) notifyBlockDisconnected(clients map[chan struct{}] // Notify interested websocket clients about the disconnected block. ntfn := btcjson.NewBlockDisconnectedNtfn(block.Hash().String(), block.Height(), block.MsgBlock().Header.Timestamp.Unix()) - marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) + marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn) if err != nil { rpcsLog.Errorf("Failed to marshal block disconnected "+ "notification: %v", err) @@ -765,7 +765,7 @@ func (m *wsNotificationManager) notifyFilteredBlockConnected(clients map[chan st ntfn.SubscribedTxs = subscribedTxs[quitChan] // Marshal and queue notification. - marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) + marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn) if err != nil { rpcsLog.Errorf("Failed to marshal filtered block "+ "connected notification: %v", err) @@ -796,7 +796,7 @@ func (*wsNotificationManager) notifyFilteredBlockDisconnected(clients map[chan s } ntfn := btcjson.NewFilteredBlockDisconnectedNtfn(block.Height(), hex.EncodeToString(w.Bytes())) - marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) + marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn) if err != nil { rpcsLog.Errorf("Failed to marshal filtered block disconnected "+ "notification: %v", err) @@ -831,7 +831,7 @@ func (m *wsNotificationManager) notifyForNewTx(clients map[chan struct{}]*wsClie } ntfn := btcjson.NewTxAcceptedNtfn(txHashStr, btcutil.Amount(amount).ToBTC()) - marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) + marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn) if err != nil { rpcsLog.Errorf("Failed to marshal tx notification: %s", err.Error()) return @@ -854,7 +854,7 @@ func (m *wsNotificationManager) notifyForNewTx(clients map[chan struct{}]*wsClie } verboseNtfn = btcjson.NewTxAcceptedVerboseNtfn(*rawTx) - marshalledJSONVerbose, err = btcjson.MarshalCmd(nil, + marshalledJSONVerbose, err = btcjson.MarshalCmd(btcjson.RpcVersion1, nil, verboseNtfn) if err != nil { rpcsLog.Errorf("Failed to marshal verbose tx "+ @@ -980,7 +980,7 @@ func blockDetails(block *btcutil.Block, txIndex int) *btcjson.BlockDetails { func newRedeemingTxNotification(txHex string, index int, block *btcutil.Block) ([]byte, error) { // Create and marshal the notification. ntfn := btcjson.NewRedeemingTxNtfn(txHex, blockDetails(block, index)) - return btcjson.MarshalCmd(nil, ntfn) + return btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn) } // notifyForTxOuts examines each transaction output, notifying interested @@ -1016,7 +1016,7 @@ func (m *wsNotificationManager) notifyForTxOuts(ops map[wire.OutPoint]map[chan s ntfn := btcjson.NewRecvTxNtfn(txHex, blockDetails(block, tx.Index())) - marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) + marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn) if err != nil { rpcsLog.Errorf("Failed to marshal processedtx notification: %v", err) continue @@ -1047,7 +1047,7 @@ func (m *wsNotificationManager) notifyRelevantTxAccepted(tx *btcutil.Tx, if len(clientsToNotify) != 0 { n := btcjson.NewRelevantTxAcceptedNtfn(txHexString(tx.MsgTx())) - marshalled, err := btcjson.MarshalCmd(nil, n) + marshalled, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, n) if err != nil { rpcsLog.Errorf("Failed to marshal notification: %v", err) return @@ -1323,153 +1323,435 @@ out: break out } - var request btcjson.Request - err = json.Unmarshal(msg, &request) - if err != nil { - if !c.authenticated { - break out - } + var batchedRequest bool - jsonErr := &btcjson.RPCError{ - Code: btcjson.ErrRPCParse.Code, - Message: "Failed to parse request: " + err.Error(), - } - reply, err := createMarshalledReply(nil, nil, jsonErr) - if err != nil { - rpcsLog.Errorf("Failed to marshal parse failure "+ - "reply: %v", err) - continue - } - c.SendMessage(reply, nil) - continue + // Determine request type + if bytes.HasPrefix(msg, batchedRequestPrefix) { + batchedRequest = true } - // 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 - } + if !batchedRequest { + var req btcjson.Request + var reply json.RawMessage + err = json.Unmarshal(msg, &req) + if err != nil { + // only process requests from authenticated clients + if !c.authenticated { + break out + } - cmd := parseCmd(&request) - if cmd.err != nil { - if !c.authenticated { - break out + jsonErr := &btcjson.RPCError{ + Code: btcjson.ErrRPCParse.Code, + Message: "Failed to parse request: " + err.Error(), + } + reply, err = createMarshalledReply(btcjson.RpcVersion1, nil, nil, jsonErr) + if err != nil { + rpcsLog.Errorf("Failed to marshal reply: %v", err) + continue + } + c.SendMessage(reply, nil) + continue } - reply, err := createMarshalledReply(cmd.id, nil, cmd.err) - if err != nil { - rpcsLog.Errorf("Failed to marshal parse failure "+ - "reply: %v", err) + 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 } - 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 + + // 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 } - 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()) + 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 } - 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", + 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(request.ID, nil, jsonErr) + reply, err = createMarshalledReply(cmd.jsonrpc, cmd.id, nil, nil) if err != nil { - rpcsLog.Errorf("Failed to marshal parse failure "+ - "reply: %v", err) + rpcsLog.Errorf("Failed to marshal authenticate reply: "+ + "%v", err.Error()) continue } c.SendMessage(reply, nil) continue } + + // Check if the client is using limited RPC credentials and + // error when not authorized to call the supplied RPC. + if !c.isAdmin { + if _, ok := rpcLimited[req.Method]; !ok { + jsonErr := &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParams.Code, + Message: "limited user not authorized for this method", + } + // Marshal and send response. + reply, err = createMarshalledReply("", req.ID, nil, jsonErr) + if err != nil { + rpcsLog.Errorf("Failed to marshal parse failure "+ + "reply: %v", err) + continue + } + c.SendMessage(reply, nil) + continue + } + } + + // Asynchronously handle the request. A semaphore is used to + // limit the number of concurrent requests currently being + // serviced. If the semaphore can not be acquired, simply wait + // until a request finished before reading the next RPC request + // from the websocket client. + // + // This could be a little fancier by timing out and erroring + // when it takes too long to service the request, but if that is + // done, the read of the next request should not be blocked by + // this semaphore, otherwise the next request will be read and + // will probably sit here for another few seconds before timing + // out as well. This will cause the total timeout duration for + // later requests to be much longer than the check here would + // imply. + // + // If a timeout is added, the semaphore acquiring should be + // moved inside of the new goroutine with a select statement + // that also reads a time.After channel. This will unblock the + // read of the next request from the websocket client and allow + // many requests to be waited on concurrently. + c.serviceRequestSem.acquire() + go func() { + c.serviceRequest(cmd) + c.serviceRequestSem.release() + }() } - // Asynchronously handle the request. A semaphore is used to - // limit the number of concurrent requests currently being - // serviced. If the semaphore can not be acquired, simply wait - // until a request finished before reading the next RPC request - // from the websocket client. - // - // This could be a little fancier by timing out and erroring - // when it takes too long to service the request, but if that is - // done, the read of the next request should not be blocked by - // this semaphore, otherwise the next request will be read and - // will probably sit here for another few seconds before timing - // out as well. This will cause the total timeout duration for - // later requests to be much longer than the check here would - // imply. - // - // If a timeout is added, the semaphore acquiring should be - // moved inside of the new goroutine with a select statement - // that also reads a time.After channel. This will unblock the - // read of the next request from the websocket client and allow - // many requests to be waited on concurrently. - c.serviceRequestSem.acquire() - go func() { - c.serviceRequest(cmd) + // Process a batched request + if batchedRequest { + var batchedRequests []interface{} + var results []json.RawMessage + var batchSize int + var reply json.RawMessage + c.serviceRequestSem.acquire() + err = json.Unmarshal(msg, &batchedRequests) + if err != nil { + // Only process requests from authenticated clients + if !c.authenticated { + break out + } + + jsonErr := &btcjson.RPCError{ + Code: btcjson.ErrRPCParse.Code, + Message: fmt.Sprintf("Failed to parse request: %v", + err), + } + reply, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr) + if err != nil { + rpcsLog.Errorf("Failed to create reply: %v", err) + } + + if reply != nil { + results = append(results, reply) + } + } + + if err == nil { + // Response with an empty batch error if the batch size is zero + if len(batchedRequests) == 0 { + if !c.authenticated { + break out + } + + jsonErr := &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidRequest.Code, + Message: "Invalid request: empty batch", + } + reply, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr) + if err != nil { + rpcsLog.Errorf("Failed to marshal reply: %v", err) + } + + if reply != nil { + results = append(results, reply) + } + } + + // Process each batch entry individually + if len(batchedRequests) > 0 { + batchSize = len(batchedRequests) + for _, entry := range batchedRequests { + var reqBytes []byte + reqBytes, err = json.Marshal(entry) + if err != nil { + // Only process requests from authenticated clients + if !c.authenticated { + break out + } + + jsonErr := &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidRequest.Code, + Message: fmt.Sprintf("Invalid request: %v", + err), + } + reply, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr) + if err != nil { + rpcsLog.Errorf("Failed to create reply: %v", err) + continue + } + + if reply != nil { + results = append(results, reply) + } + continue + } + + var req btcjson.Request + err := json.Unmarshal(reqBytes, &req) + if err != nil { + // Only process requests from authenticated clients + if !c.authenticated { + break out + } + + jsonErr := &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidRequest.Code, + Message: fmt.Sprintf("Invalid request: %v", + err), + } + reply, err = btcjson.MarshalResponse(btcjson.RpcVersion2, nil, nil, jsonErr) + if err != nil { + rpcsLog.Errorf("Failed to create reply: %v", err) + continue + } + + if reply != nil { + results = append(results, reply) + } + continue + } + + if req.Method == "" || req.Params == nil { + jsonErr := &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidRequest.Code, + Message: "Invalid request: malformed", + } + reply, err := createMarshalledReply(req.Jsonrpc, req.ID, nil, jsonErr) + if err != nil { + rpcsLog.Errorf("Failed to marshal reply: %v", err) + continue + } + + if reply != nil { + results = append(results, reply) + } + continue + } + + // Valid requests with no ID (notifications) must not have a response + // per the JSON-RPC spec. + if req.ID == nil { + if !c.authenticated { + break out + } + continue + } + + cmd := parseCmd(&req) + if cmd.err != nil { + // Only process requests from authenticated clients + if !c.authenticated { + break out + } + + reply, err = createMarshalledReply(cmd.jsonrpc, cmd.id, nil, cmd.err) + if err != nil { + rpcsLog.Errorf("Failed to marshal reply: %v", err) + continue + } + + if reply != nil { + results = append(results, reply) + } + continue + } + + rpcsLog.Debugf("Received command <%s> from %s", cmd.method, c.addr) + + // Check auth. The client is immediately disconnected if the + // first request of an unauthentiated websocket client is not + // the authenticate request, an authenticate request is received + // when the client is already authenticated, or incorrect + // authentication credentials are provided in the request. + switch authCmd, ok := cmd.cmd.(*btcjson.AuthenticateCmd); { + case c.authenticated && ok: + rpcsLog.Warnf("Websocket client %s is already authenticated", + c.addr) + break out + case !c.authenticated && !ok: + rpcsLog.Warnf("Unauthenticated websocket message " + + "received") + break out + case !c.authenticated: + // Check credentials. + login := authCmd.Username + ":" + authCmd.Passphrase + auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login)) + authSha := sha256.Sum256([]byte(auth)) + cmp := subtle.ConstantTimeCompare(authSha[:], c.server.authsha[:]) + limitcmp := subtle.ConstantTimeCompare(authSha[:], c.server.limitauthsha[:]) + if cmp != 1 && limitcmp != 1 { + rpcsLog.Warnf("Auth failure.") + break out + } + + c.authenticated = true + c.isAdmin = cmp == 1 + + // Marshal and send response. + reply, err = createMarshalledReply(cmd.jsonrpc, cmd.id, nil, nil) + if err != nil { + rpcsLog.Errorf("Failed to marshal authenticate reply: "+ + "%v", err.Error()) + continue + } + + if reply != nil { + results = append(results, reply) + } + continue + } + + // Check if the client is using limited RPC credentials and + // error when not authorized to call the supplied RPC. + if !c.isAdmin { + if _, ok := rpcLimited[req.Method]; !ok { + jsonErr := &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParams.Code, + Message: "limited user not authorized for this method", + } + // Marshal and send response. + reply, err = createMarshalledReply(req.Jsonrpc, req.ID, nil, jsonErr) + if err != nil { + rpcsLog.Errorf("Failed to marshal parse failure "+ + "reply: %v", err) + continue + } + + if reply != nil { + results = append(results, reply) + } + continue + } + } + + // Lookup the websocket extension for the command, if it doesn't + // exist fallback to handling the command as a standard command. + var resp interface{} + wsHandler, ok := wsHandlers[cmd.method] + if ok { + resp, err = wsHandler(c, cmd.cmd) + } else { + resp, err = c.server.standardCmdResult(cmd, nil) + } + + // Marshal request output. + reply, err := createMarshalledReply(cmd.jsonrpc, cmd.id, resp, err) + if err != nil { + rpcsLog.Errorf("Failed to marshal reply for <%s> "+ + "command: %v", cmd.method, err) + return + } + + if reply != nil { + results = append(results, reply) + } + } + } + } + + // generate reply + var payload = []byte{} + if batchedRequest && batchSize > 0 { + if len(results) > 0 { + // Form the batched response json + var buffer bytes.Buffer + buffer.WriteByte('[') + for idx, marshalledReply := range results { + if idx == len(results)-1 { + buffer.Write(marshalledReply) + buffer.WriteByte(']') + break + } + buffer.Write(marshalledReply) + buffer.WriteByte(',') + } + payload = buffer.Bytes() + } + } + + if !batchedRequest || batchSize == 0 { + // Respond with the first results entry for single requests + if len(results) > 0 { + payload = results[0] + } + } + + c.SendMessage(payload, nil) c.serviceRequestSem.release() - }() + } } // Ensure the connection is closed. @@ -1495,7 +1777,7 @@ func (c *wsClient) serviceRequest(r *parsedRPCCmd) { } else { result, err = c.server.standardCmdResult(r, nil) } - reply, err := createMarshalledReply(r.id, result, err) + reply, err := createMarshalledReply(r.jsonrpc, r.id, result, err) if err != nil { rpcsLog.Errorf("Failed to marshal reply for <%s> "+ "command: %v", r.method, err) @@ -2125,7 +2407,7 @@ func rescanBlock(wsc *wsClient, lookups *rescanKeys, blk *btcutil.Block) { ntfn := btcjson.NewRecvTxNtfn(txHex, blockDetails(blk, tx.Index())) - marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) + marshalledJSON, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, ntfn) if err != nil { rpcsLog.Errorf("Failed to marshal recvtx notification: %v", err) return @@ -2492,7 +2774,7 @@ fetchRange: hashList[i].String(), blk.Height(), blk.MsgBlock().Header.Timestamp.Unix(), ) - mn, err := btcjson.MarshalCmd(nil, n) + mn, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, n) if err != nil { rpcsLog.Errorf("Failed to marshal rescan "+ "progress notification: %v", err) @@ -2637,7 +2919,7 @@ func handleRescan(wsc *wsClient, icmd interface{}) (interface{}, error) { lastBlockHash.String(), lastBlock.Height(), lastBlock.MsgBlock().Header.Timestamp.Unix(), ) - if mn, err := btcjson.MarshalCmd(nil, n); err != nil { + if mn, err := btcjson.MarshalCmd(btcjson.RpcVersion1, nil, n); err != nil { rpcsLog.Errorf("Failed to marshal rescan finished "+ "notification: %v", err) } else {