From b1a71d5f83d906feb2c96d210e881a74b7a91c60 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Wed, 9 Apr 2014 11:07:09 -0500 Subject: [PATCH] Delay JSON unmarshaling until needed. This change takes advantage of the RawMessage type in the encoding/json package to defer unmarshaling of all JSON-RPC values until absolutely necessary. This is particularly important for request passthrough when btcwallet must ask btcd to handle a chain request for a wallet client. In the previous code, during the marshal and unmarshal dance to set the original client's request id in the btcd response, large JSON numbers were being mangled to use (scientific) E notation even when they could be represented as a integer without any loss of precision. --- rpc.go | 103 ++++++++++--------------- rpcclient.go | 209 ++++++++++++++++++++++++++------------------------- rpcserver.go | 64 +++++++++------- sockets.go | 18 +++-- 4 files changed, 197 insertions(+), 197 deletions(-) diff --git a/rpc.go b/rpc.go index a8c350b..bfe5f65 100644 --- a/rpc.go +++ b/rpc.go @@ -17,14 +17,41 @@ package main import ( + "encoding/json" "github.com/conformal/btcjson" ) -// RPCResponse is an interface type covering both server -// (frontend <-> btcwallet) and client (btcwallet <-> btcd) responses. -type RPCResponse interface { - Result() interface{} - Error() *btcjson.Error +// RawRPCResponse is a response to a JSON-RPC request with delayed +// unmarshaling. +type RawRPCResponse struct { + Id *uint64 + Result *json.RawMessage `json:"result"` + Error *json.RawMessage `json:"error"` +} + +func (r *RawRPCResponse) FinishUnmarshal(v interface{}) (interface{}, *btcjson.Error) { + // JSON-RPC spec makes this handling easier-ish because both result and + // error cannot be non-nil. + var jsonErr *btcjson.Error + if r.Error != nil { + if err := json.Unmarshal([]byte(*r.Error), &jsonErr); err != nil { + return nil, &btcjson.Error{ + Code: btcjson.ErrParse.Code, + Message: err.Error(), + } + } + return nil, jsonErr + } + if r.Result != nil { + if err := json.Unmarshal([]byte(*r.Result), &v); err != nil { + return nil, &btcjson.Error{ + Code: btcjson.ErrParse.Code, + Message: err.Error(), + } + } + return v, nil + } + return nil, nil } // ClientRequest is a type holding a bitcoin client's request and @@ -32,7 +59,7 @@ type RPCResponse interface { type ClientRequest struct { ws bool request btcjson.Cmd - response chan RPCResponse + response chan RawRPCResponse } // NewClientRequest creates a new ClientRequest from a btcjson.Cmd. @@ -40,78 +67,28 @@ func NewClientRequest(request btcjson.Cmd, ws bool) *ClientRequest { return &ClientRequest{ ws: ws, request: request, - response: make(chan RPCResponse), + response: make(chan RawRPCResponse), } } // Handle sends a client request to the RPC gateway for processing, // and returns the result when handling is finished. -func (r *ClientRequest) Handle() (interface{}, *btcjson.Error) { +func (r *ClientRequest) Handle() RawRPCResponse { clientRequests <- r - resp := <-r.response - return resp.Result(), resp.Error() -} - -// ClientResponse holds a result and error returned from handling a -// client's request. -type ClientResponse struct { - result interface{} - err *btcjson.Error -} - -// Result returns the result of a response to a client. -func (r *ClientResponse) Result() interface{} { - return r.result -} - -// Error returns the error of a response to a client, or nil if -// there is no error. -func (r *ClientResponse) Error() *btcjson.Error { - return r.err + return <-r.response } // ServerRequest is a type responsible for handling requests to a bitcoin // server and providing a method to access the response. type ServerRequest struct { request btcjson.Cmd - result interface{} - response chan RPCResponse + response chan RawRPCResponse } -// NewServerRequest creates a new ServerRequest from a btcjson.Cmd. request -// may be nil to create a new var for the result (with types determined by -// the unmarshaling rules described in the json package), or set to a var -// with an expected type (i.e. *btcjson.BlockResult) to directly unmarshal -// the response's result into a convenient type. -func NewServerRequest(request btcjson.Cmd, result interface{}) *ServerRequest { +// NewServerRequest creates a new ServerRequest from a btcjson.Cmd. +func NewServerRequest(request btcjson.Cmd) *ServerRequest { return &ServerRequest{ request: request, - result: result, - response: make(chan RPCResponse, 1), + response: make(chan RawRPCResponse, 1), } } - -// ServerResponse holds a response's result and error returned from sending a -// ServerRequest. -type ServerResponse struct { - // Result will be set to a concrete type (i.e. *btcjson.BlockResult) - // and may be type asserted to that type if a non-nil result was used - // to create the originating ServerRequest. Otherwise, Result will be - // set to new memory allocated by json.Unmarshal, and the type rules - // for unmarshaling described in the json package should be followed - // when type asserting Result. - result interface{} - - // Err points to an unmarshaled error, or nil if result is valid. - err *btcjson.Error -} - -// Result returns the result of a server's RPC response. -func (r *ServerResponse) Result() interface{} { - return r.result -} - -// Result returns the error of a server's RPC response. -func (r *ServerResponse) Error() *btcjson.Error { - return r.err -} diff --git a/rpcclient.go b/rpcclient.go index e4f1944..eae9327 100644 --- a/rpcclient.go +++ b/rpcclient.go @@ -37,7 +37,7 @@ type ServerConn interface { // SendRequest sends a bitcoin RPC request, returning a channel to // read the reply. A channel is used so both synchronous and // asynchronous RPC can be supported. - SendRequest(request *ServerRequest) chan RPCResponse + SendRequest(request *ServerRequest) chan RawRPCResponse } // ErrBtcdDisconnected describes an error where an operation cannot @@ -48,6 +48,9 @@ var ErrBtcdDisconnected = btcjson.Error{ Message: "btcd disconnected", } +// ErrBtcdDisconnectedRaw is the raw JSON encoding of ErrBtcdDisconnected. +var ErrBtcdDisconnectedRaw = json.RawMessage(`{"code":-1,"message":"btcd disconnected"}`) + // BtcdRPCConn is a type managing a client connection to a btcd RPC server // over websockets. type BtcdRPCConn struct { @@ -72,23 +75,20 @@ func NewBtcdRPCConn(ws *websocket.Conn) *BtcdRPCConn { // SendRequest sends an RPC request and returns a channel to read the response's // result and error. Part of the RPCConn interface. -func (btcd *BtcdRPCConn) SendRequest(request *ServerRequest) chan RPCResponse { +func (btcd *BtcdRPCConn) SendRequest(request *ServerRequest) chan RawRPCResponse { select { case <-btcd.closed: // The connection has closed, so instead of adding and sending // a request, return a channel that just replies with the // error for a disconnected btcd. - responseChan := make(chan RPCResponse, 1) - response := &ServerResponse{ - err: &ErrBtcdDisconnected, - } - responseChan <- response + responseChan := make(chan RawRPCResponse, 1) + responseChan <- RawRPCResponse{Error: &ErrBtcdDisconnectedRaw} return responseChan default: addRequest := &AddRPCRequest{ Request: request, - ResponseChan: make(chan chan RPCResponse, 1), + ResponseChan: make(chan chan RawRPCResponse, 1), } btcd.addRequest <- addRequest return <-addRequest.ResponseChan @@ -124,7 +124,7 @@ func (btcd *BtcdRPCConn) Close() { // being manaaged by a btcd RPC connection. type AddRPCRequest struct { Request *ServerRequest - ResponseChan chan chan RPCResponse + ResponseChan chan chan RawRPCResponse } // send performs the actual send of the marshaled request over the btcd @@ -136,17 +136,11 @@ func (btcd *BtcdRPCConn) send(rpcrequest *ServerRequest) error { return websocket.Message.Send(btcd.ws, mrequest) } -type receivedResponse struct { - id uint64 - raw string - reply *btcjson.Reply -} - // Start starts the goroutines required to send RPC requests and listen for // replies. func (btcd *BtcdRPCConn) Start() { done := btcd.closed - responses := make(chan *receivedResponse) + responses := make(chan RawRPCResponse) // Maintain a map of JSON IDs to RPCRequests currently being waited on. go func() { @@ -167,49 +161,61 @@ func (btcd *BtcdRPCConn) Start() { addrequest.ResponseChan <- rpcrequest.response - case recvResponse, ok := <-responses: + case rawResponse, ok := <-responses: if !ok { responses = nil close(done) break } - rpcrequest, ok := m[recvResponse.id] + rpcrequest, ok := m[*rawResponse.Id] if !ok { log.Warnf("Received unexpected btcd response") continue } - delete(m, recvResponse.id) + delete(m, *rawResponse.Id) - // If no result var was set, create and send - // send the response unmarshaled by the json - // package. - if rpcrequest.result == nil { + rpcrequest.response <- rawResponse + + /* + //rpcrequest.result + var jsonErr *btcjson.Error + if rawResponse.result != nil { + } + err := json.Unmarshal([]byte(*rawResponse.result), &result) + err := json.Unmarshal([]byte(*rawResponse.error), &error) + + rawResult := recvResponse + rawError := recvResponse response := &ServerResponse{ - result: recvResponse.reply.Result, - err: recvResponse.reply.Error, + result: r.Result, + err: jsonErr } rpcrequest.response <- response - continue - } + */ - // A return var was set, so unmarshal again - // into the var before sending the response. - r := &btcjson.Reply{ - Result: rpcrequest.result, - } - json.Unmarshal([]byte(recvResponse.raw), &r) - response := &ServerResponse{ - result: r.Result, - err: r.Error, - } - rpcrequest.response <- response + /* + // If no result var was set, create and send + // send the response unmarshaled by the json + // package. + + + if rpcrequest.result == nil { + response := &ServerResponse{ + result: recvResponse.reply.Result, + err: recvResponse.reply.Error, + } + rpcrequest.response <- response + continue + } + + // A return var was set, so unmarshal again + // into the var before sending the response. + */ case <-done: - response := &ServerResponse{ - err: &ErrBtcdDisconnected, - } + resp := RawRPCResponse{Error: &ErrBtcdDisconnectedRaw} for _, request := range m { - request.response <- response + request.response <- resp } return } @@ -256,28 +262,18 @@ func (btcd *BtcdRPCConn) Start() { }() } -// unmarshalResponse attempts to unmarshal a marshaled JSON-RPC -// response. -func unmarshalResponse(s string) (*receivedResponse, error) { - var r btcjson.Reply +// unmarshalResponse attempts to unmarshal a marshaled JSON-RPC response. +func unmarshalResponse(s string) (RawRPCResponse, error) { + var r RawRPCResponse if err := json.Unmarshal([]byte(s), &r); err != nil { - return nil, err + return r, err } // Check for a valid ID. if r.Id == nil { - return nil, errors.New("id is nil") + return r, errors.New("id is null") } - fid, ok := (*r.Id).(float64) - if !ok { - return nil, errors.New("id is not a number") - } - response := &receivedResponse{ - id: uint64(fid), - raw: s, - reply: &r, - } - return response, nil + return r, nil } // unmarshalNotification attempts to unmarshal a marshaled JSON-RPC @@ -307,61 +303,67 @@ type GetBestBlockResult struct { // in the main chain. func GetBestBlock(rpc ServerConn) (*GetBestBlockResult, *btcjson.Error) { cmd := btcws.NewGetBestBlockCmd(<-NewJSONID) - request := NewServerRequest(cmd, new(GetBestBlockResult)) - response := <-rpc.SendRequest(request) - if response.Error() != nil { - return nil, response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + + var resultData GetBestBlockResult + _, jsonErr := response.FinishUnmarshal(&resultData) + if jsonErr != nil { + return nil, jsonErr } - return response.Result().(*GetBestBlockResult), nil + return &resultData, nil } // GetBlock requests details about a block with the given hash. func GetBlock(rpc ServerConn, blockHash string) (*btcjson.BlockResult, *btcjson.Error) { // NewGetBlockCmd cannot fail with no optargs, so omit the check. cmd, _ := btcjson.NewGetBlockCmd(<-NewJSONID, blockHash) - request := NewServerRequest(cmd, new(btcjson.BlockResult)) - response := <-rpc.SendRequest(request) - if response.Error() != nil { - return nil, response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + + var resultData btcjson.BlockResult + _, jsonErr := response.FinishUnmarshal(&resultData) + if jsonErr != nil { + return nil, jsonErr } - return response.Result().(*btcjson.BlockResult), nil + return &resultData, nil } // GetCurrentNet requests the network a bitcoin RPC server is running on. func GetCurrentNet(rpc ServerConn) (btcwire.BitcoinNet, *btcjson.Error) { cmd := btcws.NewGetCurrentNetCmd(<-NewJSONID) - request := NewServerRequest(cmd, nil) - response := <-rpc.SendRequest(request) - if response.Error() != nil { - return 0, response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + + var resultData uint32 + _, jsonErr := response.FinishUnmarshal(&resultData) + if jsonErr != nil { + return 0, jsonErr } - return btcwire.BitcoinNet(uint32(response.Result().(float64))), nil + return btcwire.BitcoinNet(resultData), nil } // NotifyBlocks requests blockconnected and blockdisconnected notifications. func NotifyBlocks(rpc ServerConn) *btcjson.Error { cmd := btcws.NewNotifyBlocksCmd(<-NewJSONID) - request := NewServerRequest(cmd, nil) - response := <-rpc.SendRequest(request) - return response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + _, jsonErr := response.FinishUnmarshal(nil) + return jsonErr } // NotifyNewTXs requests notifications for new transactions that spend // to any of the addresses in addrs. func NotifyNewTXs(rpc ServerConn, addrs []string) *btcjson.Error { cmd := btcws.NewNotifyNewTXsCmd(<-NewJSONID, addrs) - request := NewServerRequest(cmd, nil) - response := <-rpc.SendRequest(request) - return response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + _, jsonErr := response.FinishUnmarshal(nil) + return jsonErr } // NotifySpent requests notifications for when a transaction is processed which // spends op. func NotifySpent(rpc ServerConn, op *btcwire.OutPoint) *btcjson.Error { cmd := btcws.NewNotifySpentCmd(<-NewJSONID, op) - request := NewServerRequest(cmd, nil) - response := <-rpc.SendRequest(request) - return response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + _, jsonErr := response.FinishUnmarshal(nil) + return jsonErr } // Rescan requests a blockchain rescan for transactions to any number of @@ -371,21 +373,23 @@ func Rescan(rpc ServerConn, beginBlock int32, addrs []string, // NewRescanCmd cannot fail with no optargs, so omit the check. cmd, _ := btcws.NewRescanCmd(<-NewJSONID, beginBlock, addrs, outpoints) - request := NewServerRequest(cmd, nil) - response := <-rpc.SendRequest(request) - return response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + _, jsonErr := response.FinishUnmarshal(nil) + return jsonErr } // SendRawTransaction sends a hex-encoded transaction for relay. func SendRawTransaction(rpc ServerConn, hextx string) (txid string, error *btcjson.Error) { // NewSendRawTransactionCmd cannot fail, so omit the check. cmd, _ := btcjson.NewSendRawTransactionCmd(<-NewJSONID, hextx) - request := NewServerRequest(cmd, new(string)) - response := <-rpc.SendRequest(request) - if response.Error() != nil { - return "", response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + + var resultData string + _, jsonErr := response.FinishUnmarshal(&resultData) + if jsonErr != nil { + return "", jsonErr } - return *response.Result().(*string), nil + return resultData, nil } // GetRawTransaction sends the non-verbose version of a getrawtransaction @@ -394,13 +398,14 @@ func SendRawTransaction(rpc ServerConn, hextx string) (txid string, error *btcjs func GetRawTransaction(rpc ServerConn, txsha *btcwire.ShaHash) (*btcutil.Tx, *btcjson.Error) { // NewGetRawTransactionCmd cannot fail with no optargs. cmd, _ := btcjson.NewGetRawTransactionCmd(<-NewJSONID, txsha.String()) - request := NewServerRequest(cmd, new(string)) - response := <-rpc.SendRequest(request) - if response.Error() != nil { - return nil, response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + + var resultData string + _, jsonErr := response.FinishUnmarshal(&resultData) + if jsonErr != nil { + return nil, jsonErr } - hextx := *response.Result().(*string) - serializedTx, err := hex.DecodeString(hextx) + serializedTx, err := hex.DecodeString(resultData) if err != nil { return nil, &btcjson.ErrDecodeHexString } @@ -416,10 +421,12 @@ func GetRawTransaction(rpc ServerConn, txsha *btcwire.ShaHash) (*btcutil.Tx, *bt func VerboseGetRawTransaction(rpc ServerConn, txsha *btcwire.ShaHash) (*btcjson.TxRawResult, *btcjson.Error) { // NewGetRawTransactionCmd cannot fail with a single optarg. cmd, _ := btcjson.NewGetRawTransactionCmd(<-NewJSONID, txsha.String(), 1) - request := NewServerRequest(cmd, new(btcjson.TxRawResult)) - response := <-rpc.SendRequest(request) - if response.Error() != nil { - return nil, response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + + var resultData btcjson.TxRawResult + _, jsonErr := response.FinishUnmarshal(&resultData) + if jsonErr != nil { + return nil, jsonErr } - return response.Result().(*btcjson.TxRawResult), nil + return &resultData, nil } diff --git a/rpcserver.go b/rpcserver.go index 306a75c..aa44071 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -20,6 +20,7 @@ import ( "bytes" "encoding/base64" "encoding/hex" + "encoding/json" "github.com/conformal/btcec" "github.com/conformal/btcjson" "github.com/conformal/btcscript" @@ -118,6 +119,9 @@ var ErrServerBusy = btcjson.Error{ Message: "Server busy", } +// ErrServerBusyRaw is the raw JSON encoding of ErrServerBusy. +var ErrServerBusyRaw = json.RawMessage(`{"code":-32000,"message":"Server busy"}`) + // RPCGateway is the common entry point for all client RPC requests and // server notifications. If a request needs to be handled by btcwallet, // it is sent to WalletRequestProcessor's request queue, or dropped if the @@ -149,16 +153,15 @@ func RPCGateway() { case requestQueue <- r: default: // Server busy with too many requests. - resp := ClientResponse{ - err: &ErrServerBusy, + resp := RawRPCResponse{ + Error: &ErrServerBusyRaw, } - r.response <- &resp + r.response <- resp } } else { r.request.SetId(<-NewJSONID) request := &ServerRequest{ request: r.request, - result: nil, response: r.response, } CurrentServerConn().SendRequest(request) @@ -197,9 +200,16 @@ func WalletRequestProcessor() { result, jsonErr := f(r.request) AcctMgr.Release() - r.response <- &ClientResponse{ - result: result, - err: jsonErr, + if jsonErr != nil { + b, _ := json.Marshal(jsonErr) + r.response <- RawRPCResponse{ + Error: (*json.RawMessage)(&b), + } + } else { + b, _ := json.Marshal(result) + r.response <- RawRPCResponse{ + Result: (*json.RawMessage)(&b), + } } case n := <-handleNtfn: @@ -563,34 +573,35 @@ func GetInfo(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Call down to btcd for all of the information in this command known // by them. This call can not realistically ever fail. gicmd, _ := btcjson.NewGetInfoCmd(<-NewJSONID) - req := NewServerRequest(gicmd, make(map[string]interface{})) - response := <-CurrentServerConn().SendRequest(req) - if response.Error() != nil { - return nil, response.Error() + response := <-CurrentServerConn().SendRequest(NewServerRequest(gicmd)) + + var info btcjson.InfoResult + _, jsonErr := response.FinishUnmarshal(&info) + if jsonErr != nil { + return nil, jsonErr } - ret := response.Result().(map[string]interface{}) balance := float64(0.0) accounts := AcctMgr.ListAccounts(1) for _, v := range accounts { balance += v } - ret["walletversion"] = wallet.VersCurrent.Uint32() - ret["balance"] = balance + info.WalletVersion = int(wallet.VersCurrent.Uint32()) + info.Balance = balance // Keypool times are not tracked. set to current time. - ret["keypoololdest"] = time.Now().Unix() - ret["keypoolsize"] = cfg.KeypoolSize + info.KeypoolOldest = time.Now().Unix() + info.KeypoolSize = int(cfg.KeypoolSize) TxFeeIncrement.Lock() - ret["paytxfee"] = TxFeeIncrement.i + info.PaytxFee = float64(TxFeeIncrement.i) / float64(btcutil.SatoshiPerBitcoin) TxFeeIncrement.Unlock() /* * We don't set the following since they don't make much sense in the * wallet architecture: - * ret["unlocked_until"] - * ret["errors"] + * - unlocked_until + * - errors */ - return ret, nil + return info, nil } // GetAccount handles a getaccount request by returning the account name @@ -1070,8 +1081,7 @@ func ListSinceBlock(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { } } - req := NewServerRequest(gbh, new(string)) - bhChan := CurrentServerConn().SendRequest(req) + bhChan := CurrentServerConn().SendRequest(NewServerRequest(gbh)) txInfoList, err := AcctMgr.ListSinceBlock(height, bs.Height, cmd.TargetConfirmations) @@ -1084,15 +1094,15 @@ func ListSinceBlock(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Done with work, get the response. response := <-bhChan - if response.Error() != nil { - return nil, response.Error() + var hash string + _, jsonErr := response.FinishUnmarshal(&hash) + if jsonErr != nil { + return nil, jsonErr } - hash := response.Result().(*string) - res := make(map[string]interface{}) res["transactions"] = txInfoList - res["lastblock"] = *hash + res["lastblock"] = hash return res, nil } diff --git a/sockets.go b/sockets.go index d4169bc..5a96055 100644 --- a/sockets.go +++ b/sockets.go @@ -263,17 +263,23 @@ func (s *server) ReplyToFrontend(msg []byte, ws, authenticated bool) ([]byte, er } cReq := NewClientRequest(cmd, ws) - result, jsonErr := cReq.Handle() + rawResp := cReq.Handle() - response := btcjson.Reply{ - Id: &id, - Result: result, - Error: jsonErr, + response := struct { + Jsonrpc string `json:"jsonrpc"` + Id interface{} `json:"id"` + Result *json.RawMessage `json:"result"` + Error *json.RawMessage `json:"error"` + }{ + Jsonrpc: "1.0", + Id: id, + Result: rawResp.Result, + Error: rawResp.Error, } mresponse, err := json.Marshal(response) if err != nil { log.Errorf("Cannot marhal response: %v", err) - response = btcjson.Reply{ + response := btcjson.Reply{ Id: &id, Error: &btcjson.ErrInternal, }