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.
This commit is contained in:
Josh Rickmar 2014-04-09 11:07:09 -05:00
parent 6216012aac
commit b1a71d5f83
4 changed files with 197 additions and 197 deletions

103
rpc.go
View file

@ -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
}

View file

@ -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,22 +161,44 @@ 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)
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: r.Result,
err: jsonErr
}
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,
@ -194,22 +210,12 @@ func (btcd *BtcdRPCConn) Start() {
// 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
*/
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
}

View file

@ -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
}

View file

@ -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,
}