diff --git a/btcjson/v2/btcjson/chainsvrcmds.go b/btcjson/v2/btcjson/chainsvrcmds.go index 47cedafe..573adf45 100644 --- a/btcjson/v2/btcjson/chainsvrcmds.go +++ b/btcjson/v2/btcjson/chainsvrcmds.go @@ -417,7 +417,7 @@ func NewGetRawTransactionCmd(txHash string, verbose *int) *GetRawTransactionCmd // GetTxOutCmd defines the gettxout JSON-RPC command. type GetTxOutCmd struct { Txid string - Vout int + Vout uint32 IncludeMempool *bool `jsonrpcdefault:"true"` } @@ -426,7 +426,7 @@ type GetTxOutCmd struct { // // The parameters which are pointers indicate they are optional. Passing nil // for optional parameters will use the default value. -func NewGetTxOutCmd(txHash string, vout int, includeMempool *bool) *GetTxOutCmd { +func NewGetTxOutCmd(txHash string, vout uint32, includeMempool *bool) *GetTxOutCmd { return &GetTxOutCmd{ Txid: txHash, Vout: vout, diff --git a/rpcserver.go b/rpcserver.go index 11c8964b..2bce6a9b 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1,4 +1,4 @@ -// Copyright (c) 2013 Conformal Systems LLC. +// Copyright (c) 2013-2015 Conformal Systems LLC. // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -11,6 +11,7 @@ import ( "encoding/base64" "encoding/binary" "encoding/hex" + "encoding/json" "errors" "fmt" "io" @@ -28,8 +29,8 @@ import ( "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec" - "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcjson/btcws" + "github.com/btcsuite/btcd/btcjson/v2/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/database" "github.com/btcsuite/btcd/txscript" @@ -105,16 +106,26 @@ var ( // Errors var ( - // ErrBadParamsField describes an error where the parameters JSON - // field cannot be properly parsed. - ErrBadParamsField = errors.New("bad params field") + // ErrRPCUnimplemented is an error returned to RPC clients when the + // provided command is recognized, but not implemented. + ErrRPCUnimplemented = &btcjson.RPCError{ + Code: btcjson.ErrRPCUnimplemented, + Message: "Command unimplemented", + } + + // ErrRPCNoWallet is an error returned to RPC clients when the provided + // command is recognized as a wallet command. + ErrRPCNoWallet = &btcjson.RPCError{ + Code: btcjson.ErrRPCNoWallet, + Message: "This implementation does not implement wallet commands", + } ) -type commandHandler func(*rpcServer, btcjson.Cmd, <-chan struct{}) (interface{}, error) +type commandHandler func(*rpcServer, interface{}, <-chan struct{}) (interface{}, error) -// handlers maps RPC command strings to appropriate handler functions. -// this is copied by init because help references rpcHandlers and thus causes -// a dependancy loop. +// rpcHandlers maps RPC command strings to appropriate handler functions. +// This is set by init because help references rpcHandlers and thus causes +// a dependency loop. var rpcHandlers map[string]commandHandler var rpcHandlersBeforeInit = map[string]commandHandler{ "addnode": handleAddNode, @@ -122,17 +133,13 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "debuglevel": handleDebugLevel, "decoderawtransaction": handleDecodeRawTransaction, "decodescript": handleDecodeScript, - "estimatefee": handleUnimplemented, - "estimatepriority": handleUnimplemented, "getaddednodeinfo": handleGetAddedNodeInfo, "getbestblock": handleGetBestBlock, "getbestblockhash": handleGetBestBlockHash, "getblock": handleGetBlock, - "getblockchaininfo": handleUnimplemented, "getblockcount": handleGetBlockCount, "getblockhash": handleGetBlockHash, "getblocktemplate": handleGetBlockTemplate, - "getchaintips": handleUnimplemented, "getconnectioncount": handleGetConnectionCount, "getcurrentnet": handleGetCurrentNet, "getdifficulty": handleGetDifficulty, @@ -142,7 +149,6 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "getmininginfo": handleGetMiningInfo, "getnettotals": handleGetNetTotals, "getnetworkhashps": handleGetNetworkHashPS, - "getnetworkinfo": handleUnimplemented, "getpeerinfo": handleGetPeerInfo, "getrawmempool": handleGetRawMempool, "getrawtransaction": handleGetRawTransaction, @@ -208,13 +214,19 @@ var rpcAskWallet = map[string]struct{}{ "walletpassphrasechange": struct{}{}, } -// Commands that are temporarily unimplemented. -var rpcUnimplemented = map[string]struct{}{} +// Commands that are currently unimplemented, but should ultimately be. +var rpcUnimplemented = map[string]struct{}{ + "estimatefee": struct{}{}, + "estimatepriority": struct{}{}, + "getblockchaininfo": struct{}{}, + "getchaintips": struct{}{}, + "getnetworkinfo": struct{}{}, +} -// builderScript is a convenience function which is used to for hard-coded -// scripts built with the script builder. Any errors are converted to a panic -// since it is only, and must only, be used with hard-coded, and therefore, -// known good, scripts. +// builderScript is a convenience function which is used for hard-coded scripts +// built with the script builder. Any errors are converted to a panic since it +// is only, and must only, be used with hard-coded, and therefore, known good, +// scripts. func builderScript(builder *txscript.ScriptBuilder) []byte { script, err := builder.Script() if err != nil { @@ -223,6 +235,28 @@ func builderScript(builder *txscript.ScriptBuilder) []byte { return script } +// internalRPCError is a convenience function to convert an internal error to +// an RPC error with the appropriate code set. It also logs the error to the +// RPC server subsystem since internal errors really should not occur. The +// context parameter is only used in the log message and may be empty if it's +// not needed. +func internalRPCError(errStr, context string) *btcjson.RPCError { + logStr := errStr + if context != "" { + logStr = context + ": " + errStr + } + rpcsLog.Error(logStr) + return btcjson.NewRPCError(btcjson.ErrRPCInternal.Code, errStr) +} + +// rpcDecodeHexError is a convenience function for returning a nicely formatted +// RPC error which indicates the provided hex string failed to decode. +func rpcDecodeHexError(gotHex string) *btcjson.RPCError { + return btcjson.NewRPCError(btcjson.ErrRPCDecodeHexString, + fmt.Sprintf("Argument must be hexadecimal string (not %q)", + gotHex)) +} + // workStateBlockInfo houses information about how to reconstruct a block given // its template and signature script. type workStateBlockInfo struct { @@ -272,21 +306,21 @@ func newGbtWorkState(timeSource blockchain.MedianTimeSource) *gbtWorkState { } } -// handleUnimplemented is a temporary handler for commands that we should -// support but do not. -func handleUnimplemented(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { - return nil, btcjson.ErrUnimplemented +// handleUnimplemented is the handler for commands that should ultimately be +// supported but are not yet implemented. +func handleUnimplemented(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + return nil, ErrRPCUnimplemented } -// handleAskWallet is the handler for commands that we do recognise as valid -// but that we can not answer correctly since it involves wallet state. +// handleAskWallet is the handler for commands that are recognized as valid, but +// are unable to answer correctly since it involves wallet state. // These commands will be implemented in btcwallet. -func handleAskWallet(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { - return nil, btcjson.ErrNoWallet +func handleAskWallet(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + return nil, ErrRPCNoWallet } // handleAddNode handles addnode commands. -func handleAddNode(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleAddNode(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.AddNodeCmd) addr := normalizeAddress(c.Addr, activeNetParams.DefaultPort) @@ -303,10 +337,7 @@ func handleAddNode(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (in } if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } + return nil, internalRPCError(err.Error(), "") } // no data returned unless an error. @@ -317,18 +348,16 @@ func handleAddNode(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (in // latest protocol version and returns a hex-encoded string of the result. func messageToHex(msg wire.Message) (string, error) { var buf bytes.Buffer - err := msg.BtcEncode(&buf, maxProtocolVersion) - if err != nil { - return "", btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } + if err := msg.BtcEncode(&buf, maxProtocolVersion); err != nil { + context := fmt.Sprintf("Failed to encode msg of type %T", msg) + return "", internalRPCError(err.Error(), context) } + return hex.EncodeToString(buf.Bytes()), nil } // handleCreateRawTransaction handles createrawtransaction commands. -func handleCreateRawTransaction(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleCreateRawTransaction(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.CreateRawTransactionCmd) // Add all transaction inputs to a new transaction after performing @@ -337,14 +366,7 @@ func handleCreateRawTransaction(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan for _, input := range c.Inputs { txHash, err := wire.NewShaHashFromStr(input.Txid) if err != nil { - return nil, btcjson.ErrDecodeHexString - } - - if input.Vout < 0 { - return nil, btcjson.Error{ - Code: btcjson.ErrInvalidParameter.Code, - Message: "Invalid parameter, vout must be positive", - } + return nil, rpcDecodeHexError(input.Txid) } prevOut := wire.NewOutPoint(txHash, uint32(input.Vout)) @@ -357,8 +379,8 @@ func handleCreateRawTransaction(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan for encodedAddr, amount := range c.Amounts { // Ensure amount is in the valid range for monetary amounts. if amount <= 0 || amount > btcutil.MaxSatoshi { - return nil, btcjson.Error{ - Code: btcjson.ErrType.Code, + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCType, Message: "Invalid amount", } } @@ -367,10 +389,9 @@ func handleCreateRawTransaction(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan addr, err := btcutil.DecodeAddress(encodedAddr, activeNetParams.Params) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrInvalidAddressOrKey.Code, - Message: btcjson.ErrInvalidAddressOrKey.Message + - ": " + err.Error(), + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: "Invalid address or key: " + err.Error(), } } @@ -381,31 +402,41 @@ func handleCreateRawTransaction(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan case *btcutil.AddressPubKeyHash: case *btcutil.AddressScriptHash: default: - return nil, btcjson.ErrInvalidAddressOrKey + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: "Invalid address or key", + } } if !addr.IsForNet(s.server.chainParams) { - return nil, btcjson.Error{ - Code: btcjson.ErrInvalidAddressOrKey.Code, - Message: fmt.Sprintf("%s: %q", - btcjson.ErrInvalidAddressOrKey.Message, - encodedAddr), + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: "Invalid address: " + encodedAddr + + " is for the wrong network", } } // Create a new script which pays to the provided address. pkScript, err := txscript.PayToAddrScript(addr) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } + context := "Failed to generate pay-to-address script" + return nil, internalRPCError(err.Error(), context) } - txOut := wire.NewTxOut(amount, pkScript) + // Convert the amount to satoshi. + satoshi, err := btcutil.NewAmount(amount) + if err != nil { + context := "Failed to convert amount" + return nil, internalRPCError(err.Error(), context) + } + + txOut := wire.NewTxOut(int64(satoshi), pkScript) mtx.AddTxOut(txOut) } - // Return the serialized and hex-encoded transaction. + // Return the serialized and hex-encoded transaction. Note that this + // is intentionally not directly returning because the first return + // value is a string and it would result in returning an empty string to + // the client instead of nothing (nil) in the case of an error. mtxHex, err := messageToHex(mtx) if err != nil { return nil, err @@ -414,7 +445,7 @@ func handleCreateRawTransaction(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan } // handleDebugLevel handles debuglevel commands. -func handleDebugLevel(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleDebugLevel(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.DebugLevelCmd) // Special show command to list supported subsystems. @@ -425,8 +456,8 @@ func handleDebugLevel(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) err := parseAndSetDebugLevels(c.LevelSpec) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrInvalidParams.Code, + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParams.Code, Message: err.Error(), } } @@ -497,9 +528,9 @@ func createVoutList(mtx *wire.MsgTx, chainParams *chaincfg.Params) []btcjson.Vou // createTxRawResult converts the passed transaction and associated parameters // to a raw transaction JSON object. -func createTxRawResult(chainParams *chaincfg.Params, txSha string, - mtx *wire.MsgTx, blk *btcutil.Block, maxidx int64, - blksha *wire.ShaHash) (*btcjson.TxRawResult, error) { +func createTxRawResult(chainParams *chaincfg.Params, txHash string, + mtx *wire.MsgTx, blk *btcutil.Block, maxIdx int64, + blkHash *wire.ShaHash) (*btcjson.TxRawResult, error) { mtxHex, err := messageToHex(mtx) if err != nil { @@ -508,7 +539,7 @@ func createTxRawResult(chainParams *chaincfg.Params, txSha string, txReply := &btcjson.TxRawResult{ Hex: mtxHex, - Txid: txSha, + Txid: txHash, Vout: createVoutList(mtx, chainParams), Vin: createVinList(mtx), Version: mtx.Version, @@ -522,15 +553,15 @@ func createTxRawResult(chainParams *chaincfg.Params, txSha string, // This is not a typo, they are identical in bitcoind as well. txReply.Time = blockHeader.Timestamp.Unix() txReply.Blocktime = blockHeader.Timestamp.Unix() - txReply.BlockHash = blksha.String() - txReply.Confirmations = uint64(1 + maxidx - idx) + txReply.BlockHash = blkHash.String() + txReply.Confirmations = uint64(1 + maxIdx - idx) } return txReply, nil } // handleDecodeRawTransaction handles decoderawtransaction commands. -func handleDecodeRawTransaction(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleDecodeRawTransaction(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.DecodeRawTransactionCmd) // Deserialize the transaction. @@ -540,25 +571,21 @@ func handleDecodeRawTransaction(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan } serializedTx, err := hex.DecodeString(hexStr) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrDecodeHexString.Code, - Message: fmt.Sprintf("argument must be hexadecimal "+ - "string (not %q)", hexStr), - } + return nil, rpcDecodeHexError(hexStr) } var mtx wire.MsgTx err = mtx.Deserialize(bytes.NewReader(serializedTx)) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrDeserialization.Code, - Message: "TX decode failed", + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDeserialization, + Message: "TX decode failed: " + err.Error(), } } - txSha, _ := mtx.TxSha() + txHash, _ := mtx.TxSha() // Create and return the result. txReply := btcjson.TxRawDecodeResult{ - Txid: txSha.String(), + Txid: txHash.String(), Version: mtx.Version, Locktime: mtx.LockTime, Vin: createVinList(&mtx), @@ -568,7 +595,7 @@ func handleDecodeRawTransaction(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan } // handleDecodeScript handles decodescript commands. -func handleDecodeScript(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleDecodeScript(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.DecodeScriptCmd) // Convert the hex script to bytes. @@ -578,11 +605,7 @@ func handleDecodeScript(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{} } script, err := hex.DecodeString(hexStr) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrDecodeHexString.Code, - Message: fmt.Sprintf("argument must be hexadecimal "+ - "string (not %q)", hexStr), - } + return nil, rpcDecodeHexError(hexStr) } // The disassembled string will contain [error] inline if the script @@ -602,10 +625,8 @@ func handleDecodeScript(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{} // Convert the script itself to a pay-to-script-hash address. p2sh, err := btcutil.NewAddressScriptHash(script, s.server.chainParams) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } + context := "Failed to convert script to pay-to-script-hash" + return nil, internalRPCError(err.Error(), context) } // Generate and return the reply. @@ -620,31 +641,32 @@ func handleDecodeScript(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{} } // handleGetAddedNodeInfo handles getaddednodeinfo commands. -func handleGetAddedNodeInfo(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetAddedNodeInfo(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.GetAddedNodeInfoCmd) // Retrieve a list of persistent (added) peers from the bitcoin server // and filter the list of peer per the specified address (if any). peers := s.server.AddedNodeInfo() - if c.Node != "" { + if c.Node != nil { + node := *c.Node found := false for i, peer := range peers { - if peer.addr == c.Node { + if peer.addr == node { peers = peers[i : i+1] found = true } } if !found { - return nil, btcjson.Error{ - Code: -24, - Message: "Node has not been added.", + return nil, &btcjson.RPCError{ + Code: -24, // TODO: ErrRPCClientNodeNotAdded + Message: "Node has not been added", } } } // Without the dns flag, the result is just a slice of the addresses as // strings. - if !c.Dns { + if !c.DNS { results := make([]string, 0, len(peers)) for _, peer := range peers { results = append(results, peer.addr) @@ -660,8 +682,7 @@ func handleGetAddedNodeInfo(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan stru // or a domain name. var result btcjson.GetAddedNodeInfoResult result.AddedNode = peer.addr - isConnected := peer.Connected() - result.Connected = &isConnected + result.Connected = btcjson.Bool(peer.Connected()) // Split the address into host and port portions so we can do // a DNS lookup against the host. When no port is specified in @@ -703,13 +724,16 @@ func handleGetAddedNodeInfo(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan stru } // handleGetBestBlock implements the getbestblock command. -func handleGetBestBlock(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetBestBlock(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { // All other "get block" commands give either the height, the // hash, or both but require the block SHA. This gets both for // the best block. sha, height, err := s.server.db.NewestSha() if err != nil { - return nil, btcjson.ErrBestBlockHash + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCBestBlockHash, + Message: "Error getting best block hash", + } } result := &btcws.GetBestBlockResult{ @@ -720,11 +744,14 @@ func handleGetBestBlock(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{} } // handleGetBestBlockHash implements the getbestblockhash command. -func handleGetBestBlockHash(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetBestBlockHash(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { sha, _, err := s.server.db.NewestSha() if err != nil { rpcsLog.Errorf("Error getting newest sha: %v", err) - return nil, btcjson.ErrBestBlockHash + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCBestBlockHash, + Message: "Error getting best block hash", + } } return sha.String(), nil @@ -751,22 +778,28 @@ func getDifficultyRatio(bits uint32) float64 { } // handleGetBlock implements the getblock command. -func handleGetBlock(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetBlock(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.GetBlockCmd) + sha, err := wire.NewShaHashFromStr(c.Hash) if err != nil { - rpcsLog.Errorf("Error generating sha: %v", err) - return nil, btcjson.ErrBlockNotFound + return nil, rpcDecodeHexError(c.Hash) } blk, err := s.server.db.FetchBlockBySha(sha) if err != nil { - rpcsLog.Errorf("Error fetching sha: %v", err) - return nil, btcjson.ErrBlockNotFound + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCBlockNotFound, + Message: "Block not found", + } } // When the verbose flag isn't set, simply return the network-serialized // block as a hex-encoded string. - if !c.Verbose { + if c.Verbose != nil && !*c.Verbose { + // Note that this is intentionally not directly returning + // because the first return value is a string and it would + // result in returning an empty string to the client instead of + // nothing (nil) in the case of an error. blkHex, err := messageToHex(blk.MsgBlock()) if err != nil { return nil, err @@ -777,35 +810,32 @@ func handleGetBlock(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (i // The verbose flag is set, so generate the JSON object and return it. buf, err := blk.Bytes() if err != nil { - rpcsLog.Errorf("Error fetching block: %v", err) - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } + context := "Failed to get block bytes" + return nil, internalRPCError(err.Error(), context) } idx := blk.Height() - _, maxidx, err := s.server.db.NewestSha() + _, maxIdx, err := s.server.db.NewestSha() if err != nil { - rpcsLog.Errorf("Cannot get newest sha: %v", err) - return nil, btcjson.ErrBlockNotFound + context := "Failed to get newest hash" + return nil, internalRPCError(err.Error(), context) } blockHeader := &blk.MsgBlock().Header - blockReply := btcjson.BlockResult{ + blockReply := btcjson.GetBlockVerboseResult{ Hash: c.Hash, Version: blockHeader.Version, MerkleRoot: blockHeader.MerkleRoot.String(), PreviousHash: blockHeader.PrevBlock.String(), Nonce: blockHeader.Nonce, Time: blockHeader.Timestamp.Unix(), - Confirmations: uint64(1 + maxidx - idx), + Confirmations: uint64(1 + maxIdx - idx), Height: idx, Size: int32(len(buf)), Bits: strconv.FormatInt(int64(blockHeader.Bits), 16), Difficulty: getDifficultyRatio(blockHeader.Bits), } - if !c.VerboseTx { + if c.VerboseTx == nil || !*c.VerboseTx { transactions := blk.Transactions() txNames := make([]string, len(transactions)) for i, tx := range transactions { @@ -817,14 +847,12 @@ func handleGetBlock(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (i txns := blk.Transactions() rawTxns := make([]btcjson.TxRawResult, len(txns)) for i, tx := range txns { - txSha := tx.Sha().String() + txHash := tx.Sha().String() mtx := tx.MsgTx() rawTxn, err := createTxRawResult(s.server.chainParams, - txSha, mtx, blk, maxidx, sha) + txHash, mtx, blk, maxIdx, sha) if err != nil { - rpcsLog.Errorf("Cannot create TxRawResult for "+ - "transaction %s: %v", txSha, err) return nil, err } rawTxns[i] = *rawTxn @@ -833,12 +861,12 @@ func handleGetBlock(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (i } // Get next block unless we are already at the top. - if idx < maxidx { + if idx < maxIdx { var shaNext *wire.ShaHash shaNext, err = s.server.db.FetchBlockShaByHeight(int64(idx + 1)) if err != nil { - rpcsLog.Errorf("No next block: %v", err) - return nil, btcjson.ErrBlockNotFound + context := "No next block" + return nil, internalRPCError(err.Error(), context) } blockReply.NextHash = shaNext.String() } @@ -847,23 +875,28 @@ func handleGetBlock(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (i } // handleGetBlockCount implements the getblockcount command. -func handleGetBlockCount(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { - _, maxidx, err := s.server.db.NewestSha() +func handleGetBlockCount(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + _, maxIdx, err := s.server.db.NewestSha() if err != nil { rpcsLog.Errorf("Error getting newest sha: %v", err) - return nil, btcjson.ErrBlockCount + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCBlockCount, + Message: "Error getting block count: " + err.Error(), + } } - return maxidx, nil + return maxIdx, nil } // handleGetBlockHash implements the getblockhash command. -func handleGetBlockHash(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetBlockHash(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.GetBlockHashCmd) sha, err := s.server.db.FetchBlockShaByHeight(c.Index) if err != nil { - rpcsLog.Errorf("Error getting block: %v", err) - return nil, btcjson.ErrOutOfRange + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCOutOfRange, + Message: "Block number out of range", + } } return sha.String(), nil @@ -1061,13 +1094,8 @@ func (state *gbtWorkState) updateBlockTemplate(s *rpcServer, useCoinbaseValue bo // appropriate address(es). blkTemplate, err := NewBlockTemplate(s.server.txMemPool, payAddr) if err != nil { - errStr := fmt.Sprintf("Failed to create new block "+ - "template: %v", err) - rpcsLog.Error(errStr) - return btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: errStr, - } + return internalRPCError("Failed to create new block "+ + "template: "+err.Error(), "") } template = blkTemplate msgBlock = template.block @@ -1080,10 +1108,8 @@ func (state *gbtWorkState) updateBlockTemplate(s *rpcServer, useCoinbaseValue bo chainState := &s.server.blockManager.chainState minTimestamp, err := minimumMedianTime(chainState) if err != nil { - return btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } + context := "Failed to get minimum median time" + return internalRPCError(err.Error(), context) } // Update work state to ensure another block template isn't @@ -1123,10 +1149,8 @@ func (state *gbtWorkState) updateBlockTemplate(s *rpcServer, useCoinbaseValue bo // pay to the randomly selected payment address. pkScript, err := txscript.PayToAddrScript(payToAddr) if err != nil { - return btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } + context := "Failed to create pay-to-addr script" + return internalRPCError(err.Error(), context) } template.block.Transactions[0].TxOut[0].PkScript = pkScript template.validPayAddress = true @@ -1172,8 +1196,8 @@ func (state *gbtWorkState) blockTemplateResult(useCoinbaseValue bool, submitOld adjustedTime := state.timeSource.AdjustedTime() maxTime := adjustedTime.Add(time.Second * blockchain.MaxTimeOffsetSeconds) if header.Timestamp.After(maxTime) { - return nil, btcjson.Error{ - Code: btcjson.ErrOutOfRange.Code, + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCOutOfRange, Message: fmt.Sprintf("The template time is after the "+ "maximum allowed time for a block - template "+ "time %v, maximum time %v", adjustedTime, @@ -1216,10 +1240,8 @@ func (state *gbtWorkState) blockTemplateResult(useCoinbaseValue bool, submitOld // Serialize the transaction for later conversion to hex. txBuf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) if err := tx.Serialize(txBuf); err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } + context := "Failed to serialize transaction" + return nil, internalRPCError(err.Error(), context) } resultTx := btcjson.GetBlockTemplateResultTx{ @@ -1263,8 +1285,8 @@ func (state *gbtWorkState) blockTemplateResult(useCoinbaseValue bool, submitOld // Ensure the template has a valid payment address associated // with it when a full coinbase is requested. if !template.validPayAddress { - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInternal.Code, Message: "A coinbase transaction has been " + "requested, but the server has not " + "been configured with any payment " + @@ -1277,10 +1299,8 @@ func (state *gbtWorkState) blockTemplateResult(useCoinbaseValue bool, submitOld txHash, _ := tx.TxSha() txBuf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) if err := tx.Serialize(txBuf); err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } + context := "Failed to serialize transaction" + return nil, internalRPCError(err.Error(), context) } resultTx := btcjson.GetBlockTemplateResultTx{ @@ -1424,8 +1444,8 @@ func handleGetBlockTemplateRequest(s *rpcServer, request *btcjson.TemplateReques // When a coinbase transaction has been requested, respond with an error // if there are no addresses to pay the created block template to. if !useCoinbaseValue && len(cfg.miningAddrs) == 0 { - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInternal.Code, Message: "A coinbase transaction has been requested, " + "but the server has not been configured with " + "any payment addresses via --miningaddr", @@ -1437,13 +1457,19 @@ func handleGetBlockTemplateRequest(s *rpcServer, request *btcjson.TemplateReques // However, allow this state when running in the regression test or // simulation test mode. if !(cfg.RegressionTest || cfg.SimNet) && s.server.ConnectedCount() == 0 { - return nil, btcjson.ErrClientNotConnected + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCClientNotConnected, + Message: "Bitcoin is not connected", + } } // No point in generating or accepting work before the chain is synced. _, currentHeight := s.server.blockManager.chainState.Best() if currentHeight != 0 && !s.server.blockManager.IsCurrent() { - return nil, btcjson.ErrClientInInitialDownload + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCClientInInitialDownload, + Message: "Bitcoin is downloading blocks...", + } } // When a long poll ID was provided, this is a long poll request by the @@ -1571,9 +1597,9 @@ func chainErrToGBTErrString(err error) string { func handleGetBlockTemplateProposal(s *rpcServer, request *btcjson.TemplateRequest) (interface{}, error) { hexData := request.Data if hexData == "" { - return false, btcjson.Error{ - Code: btcjson.ErrType.Code, - Message: fmt.Sprintf("data must contain the " + + return false, &btcjson.RPCError{ + Code: btcjson.ErrRPCType, + Message: fmt.Sprintf("Data must contain the " + "hex-encoded serialized block that is being " + "proposed"), } @@ -1585,16 +1611,16 @@ func handleGetBlockTemplateProposal(s *rpcServer, request *btcjson.TemplateReque } dataBytes, err := hex.DecodeString(hexData) if err != nil { - return false, btcjson.Error{ - Code: btcjson.ErrDeserialization.Code, - Message: fmt.Sprintf("data must be "+ + return false, &btcjson.RPCError{ + Code: btcjson.ErrRPCDeserialization, + Message: fmt.Sprintf("Data must be "+ "hexadecimal string (not %q)", hexData), } } var msgBlock wire.MsgBlock if err := msgBlock.Deserialize(bytes.NewReader(dataBytes)); err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrDeserialization.Code, + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDeserialization, Message: "Block decode failed: " + err.Error(), } } @@ -1611,10 +1637,10 @@ func handleGetBlockTemplateProposal(s *rpcServer, request *btcjson.TemplateReque isOrphan, err := s.server.blockManager.ProcessBlock(block, flags) if err != nil { if _, ok := err.(blockchain.RuleError); !ok { - rpcsLog.Errorf("Failed to process block proposal: %v", - err) - return nil, btcjson.Error{ - Code: -25, // ErrRpcVerify + err := rpcsLog.Errorf("Failed to process block "+ + "proposal: %v", err) + return nil, &btcjson.RPCError{ + Code: -25, // TODO: ErrRpcVerify Message: err.Error(), } } @@ -1633,7 +1659,7 @@ func handleGetBlockTemplateProposal(s *rpcServer, request *btcjson.TemplateReque // // See https://en.bitcoin.it/wiki/BIP_0022 and // https://en.bitcoin.it/wiki/BIP_0023 for more details. -func handleGetBlockTemplate(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetBlockTemplate(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.GetBlockTemplateCmd) request := c.Request @@ -1650,63 +1676,69 @@ func handleGetBlockTemplate(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan stru return handleGetBlockTemplateProposal(s, request) } - return nil, btcjson.Error{ - Code: btcjson.ErrInvalidParameter.Code, + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, Message: "Invalid mode", } } // handleGetConnectionCount implements the getconnectioncount command. -func handleGetConnectionCount(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetConnectionCount(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { return s.server.ConnectedCount(), nil } // handleGetCurrentNet implements the getcurrentnet command. -func handleGetCurrentNet(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetCurrentNet(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { return s.server.chainParams.Net, nil } // handleGetDifficulty implements the getdifficulty command. -func handleGetDifficulty(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetDifficulty(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { sha, _, err := s.server.db.NewestSha() if err != nil { rpcsLog.Errorf("Error getting sha: %v", err) - return nil, btcjson.ErrDifficulty + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDifficulty, + Message: "Error getting difficulty: " + err.Error(), + } } blockHeader, err := s.server.db.FetchBlockHeaderBySha(sha) if err != nil { rpcsLog.Errorf("Error getting block: %v", err) - return nil, btcjson.ErrDifficulty + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDifficulty, + Message: "Error getting difficulty: " + err.Error(), + } } return getDifficultyRatio(blockHeader.Bits), nil } // handleGetGenerate implements the getgenerate command. -func handleGetGenerate(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetGenerate(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { return s.server.cpuMiner.IsMining(), nil } // handleGetHashesPerSec implements the gethashespersec command. -func handleGetHashesPerSec(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetHashesPerSec(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { return int64(s.server.cpuMiner.HashesPerSecond()), nil } // handleGetInfo implements the getinfo command. We only return the fields // that are not related to wallet functionality. -func handleGetInfo(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetInfo(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { // We require the current block height and sha. sha, height, err := s.server.db.NewestSha() if err != nil { - rpcsLog.Errorf("Error getting sha: %v", err) - return nil, btcjson.ErrBlockCount + context := "Failed to get newest hash" + return nil, internalRPCError(err.Error(), context) } blkHeader, err := s.server.db.FetchBlockHeaderBySha(sha) if err != nil { - rpcsLog.Errorf("Error getting block: %v", err) - return nil, btcjson.ErrDifficulty + context := "Failed to get block" + return nil, internalRPCError(err.Error(), context) } - ret := &btcjson.InfoResult{ + ret := &btcjson.InfoChainResult{ Version: int32(1000000*appMajor + 10000*appMinor + 100*appPatch), ProtocolVersion: int32(maxProtocolVersion), Blocks: int32(height), @@ -1723,45 +1755,35 @@ func handleGetInfo(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (in // handleGetMiningInfo implements the getmininginfo command. We only return the // fields that are not related to wallet functionality. -func handleGetMiningInfo(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetMiningInfo(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { sha, height, err := s.server.db.NewestSha() if err != nil { - rpcsLog.Errorf("Error getting sha: %v", err) - return nil, btcjson.ErrBlockCount + context := "Failed to get newest hash" + return nil, internalRPCError(err.Error(), context) } block, err := s.server.db.FetchBlockBySha(sha) if err != nil { - rpcsLog.Errorf("Error getting block: %v", err) - return nil, btcjson.ErrBlockNotFound + context := "Failed to get block" + return nil, internalRPCError(err.Error(), context) } blockBytes, err := block.Bytes() if err != nil { - rpcsLog.Errorf("Error getting block: %v", err) - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } + context := "Failed to get block bytes" + return nil, internalRPCError(err.Error(), context) } // Create a default getnetworkhashps command to use defaults and make // use of the existing getnetworkhashps handler. - gnhpsCmd, err := btcjson.NewGetNetworkHashPSCmd(0) - if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } - } + gnhpsCmd := btcjson.NewGetNetworkHashPSCmd(nil, nil) networkHashesPerSecIface, err := handleGetNetworkHashPS(s, gnhpsCmd, closeChan) if err != nil { - // This is already a btcjson.Error from the handler. return nil, err } networkHashesPerSec, ok := networkHashesPerSecIface.(int64) if !ok { - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInternal.Code, Message: "networkHashesPerSec is not an int64", } } @@ -1782,7 +1804,7 @@ func handleGetMiningInfo(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{ } // handleGetNetTotals implements the getnettotals command. -func handleGetNetTotals(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetNetTotals(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { totalBytesRecv, totalBytesSent := s.server.NetTotals() reply := &btcjson.GetNetTotalsResult{ TotalBytesRecv: totalBytesRecv, @@ -1793,22 +1815,23 @@ func handleGetNetTotals(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{} } // handleGetNetworkHashPS implements the getnetworkhashps command. -func handleGetNetworkHashPS(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetNetworkHashPS(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.GetNetworkHashPSCmd) _, newestHeight, err := s.server.db.NewestSha() if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } + context := "Failed to get newest hash" + return nil, internalRPCError(err.Error(), context) } // When the passed height is too high or zero, just return 0 now // since we can't reasonably calculate the number of network hashes // per second from invalid values. When it's negative, use the current // best block height. - endHeight := int64(c.Height) + var endHeight int64 + if c.Height != nil { + endHeight = int64(*c.Height) + } if endHeight > newestHeight || endHeight == 0 { return 0, nil } @@ -1820,11 +1843,15 @@ func handleGetNetworkHashPS(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan stru // blocks. When the passed value is negative, use the last block the // difficulty changed as the starting height. Also make sure the // starting height is not before the beginning of the chain. + var numBlocks int64 + if c.Blocks != nil { + numBlocks = int64(*c.Blocks) + } var startHeight int64 - if c.Blocks <= 0 { + if numBlocks <= 0 { startHeight = endHeight - ((endHeight % blockchain.BlocksPerRetarget) + 1) } else { - startHeight = endHeight - int64(c.Blocks) + startHeight = endHeight - numBlocks } if startHeight < 0 { startHeight = 0 @@ -1839,18 +1866,14 @@ func handleGetNetworkHashPS(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan stru for curHeight := startHeight; curHeight <= endHeight; curHeight++ { hash, err := s.server.db.FetchBlockShaByHeight(curHeight) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } + context := "Failed to fetch block hash" + return nil, internalRPCError(err.Error(), context) } header, err := s.server.db.FetchBlockHeaderBySha(hash) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } + context := "Failed to fetch block header" + return nil, internalRPCError(err.Error(), context) } if curHeight == startHeight { @@ -1881,23 +1904,24 @@ func handleGetNetworkHashPS(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan stru } // handleGetPeerInfo implements the getpeerinfo command. -func handleGetPeerInfo(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetPeerInfo(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { return s.server.PeerInfo(), nil } // handleGetRawMempool implements the getrawmempool command. -func handleGetRawMempool(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetRawMempool(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.GetRawMempoolCmd) mp := s.server.txMemPool descs := mp.TxDescs() - if c.Verbose { - result := make(map[string]*btcjson.GetRawMempoolResult, len(descs)) + if c.Verbose != nil && *c.Verbose { + result := make(map[string]*btcjson.GetRawMempoolVerboseResult, + len(descs)) _, newestHeight, err := s.server.db.NewestSha() if err != nil { - rpcsLog.Errorf("Cannot get newest sha: %v", err) - return nil, btcjson.ErrBlockNotFound + context := "Failed to get newest hash" + return nil, internalRPCError(err.Error(), context) } mp.RLock() @@ -1914,9 +1938,9 @@ func handleGetRawMempool(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{ newestHeight+1) } - mpd := &btcjson.GetRawMempoolResult{ + mpd := &btcjson.GetRawMempoolVerboseResult{ Size: int32(desc.Tx.MsgTx().SerializeSize()), - Fee: btcutil.Amount(desc.Fee).ToUnit(btcutil.AmountSatoshi), + Fee: btcutil.Amount(desc.Fee).ToBTC(), Time: desc.Added.Unix(), Height: desc.Height, StartingPriority: startingPriority, @@ -1948,45 +1972,44 @@ func handleGetRawMempool(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{ } // handleGetRawTransaction implements the getrawtransaction command. -func handleGetRawTransaction(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetRawTransaction(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.GetRawTransactionCmd) // Convert the provided transaction hash hex to a ShaHash. - txSha, err := wire.NewShaHashFromStr(c.Txid) + txHash, err := wire.NewShaHashFromStr(c.Txid) if err != nil { - rpcsLog.Errorf("Error generating sha: %v", err) - return nil, btcjson.Error{ - Code: btcjson.ErrBlockNotFound.Code, - Message: "Parameter 1 must be a hexaecimal string", - } + return nil, rpcDecodeHexError(c.Txid) } // Try to fetch the transaction from the memory pool and if that fails, // try the block database. var mtx *wire.MsgTx - var blksha *wire.ShaHash - tx, err := s.server.txMemPool.FetchTransaction(txSha) + var blkHash *wire.ShaHash + tx, err := s.server.txMemPool.FetchTransaction(txHash) if err != nil { - txList, err := s.server.db.FetchTxBySha(txSha) - if err != nil { - rpcsLog.Errorf("Error fetching tx: %v", err) - return nil, btcjson.ErrNoTxInfo - } - if len(txList) == 0 { - return nil, btcjson.ErrNoTxInfo + txList, err := s.server.db.FetchTxBySha(txHash) + if err != nil || len(txList) == 0 { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCNoTxInfo, + Message: "No information available about transaction", + } } lastTx := len(txList) - 1 mtx = txList[lastTx].Tx - blksha = txList[lastTx].BlkSha + blkHash = txList[lastTx].BlkSha } else { mtx = tx.MsgTx() } // When the verbose flag isn't set, simply return the network-serialized // transaction as a hex-encoded string. - if c.Verbose == 0 { + if c.Verbose == nil || *c.Verbose == 0 { + // Note that this is intentionally not directly returning + // because the first return value is a string and it would + // result in returning an empty string to the client instead of + // nothing (nil) in the case of an error. mtxHex, err := messageToHex(mtx) if err != nil { return nil, err @@ -1995,25 +2018,27 @@ func handleGetRawTransaction(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan str } var blk *btcutil.Block - var maxidx int64 - if blksha != nil { - blk, err = s.server.db.FetchBlockBySha(blksha) + var maxIdx int64 + if blkHash != nil { + blk, err = s.server.db.FetchBlockBySha(blkHash) if err != nil { rpcsLog.Errorf("Error fetching sha: %v", err) - return nil, btcjson.ErrBlockNotFound + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCBlockNotFound, + Message: "Block not found: " + err.Error(), + } } - _, maxidx, err = s.server.db.NewestSha() + _, maxIdx, err = s.server.db.NewestSha() if err != nil { - rpcsLog.Errorf("Cannot get newest sha: %v", err) - return nil, btcjson.ErrNoNewestBlockInfo + context := "Failed to get newest hash" + return nil, internalRPCError(err.Error(), context) } } - rawTxn, err := createTxRawResult(s.server.chainParams, c.Txid, mtx, - blk, maxidx, blksha) + rawTxn, err := createTxRawResult(s.server.chainParams, c.Txid, mtx, blk, + maxIdx, blkHash) if err != nil { - rpcsLog.Errorf("Cannot create TxRawResult for txSha=%s: %v", txSha, err) return nil, err } return *rawTxn, nil @@ -2055,64 +2080,76 @@ func reverseUint32Array(b []byte) { } // handleGetTxOut handles gettxout commands. -func handleGetTxOut(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetTxOut(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.GetTxOutCmd) // Convert the provided transaction hash hex to a ShaHash. - txSha, err := wire.NewShaHashFromStr(c.Txid) + txHash, err := wire.NewShaHashFromStr(c.Txid) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrInvalidParameter.Code, - Message: fmt.Sprintf("argument must be hexadecimal "+ - "string (not %q)", c.Txid), - } + return nil, rpcDecodeHexError(c.Txid) } - // If requested and the tx is available in the mempool try to fetch it from - // there, otherwise attempt to fetch from the block database. + // If requested and the tx is available in the mempool try to fetch it + // from there, otherwise attempt to fetch from the block database. var mtx *wire.MsgTx var bestBlockSha string var confirmations int64 var dbSpentInfo []bool - if c.IncludeMempool && s.server.txMemPool.HaveTransaction(txSha) { - tx, err := s.server.txMemPool.FetchTransaction(txSha) + includeMempool := true + if c.IncludeMempool != nil { + includeMempool = *c.IncludeMempool + } + // TODO: This is racy. It should attempt to fetch it directly and check + // the error. + if includeMempool && s.server.txMemPool.HaveTransaction(txHash) { + tx, err := s.server.txMemPool.FetchTransaction(txHash) if err != nil { - rpcsLog.Errorf("Error fetching tx: %v", err) - return nil, btcjson.ErrNoTxInfo + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCNoTxInfo, + Message: "No information available about transaction", + } } mtx = tx.MsgTx() confirmations = 0 bestBlockSha = "" } else { - txList, err := s.server.db.FetchTxBySha(txSha) + txList, err := s.server.db.FetchTxBySha(txHash) if err != nil || len(txList) == 0 { - return nil, btcjson.ErrNoTxInfo + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCNoTxInfo, + Message: "No information available about transaction", + } } lastTx := txList[len(txList)-1] mtx = lastTx.Tx - blksha := lastTx.BlkSha + blkHash := lastTx.BlkSha txHeight := lastTx.Height dbSpentInfo = lastTx.TxSpent _, bestHeight, err := s.server.db.NewestSha() if err != nil { - rpcsLog.Errorf("Cannot get newest sha: %v", err) - return nil, btcjson.ErrBlockNotFound + context := "Failed to get newest hash" + return nil, internalRPCError(err.Error(), context) } confirmations = 1 + bestHeight - txHeight - bestBlockSha = blksha.String() + bestBlockSha = blkHash.String() } - if c.Output < 0 || c.Output > len(mtx.TxOut)-1 { - return nil, btcjson.ErrInvalidTxVout + if c.Vout > uint32(len(mtx.TxOut)-1) { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidTxVout, + Message: "Ouput index number (vout) does not exist " + + "for transaction.", + } } - txOut := mtx.TxOut[c.Output] + txOut := mtx.TxOut[c.Vout] if txOut == nil { - rpcsLog.Errorf("Output index: %d, for txid: %s does not exist.", c.Output, c.Txid) - return nil, btcjson.ErrInternal + errStr := fmt.Sprintf("Output index: %d for txid: %s does "+ + "not exist", c.Vout, c.Txid) + return nil, internalRPCError(errStr, "") } // To match the behavior of the reference client, this handler returns @@ -2120,7 +2157,7 @@ func handleGetTxOut(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (i // transaction already in the database. Unspent transaction outputs // from transactions in mempool, as well as mined transactions that are // spent by a mempool transaction, are not affected by this. - if dbSpentInfo != nil && dbSpentInfo[c.Output] { + if dbSpentInfo != nil && dbSpentInfo[c.Vout] { return nil, nil } @@ -2193,13 +2230,8 @@ func handleGetWorkRequest(s *rpcServer) (interface{}, error) { template, err := NewBlockTemplate(s.server.txMemPool, payToAddr) if err != nil { - errStr := fmt.Sprintf("Failed to create new block "+ - "template: %v", err) - rpcsLog.Error(errStr) - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: errStr, - } + context := "Failed to create new block template" + return nil, internalRPCError(err.Error(), context) } msgBlock = template.block @@ -2239,11 +2271,7 @@ func handleGetWorkRequest(s *rpcServer) (interface{}, error) { if err != nil { errStr := fmt.Sprintf("Failed to update extra nonce: "+ "%v", err) - rpcsLog.Warnf(errStr) - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: errStr, - } + return nil, internalRPCError(errStr, "") } rpcsLog.Debugf("Updated block template (timestamp %v, extra "+ @@ -2275,11 +2303,7 @@ func handleGetWorkRequest(s *rpcServer) (interface{}, error) { err := msgBlock.Header.Serialize(buf) if err != nil { errStr := fmt.Sprintf("Failed to serialize data: %v", err) - rpcsLog.Warnf(errStr) - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: errStr, - } + return nil, internalRPCError(errStr, "") } // Calculate the midstate for the block header. The midstate here is @@ -2345,16 +2369,12 @@ func handleGetWorkSubmission(s *rpcServer, hexData string) (interface{}, error) } data, err := hex.DecodeString(hexData) if err != nil { - return false, btcjson.Error{ - Code: btcjson.ErrDecodeHexString.Code, - Message: fmt.Sprintf("argument must be "+ - "hexadecimal string (not %q)", hexData), - } + return false, rpcDecodeHexError(hexData) } if len(data) != getworkDataLen { - return false, btcjson.Error{ - Code: btcjson.ErrInvalidParameter.Code, - Message: fmt.Sprintf("argument must be "+ + return false, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: fmt.Sprintf("Argument must be "+ "%d bytes (not %d)", getworkDataLen, len(data)), } @@ -2372,9 +2392,9 @@ func handleGetWorkSubmission(s *rpcServer, hexData string) (interface{}, error) bhBuf := bytes.NewReader(data[0:wire.MaxBlockHeaderPayload]) err = submittedHeader.Deserialize(bhBuf) if err != nil { - return false, btcjson.Error{ - Code: btcjson.ErrInvalidParameter.Code, - Message: fmt.Sprintf("argument does not "+ + return false, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: fmt.Sprintf("Argument does not "+ "contain a valid block header: %v", err), } } @@ -2406,11 +2426,9 @@ func handleGetWorkSubmission(s *rpcServer, hexData string) (interface{}, error) // Anything other than a rule violation is an unexpected error, // so return that error as an internal error. if _, ok := err.(blockchain.RuleError); !ok { - return false, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: fmt.Sprintf("Unexpected error while "+ - "checking proof of work: %v", err), - } + return false, internalRPCError("Unexpected error "+ + "while checking proof of work: "+err.Error(), + "") } rpcsLog.Debugf("Block submitted via getwork does not meet "+ @@ -2432,11 +2450,8 @@ func handleGetWorkSubmission(s *rpcServer, hexData string) (interface{}, error) // Anything other than a rule violation is an unexpected error, // so return that error as an internal error. if _, ok := err.(blockchain.RuleError); !ok { - return false, btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: fmt.Sprintf("Unexpected error while "+ - "processing block: %v", err), - } + return false, internalRPCError("Unexpected error "+ + "while processing block: "+err.Error(), "") } rpcsLog.Infof("Block submitted via getwork rejected: %v", err) @@ -2450,14 +2465,14 @@ func handleGetWorkSubmission(s *rpcServer, hexData string) (interface{}, error) } // handleGetWork implements the getwork command. -func handleGetWork(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleGetWork(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.GetWorkCmd) // Respond with an error if there are no addresses to pay the created // blocks to. if len(cfg.miningAddrs) == 0 { - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInternal.Code, Message: "No payment addresses specified via --miningaddr", } } @@ -2467,13 +2482,19 @@ func handleGetWork(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (in // However, allow this state when running in the regression test or // simulation test mode. if !(cfg.RegressionTest || cfg.SimNet) && s.server.ConnectedCount() == 0 { - return nil, btcjson.ErrClientNotConnected + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCClientNotConnected, + Message: "Bitcoin is not connected", + } } // No point in generating or accepting work before the chain is synced. _, currentHeight := s.server.blockManager.chainState.Best() if currentHeight != 0 && !s.server.blockManager.IsCurrent() { - return nil, btcjson.ErrClientInInitialDownload + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCClientInInitialDownload, + Message: "Bitcoin is downloading blocks...", + } } // Protect concurrent access from multiple RPC invocations for work @@ -2484,72 +2505,60 @@ func handleGetWork(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (in // When the caller provides data, it is a submission of a supposedly // solved block that needs to be checked and submitted to the network // if valid. - if c.Data != "" { - return handleGetWorkSubmission(s, c.Data) + if c.Data != nil && *c.Data != "" { + return handleGetWorkSubmission(s, *c.Data) } // No data was provided, so the caller is requesting work. return handleGetWorkRequest(s) } -var helpAddenda = map[string]string{ - "sendrawtransaction": ` -NOTE: btcd does not currently support the "allowhighfees" parameter.`, -} +// handleHelp implements the help command. +func handleHelp(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + c := cmd.(*btcjson.HelpCmd) -// getHelp text retreives help text from btcjson for the command in question. -// If there is any extra btcd specific information related to the given command -// then this is appended to the string. -func getHelpText(cmdName string) (string, error) { - help, err := btcjson.GetHelpString(cmdName) + // Provide a usage overview of all commands when no specific command + // was specified. + var command string + if c.Command != nil { + command = *c.Command + } + if command == "" { + usage, err := s.helpCacher.rpcUsage(false) + if err != nil { + context := "Failed to generate RPC usage" + return nil, internalRPCError(err.Error(), context) + } + return usage, nil + } + + // Check that the command asked for is supported and implemented. Only + // search the main list of handlers since help should not be provided + // for commands that are unimplemented or related to wallet + // functionality. + if _, ok := rpcHandlers[command]; !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: "Unknown command: " + command, + } + } + + // Get the help for the command. + help, err := s.helpCacher.rpcMethodHelp(command) if err != nil { - return "", err + context := "Failed to generate help" + return nil, internalRPCError(err.Error(), context) } - if helpAddendum, ok := helpAddenda[cmdName]; ok { - help += helpAddendum - } - return help, nil } -// handleHelp implements the help command. -func handleHelp(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { - help := cmd.(*btcjson.HelpCmd) - - // if no args we give a list of all known commands - if help.Command == "" { - commands := "" - first := true - // TODO(oga) this should have one liner usage for each command - // really, but for now just a list of commands is sufficient. - for k := range rpcHandlers { - if !first { - commands += "\n" - } - commands += k - first = false - } - return commands, nil - } - - // Check that we actually support the command asked for. We only - // search the main list of hanlders since we do not wish to provide help - // for commands that are unimplemented or relate to wallet - // functionality. - if _, ok := rpcHandlers[help.Command]; !ok { - return "", fmt.Errorf("help: unknown command: %s", help.Command) - } - - return getHelpText(help.Command) -} - // handlePing implements the ping command. -func handlePing(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handlePing(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { // Ask server to ping \o_ nonce, err := wire.RandomUint64() if err != nil { - return nil, fmt.Errorf("Not sending ping - can not generate "+ - "nonce: %v", err) + return nil, internalRPCError("Not sending ping - failed to "+ + "generate nonce: "+err.Error(), "") } s.server.BroadcastMessage(wire.NewMsgPing(nonce)) @@ -2557,18 +2566,18 @@ func handlePing(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (inter } // handleSearchRawTransaction implements the searchrawtransactions command. -func handleSearchRawTransactions(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleSearchRawTransactions(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { if !cfg.AddrIndex { - return nil, btcjson.Error{ - Code: btcjson.ErrMisc.Code, - Message: "addrindex is not currently enabled", + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: "Address index must be enabled (--addrindex)", } } if !s.server.addrIndexer.IsCaughtUp() { - return nil, btcjson.Error{ - Code: btcjson.ErrMisc.Code, - Message: "Address index has not yet caught up to the current " + - "best height", + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: "Address index has not yet caught up to the " + + "current best height", } } @@ -2577,10 +2586,9 @@ func handleSearchRawTransactions(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan // Attempt to decode the supplied address. addr, err := btcutil.DecodeAddress(c.Address, s.server.chainParams) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrInvalidAddressOrKey.Code, - Message: fmt.Sprintf("%s: %v", - btcjson.ErrInvalidAddressOrKey.Message, err), + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: "Invalid address or key: " + err.Error(), } } @@ -2595,14 +2603,27 @@ func handleSearchRawTransactions(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan } } - if len(addressTxs) >= c.Count { + var numRequested, numToSkip int + if c.Count != nil { + numRequested = *c.Count + if numRequested < 0 { + numRequested = 1 + } + } + if c.Skip != nil { + numToSkip = *c.Skip + if numToSkip < 0 { + numToSkip = 0 + } + } + if len(addressTxs) >= numRequested { // Tx's in the mempool exceed the requested number of tx's. // Slice off any possible overflow. - addressTxs = addressTxs[:c.Count] + addressTxs = addressTxs[:numRequested] } else { // Otherwise, we'll also take a look into the database. - dbTxs, err := s.server.db.FetchTxsForAddr(addr, c.Skip, - c.Count-len(addressTxs)) + dbTxs, err := s.server.db.FetchTxsForAddr(addr, numToSkip, + numRequested-len(addressTxs)) if err == nil && len(dbTxs) != 0 { for _, txReply := range dbTxs { addressTxs = append(addressTxs, txReply) @@ -2613,11 +2634,14 @@ func handleSearchRawTransactions(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan // If neither source yielded any results, then the address has never // been used. if len(addressTxs) == 0 { - return nil, btcjson.ErrNoTxInfo + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCNoTxInfo, + Message: "No information available about transaction", + } } // When not in verbose mode, simply return a list of serialized txs. - if c.Verbose == 0 { + if c.Verbose != nil && *c.Verbose == false { serializedTxs := make([]string, len(addressTxs), len(addressTxs)) for i, txReply := range addressTxs { serializedTxs[i], err = messageToHex(txReply.Tx) @@ -2625,21 +2649,20 @@ func handleSearchRawTransactions(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan return nil, err } } - return serializedTxs, nil } // Otherwise, we'll need to populate raw tx results. // Grab the current best height for tx confirmation calculation. - _, maxidx, err := s.server.db.NewestSha() + _, maxIdx, err := s.server.db.NewestSha() if err != nil { - rpcsLog.Errorf("Cannot get newest sha: %v", err) - return nil, btcjson.ErrBlockNotFound + context := "Failed to get newest hash" + return nil, internalRPCError(err.Error(), context) } rawTxns := make([]btcjson.TxRawResult, len(addressTxs), len(addressTxs)) for i, txReply := range addressTxs { - txSha := txReply.Sha.String() + txHash := txReply.Sha.String() mtx := txReply.Tx // Transactions grabbed from the mempool aren't yet @@ -2651,20 +2674,21 @@ func handleSearchRawTransactions(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan blk, err = s.server.db.FetchBlockBySha(txReply.BlkSha) if err != nil { rpcsLog.Errorf("Error fetching sha: %v", err) - return nil, btcjson.ErrBlockNotFound + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCBlockNotFound, + Message: "Block not found", + } } } - var blkSha *wire.ShaHash + var blkHash *wire.ShaHash if blk != nil { - blkSha, _ = blk.Sha() + blkHash, _ = blk.Sha() } rawTxn, err := createTxRawResult(s.server.chainParams, - txSha, mtx, blk, maxidx, blkSha) + txHash, mtx, blk, maxIdx, blkHash) if err != nil { - rpcsLog.Errorf("Cannot create TxRawResult for "+ - "transaction %s: %v", txSha, err) return nil, err } rawTxns[i] = *rawTxn @@ -2673,7 +2697,7 @@ func handleSearchRawTransactions(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan } // handleSendRawTransaction implements the sendrawtransaction command. -func handleSendRawTransaction(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleSendRawTransaction(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.SendRawTransactionCmd) // Deserialize and send off to tx relay hexStr := c.HexTx @@ -2682,20 +2706,15 @@ func handleSendRawTransaction(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan st } serializedTx, err := hex.DecodeString(hexStr) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrDecodeHexString.Code, - Message: fmt.Sprintf("argument must be hexadecimal "+ - "string (not %q)", hexStr), - } + return nil, rpcDecodeHexError(hexStr) } msgtx := wire.NewMsgTx() err = msgtx.Deserialize(bytes.NewReader(serializedTx)) if err != nil { - err := btcjson.Error{ - Code: btcjson.ErrDeserialization.Code, - Message: "TX decode failed", + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDeserialization, + Message: "TX decode failed: " + err.Error(), } - return nil, err } tx := btcutil.NewTx(msgtx) @@ -2714,15 +2733,14 @@ func handleSendRawTransaction(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan st rpcsLog.Errorf("Failed to process transaction %v: %v", tx.Sha(), err) } - err = btcjson.Error{ - Code: btcjson.ErrDeserialization.Code, - Message: fmt.Sprintf("TX rejected: %v", err), + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDeserialization, + Message: "TX rejected: " + err.Error(), } - return nil, err } - // We keep track of all the sendrawtransaction request txs so that we - // can rebroadcast them if they don't make their way into a block. + // Keep track of all the sendrawtransaction request txns so that they + // can be rebroadcast if they don't make their way into a block. iv := wire.NewInvVect(wire.InvTypeTx, tx.Sha()) s.server.AddRebroadcastInventory(iv, tx) @@ -2730,14 +2748,18 @@ func handleSendRawTransaction(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan st } // handleSetGenerate implements the setgenerate command. -func handleSetGenerate(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleSetGenerate(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.SetGenerateCmd) // Disable generation regardless of the provided generate flag if the // maximum number of threads (goroutines for our purposes) is 0. // Otherwise enable or disable it depending on the provided flag. generate := c.Generate - if c.GenProcLimit == 0 { + genProcLimit := -1 + if c.GenProcLimit != nil { + genProcLimit = *c.GenProcLimit + } + if genProcLimit == 0 { generate = false } @@ -2747,28 +2769,28 @@ func handleSetGenerate(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) // Respond with an error if there are no addresses to pay the // created blocks to. if len(cfg.miningAddrs) == 0 { - return nil, btcjson.Error{ - Code: btcjson.ErrInternal.Code, + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInternal.Code, Message: "No payment addresses specified " + "via --miningaddr", } } // It's safe to call start even if it's already started. - s.server.cpuMiner.SetNumWorkers(int32(c.GenProcLimit)) + s.server.cpuMiner.SetNumWorkers(int32(genProcLimit)) s.server.cpuMiner.Start() } return nil, nil } // handleStop implements the stop command. -func handleStop(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleStop(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { s.server.Stop() return "btcd stopping.", nil } // handleSubmitBlock implements the submitblock command. -func handleSubmitBlock(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleSubmitBlock(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.SubmitBlockCmd) // Deserialize the submitted block. @@ -2778,18 +2800,14 @@ func handleSubmitBlock(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) } serializedBlock, err := hex.DecodeString(hexStr) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrDecodeHexString.Code, - Message: fmt.Sprintf("argument must be hexadecimal "+ - "string (not %q)", hexStr), - } + return nil, rpcDecodeHexError(hexStr) } block, err := btcutil.NewBlockFromBytes(serializedBlock) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrDeserialization.Code, - Message: "Block decode failed", + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDeserialization, + Message: "Block decode failed: " + err.Error(), } } @@ -2871,32 +2889,39 @@ func verifyChain(db database.Db, level, depth int32, timeSource blockchain.Media } // handleVerifyChain implements the verifychain command. -func handleVerifyChain(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleVerifyChain(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.VerifyChainCmd) - err := verifyChain(s.server.db, c.CheckLevel, c.CheckDepth, + var checkLevel, checkDepth int32 + if c.CheckLevel != nil { + checkLevel = *c.CheckLevel + } + if c.CheckDepth != nil { + checkDepth = *c.CheckDepth + } + + err := verifyChain(s.server.db, checkLevel, checkDepth, s.server.timeSource) return err == nil, nil } // handleVerifyMessage implements the verifymessage command. -func handleVerifyMessage(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{}) (interface{}, error) { +func handleVerifyMessage(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.VerifyMessageCmd) // Decode the provided address. addr, err := btcutil.DecodeAddress(c.Address, activeNetParams.Params) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrInvalidAddressOrKey.Code, - Message: fmt.Sprintf("%s: %v", - btcjson.ErrInvalidAddressOrKey.Message, err), + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: "Invalid address or key: " + err.Error(), } } // Only P2PKH addresses are valid for signing. if _, ok := addr.(*btcutil.AddressPubKeyHash); !ok { - return nil, btcjson.Error{ - Code: btcjson.ErrType.Code, + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCType, Message: "Address is not a pay-to-pubkey-hash address", } } @@ -2904,9 +2929,9 @@ func handleVerifyMessage(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{ // Decode base64 signature. sig, err := base64.StdEncoding.DecodeString(c.Signature) if err != nil { - return nil, btcjson.Error{ - Code: btcjson.ErrParse.Code, - Message: fmt.Sprintf("Malformed base64 encoding: %v", err), + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCParse.Code, + Message: "Malformed base64 encoding: " + err.Error(), } } @@ -2915,8 +2940,8 @@ func handleVerifyMessage(s *rpcServer, cmd btcjson.Cmd, closeChan <-chan struct{ pk, wasCompressed, err := btcec.RecoverCompact(btcec.S256(), sig, wire.DoubleSha256([]byte("Bitcoin Signed Message:\n"+c.Message))) if err != nil { - // Mirror Bitcoin Core behavior, which treats error in RecoverCompact as - // invalid signature. + // Mirror Bitcoin Core behavior, which treats error in + // RecoverCompact as invalid signature. return false, nil } @@ -2955,6 +2980,7 @@ type rpcServer struct { listeners []net.Listener workState *workState gbtWorkState *gbtWorkState + helpCacher *helpCacher quit chan int } @@ -3100,77 +3126,102 @@ func (s *rpcServer) checkAuth(r *http.Request, require bool) (bool, error) { return true, nil } -// standardCmdReply checks that a parsed command is a standard -// Bitcoin JSON-RPC command and runs the proper handler to reply to the -// command. -func (s *rpcServer) standardCmdReply(cmd btcjson.Cmd, closeChan <-chan struct{}) (reply btcjson.Reply) { - id := cmd.Id() - reply.Id = &id +// parsedRPCCmd represents a JSON-RPC request object that has been parsed into +// 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 +} - handler, ok := rpcHandlers[cmd.Method()] +// standardCmdResult checks that a parsed command is a standard Bitcoin JSON-RPC +// command and runs the appropriate handler to reply to the command. Any +// commands which are not recognized or not implemented will return an error +// suitable for use in replies. +func (s *rpcServer) standardCmdResult(cmd *parsedRPCCmd, closeChan <-chan struct{}) (interface{}, error) { + handler, ok := rpcHandlers[cmd.method] if ok { goto handled } - _, ok = rpcAskWallet[cmd.Method()] + _, ok = rpcAskWallet[cmd.method] if ok { handler = handleAskWallet goto handled } - _, ok = rpcUnimplemented[cmd.Method()] + _, ok = rpcUnimplemented[cmd.method] if ok { handler = handleUnimplemented goto handled } - reply.Error = &btcjson.ErrMethodNotFound - return reply + return nil, btcjson.ErrRPCMethodNotFound handled: - result, err := handler(s, cmd, closeChan) - if err != nil { - jsonErr, ok := err.(btcjson.Error) - if !ok { - // In the case where we did not have a btcjson - // error to begin with, make a new one to send, - // but this really should not happen. - jsonErr = btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: err.Error(), - } - } - reply.Error = &jsonErr - } else { - reply.Result = result - } - return reply + return handler(s, cmd.cmd, closeChan) } -// parseCmd parses a marshaled known command, returning any errors as a -// btcjson.Error that can be used in replies. The returned cmd may still -// be non-nil if b is at least a valid marshaled JSON-RPC message. -func parseCmd(b []byte) (btcjson.Cmd, *btcjson.Error) { - cmd, err := btcjson.ParseMarshaledCmd(b) +// parseCmd parses a JSON-RPC request object into known concrete command. The +// err field of the returned parsedRPCCmd struct will contain an RPC error that +// 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 + + cmd, err := btcjson.UnmarshalCmd(request) if err != nil { - jsonErr, ok := err.(btcjson.Error) - if !ok { - jsonErr = btcjson.Error{ - Code: btcjson.ErrParse.Code, - Message: err.Error(), - } + // When the error is because the method is not registered, + // produce a method not found RPC error. + if jerr, ok := err.(btcjson.Error); ok && + jerr.ErrorCode == btcjson.ErrUnregisteredMethod { + + parsedCmd.err = btcjson.ErrRPCMethodNotFound + return &parsedCmd } - return cmd, &jsonErr + + // Otherwise, some type of invalid parameters is the + // cause, so produce the equivalent RPC error. + parsedCmd.err = btcjson.NewRPCError( + btcjson.ErrRPCInvalidParams.Code, err.Error()) + return &parsedCmd } - return cmd, nil + + parsedCmd.cmd = cmd + return &parsedCmd } -// jsonRPCRead is the RPC wrapper around the jsonRead function to handle reading -// and responding to RPC messages. +// 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) { + var jsonErr *btcjson.RPCError + if replyErr != nil { + if jErr, ok := replyErr.(*btcjson.RPCError); ok { + jsonErr = jErr + } else { + jsonErr = internalRPCError(replyErr.Error(), "") + } + } + + return btcjson.MarshalResponse(id, result, jsonErr) +} + +// jsonRPCRead handles reading and responding to RPC messages. func (s *rpcServer) jsonRPCRead(w http.ResponseWriter, r *http.Request) { if atomic.LoadInt32(&s.shutdown) != 0 { return } - body, err := btcjson.GetRaw(r.Body) + + // Read and close the JSON-RPC request body from the caller. + body, err := ioutil.ReadAll(r.Body) + r.Body.Close() if err != nil { - rpcsLog.Errorf("Error getting json message: %v", err) + errMsg := fmt.Sprintf("error reading JSON message: %v", err) + errCode := http.StatusBadRequest + http.Error(w, strconv.FormatInt(int64(errCode), 10)+" "+errMsg, + errCode) return } @@ -3201,17 +3252,28 @@ func (s *rpcServer) jsonRPCRead(w http.ResponseWriter, r *http.Request) { defer buf.Flush() conn.SetReadDeadline(timeZeroVal) - var reply btcjson.Reply - cmd, jsonErr := parseCmd(body) - if cmd != nil { - // Unmarshaling at least a valid JSON-RPC message succeeded. - // Use the provided id for errors. - id := cmd.Id() - reply.Id = &id + // 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(), + } } - if jsonErr != nil { - reply.Error = jsonErr - } else { + if jsonErr == nil { + // Requests with no ID (notifications) must not have a response + // per the JSON-RPC spec. + if request.ID == nil { + return + } + + // The parse was at least successful enough to have an ID so + // set it for the response. + responseID = request.ID + // Setup a close notifier. Since the connection is hijacked, // the CloseNotifer on the ResponseWriter is not available. closeChan := make(chan struct{}, 1) @@ -3222,23 +3284,33 @@ func (s *rpcServer) jsonRPCRead(w http.ResponseWriter, r *http.Request) { } }() - reply = s.standardCmdReply(cmd, closeChan) + // 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) + } } - rpcsLog.Tracef("reply: %v", reply) + // Marshal the response. + msg, err := createMarshalledReply(responseID, result, jsonErr) + if err != nil { + rpcsLog.Errorf("Failed to marshal reply: %v", err) + return + } + // Write the response. err = s.writeHTTPResponseHeaders(r, w.Header(), http.StatusOK, buf) if err != nil { rpcsLog.Error(err) return } - - msg, err := btcjson.MarshallAndSend(reply, buf) - if err != nil { - rpcsLog.Error(err) - return + if _, err := buf.Write(msg); err != nil { + rpcsLog.Errorf("Failed to write marshalled reply: %v", err) } - rpcsLog.Tracef(msg) } // jsonAuthFail sends a message back to the client if the http auth is rejected. @@ -3279,6 +3351,8 @@ func (s *rpcServer) Start() { jsonAuthFail(w) return } + + // Read and respond to the request. s.jsonRPCRead(w, r) }) @@ -3351,6 +3425,7 @@ func newRPCServer(listenAddrs []string, s *server) (*rpcServer, error) { statusLines: make(map[int]string), workState: newWorkState(), gbtWorkState: newGbtWorkState(s.timeSource), + helpCacher: newHelpCacher(), quit: make(chan int), } rpc.ntfnMgr = newWsNotificationManager(&rpc) diff --git a/rpcserverhelp.go b/rpcserverhelp.go new file mode 100644 index 00000000..1e5694b7 --- /dev/null +++ b/rpcserverhelp.go @@ -0,0 +1,621 @@ +// Copyright (c) 2015 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import ( + "errors" + "sort" + "strings" + "sync" + + "github.com/btcsuite/btcd/btcjson/v2/btcjson" +) + +// helpDescsEnUS defines the English descriptions used for the help strings. +var helpDescsEnUS = map[string]string{ + // DebugLevelCmd help. + "debuglevel--synopsis": "Dynamically changes the debug logging level.\n" + + "The levelspec can either a debug level or of the form:\n" + + "=,=,...\n" + + "The valid debug levels are trace, debug, info, warn, error, and critical.\n" + + "The valid subsystems are AMGR, ADXR, BCDB, BMGR, BTCD, CHAN, DISC, PEER, RPCS, SCRP, SRVR, and TXMP.\n" + + "Finally the keyword 'show' will return a list of the available subsystems.", + "debuglevel-levelspec": "The debug level(s) to use or the keyword 'show'", + "debuglevel--condition0": "levelspec!=show", + "debuglevel--condition1": "levelspec=show", + "debuglevel--result0": "The string 'Done.'", + "debuglevel--result1": "The list of subsystems", + + // AddNodeCmd help. + "addnode--synopsis": "Attempts to add or remove a persistent peer.", + "addnode-addr": "IP address and port of the peer to operate on", + "addnode-subcmd": "'add' to add a persistent peer, 'remove' to remove a persistent peer, or 'onetry' to try a single connection to a peer", + + // TransactionInput help. + "transactioninput-txid": "The hash of the input transaction", + "transactioninput-vout": "The specific output of the input transaction to redeem", + + // CreateRawTransactionCmd help. + "createrawtransaction--synopsis": "Returns a new transaction spending the provided inputs and sending to the provided addresses.\n" + + "The transaction inputs are not signed in the created transaction.\n" + + "The signrawtransaction RPC command provided by wallet must be used to sign the resulting transaction.", + "createrawtransaction-inputs": "The inputs to the transaction", + "createrawtransaction-amounts": "JSON object with the destination addresses as keys and amounts as values", + "createrawtransaction-amounts--key": "address", + "createrawtransaction-amounts--value": "n.nnn", + "createrawtransaction-amounts--desc": "The destination address as the key and the amount in BTC as the value", + "createrawtransaction--result0": "Hex-encoded bytes of the serialized transaction", + + // ScriptSig help. + "scriptsig-asm": "Disassembly of the script", + "scriptsig-hex": "Hex-encoded bytes of the script", + + // Vin help. + "vin-coinbase": "The hex-encoded bytes of the signature script (coinbase txns only)", + "vin-txid": "The hash of the origin transaction (non-coinbase txns only)", + "vin-vout": "The index of the output being redeemed from the origin transaction (non-coinbase txns only)", + "vin-scriptSig": "The signature script used to redeem the origin transaction as a JSON object (non-coinbase txns only)", + "vin-sequence": "The script sequence number", + + // ScriptPubKeyResult help. + "scriptpubkeyresult-asm": "Disassembly of the script", + "scriptpubkeyresult-hex": "Hex-encoded bytes of the script", + "scriptpubkeyresult-reqSigs": "The number of required signatures", + "scriptpubkeyresult-type": "The type of the script (e.g. 'pubkeyhash')", + "scriptpubkeyresult-addresses": "The bitcoin addresses associated with this script", + + // Vout help. + "vout-value": "The amount in BTC", + "vout-n": "The index of this transaction output", + "vout-scriptPubKey": "The public key script used to pay coins as a JSON object", + + // TxRawDecodeResult help. + "txrawdecoderesult-txid": "The hash of the transaction", + "txrawdecoderesult-version": "The transaction version", + "txrawdecoderesult-locktime": "The transaction lock time", + "txrawdecoderesult-vin": "The transaction inputs as JSON objects", + "txrawdecoderesult-vout": "The transaction outputs as JSON objects", + + // DecodeRawTransactionCmd help. + "decoderawtransaction--synopsis": "Returns a JSON object representing the provided serialized, hex-encoded transaction.", + "decoderawtransaction-hextx": "Serialized, hex-encoded transaction", + + // DecodeScriptResult help. + "decodescriptresult-asm": "Disassembly of the script", + "decodescriptresult-reqSigs": "The number of required signatures", + "decodescriptresult-type": "The type of the script (e.g. 'pubkeyhash')", + "decodescriptresult-addresses": "The bitcoin addresses associated with this script", + "decodescriptresult-p2sh": "The script hash for use in pay-to-script-hash transactions", + + // DecodeScriptCmd help. + "decodescript--synopsis": "Returns a JSON object with information about the provided hex-encoded script.", + "decodescript-hexscript": "Hex-encoded script", + + // GetAddedNodeInfoResultAddr help. + "getaddednodeinforesultaddr-address": "The ip address for this DNS entry", + "getaddednodeinforesultaddr-connected": "The connection 'direction' (inbound/outbound/false)", + + // GetAddedNodeInfoResult help. + "getaddednodeinforesult-addednode": "The ip address or domain of the added peer", + "getaddednodeinforesult-connected": "Whether or not the peer is currently connected", + "getaddednodeinforesult-addresses": "DNS lookup and connection information about the peer", + + // GetAddedNodeInfo help. + "getaddednodeinfo--synopsis": "Returns information about manually added (persistent) peers.", + "getaddednodeinfo-dns": "Specifies whether the returned data is a JSON object including DNS and connection information, or just a list of added peers", + "getaddednodeinfo-node": "Only return information about this specific peer instead of all added peers", + "getaddednodeinfo--condition0": "dns=false", + "getaddednodeinfo--condition1": "dns=true", + "getaddednodeinfo--result0": "List of added peers", + + // GetBestBlockResult help. + "getbestblockresult-hash": "Hex-encoded bytes of the best block hash", + "getbestblockresult-height": "Height of the best block", + + // GetBestBlockCmd help. + "getbestblock--synopsis": "Get block height and hash of best block in the main chain.", + "getbestblock--result0": "Get block height and hash of best block in the main chain.", + + // GetBestBlockHashCmd help. + "getbestblockhash--synopsis": "Returns the hash of the of the best (most recent) block in the longest block chain.", + "getbestblockhash--result0": "The hex-encoded block hash", + + // GetBlockCmd help. + "getblock--synopsis": "Returns information about a block given its hash.", + "getblock-hash": "The hash of the block", + "getblock-verbose": "Specifies the block is returned as a JSON object instead of hex-encoded string", + "getblock-verbosetx": "Specifies that each transaction is returned as a JSON object and only applies if the verbose flag is true (btcd extension)", + "getblock--condition0": "verbose=false", + "getblock--condition1": "verbose=true", + "getblock--result0": "Hex-encoded bytes of the serialized block", + + // TxRawResult help. + "txrawresult-hex": "Hex-encoded transaction", + "txrawresult-txid": "The hash of the transaction", + "txrawresult-version": "The transaction version", + "txrawresult-locktime": "The transaction lock time", + "txrawresult-vin": "The transaction inputs as JSON objects", + "txrawresult-vout": "The transaction outputs as JSON objects", + "txrawresult-blockhash": "Hash of the block the transaction is part of", + "txrawresult-confirmations": "Number of confirmations of the block", + "txrawresult-time": "Transaction time in seconds since 1 Jan 1970 GMT", + "txrawresult-blocktime": "Block time in seconds since the 1 Jan 1970 GMT", + + // GetBlockVerboseResult help. + "getblockverboseresult-hash": "The hash of the block (same as provided)", + "getblockverboseresult-confirmations": "The number of confirmations", + "getblockverboseresult-size": "The size of the block", + "getblockverboseresult-height": "The height of the block in the block chain", + "getblockverboseresult-version": "The block version", + "getblockverboseresult-merkleroot": "Root hash of the merkle tree", + "getblockverboseresult-tx": "The transaction hashes (only when verbosetx=false)", + "getblockverboseresult-rawtx": "The transactions as JSON objects (only when verbosetx=true)", + "getblockverboseresult-time": "The block time in seconds since 1 Jan 1970 GMT", + "getblockverboseresult-nonce": "The block nonce", + "getblockverboseresult-bits": "The bits which represent the block difficulty", + "getblockverboseresult-difficulty": "The proof-of-work difficulty as a multiple of the minimum difficulty", + "getblockverboseresult-previousblockhash": "The hash of the previous block", + "getblockverboseresult-nextblockhash": "The hash of the next block", + + // GetBlockCountCmd help. + "getblockcount--synopsis": "Returns the number of blocks in the longest block chain.", + "getblockcount--result0": "The current block count", + + // GetBlockHashCmd help. + "getblockhash--synopsis": "Returns hash of the block in best block chain at the given height.", + "getblockhash-index": "The block height", + "getblockhash--result0": "The block hash", + + // TemplateRequest help. + "templaterequest-mode": "This is 'template', 'proposal', or omitted", + "templaterequest-capabilities": "List of capabilities", + "templaterequest-longpollid": "The long poll ID of a job to monitor for expiration; required and valid only for long poll requests ", + "templaterequest-sigoplimit": "Number of signature operations allowed in blocks (this parameter is ignored)", + "templaterequest-sizelimit": "Number of bytes allowed in blocks (this parameter is ignored)", + "templaterequest-maxversion": "Highest supported block version number (this parameter is ignored)", + "templaterequest-target": "The desired target for the block template (this parameter is ignored)", + "templaterequest-data": "Hex-encoded block data (only for mode=proposal)", + "templaterequest-workid": "The server provided workid if provided in block template (not applicable)", + + // GetBlockTemplateResultTx help. + "getblocktemplateresulttx-data": "Hex-encoded transaction data (byte-for-byte)", + "getblocktemplateresulttx-hash": "Hex-encoded transaction hash (little endian if treated as a 256-bit number)", + "getblocktemplateresulttx-depends": "Other transactions before this one (by 1-based index in the 'transactions' list) that must be present in the final block if this one is", + "getblocktemplateresulttx-fee": "Difference in value between transaction inputs and outputs (in Satoshi)", + "getblocktemplateresulttx-sigops": "Total number of signature operations as counted for purposes of block limits", + + // GetBlockTemplateResultAux help. + "getblocktemplateresultaux-flags": "Hex-encoded byte-for-byte data to include in the coinbase signature script", + + // GetBlockTemplateResult help. + "getblocktemplateresult-bits": "Hex-encoded compressed difficulty", + "getblocktemplateresult-curtime": "Current time as seen by the server (recommended for block time); must fall within mintime/maxtime rules", + "getblocktemplateresult-height": "Height of the block to be solved", + "getblocktemplateresult-previousblockhash": "Hex-encoded big-endian hash of the previous block", + "getblocktemplateresult-sigoplimit": "Number of sigops allowed in blocks ", + "getblocktemplateresult-sizelimit": "Number of bytes allowed in blocks", + "getblocktemplateresult-transactions": "Array of transactions as JSON objects", + "getblocktemplateresult-version": "The block version", + "getblocktemplateresult-coinbaseaux": "Data that should be included in the coinbase signature script", + "getblocktemplateresult-coinbasetxn": "Information about the coinbase transaction", + "getblocktemplateresult-coinbasevalue": "Total amount available for the coinbase in Satoshi", + "getblocktemplateresult-workid": "This value must be returned with result if provided (not provided)", + "getblocktemplateresult-longpollid": "Identifier for long poll request which allows monitoring for expiration", + "getblocktemplateresult-longpolluri": "An alternate URI to use for long poll requests if provided (not provided)", + "getblocktemplateresult-submitold": "Not applicable", + "getblocktemplateresult-target": "Hex-encoded big-endian number which valid results must be less than", + "getblocktemplateresult-expires": "Maximum number of seconds (starting from when the server sent the response) this work is valid for", + "getblocktemplateresult-maxtime": "Maximum allowed time", + "getblocktemplateresult-mintime": "Minimum allowed time", + "getblocktemplateresult-mutable": "List of mutations the server explicitly allows", + "getblocktemplateresult-noncerange": "Two concatenated hex-encoded big-endian 32-bit integers which represent the valid ranges of nonces the miner may scan", + "getblocktemplateresult-capabilities": "List of server capabilities including 'proposal' to indicate support for block proposals", + "getblocktemplateresult-reject-reason": "Reason the proposal was invalid as-is (only applies to proposal responses)", + + // GetBlockTemplateCmd help. + "getblocktemplate--synopsis": "Returns a JSON object with information necessary to construct a block to mine or accepts a proposal to validate.\n" + + "See BIP0022 and BIP0023 for the full specification.", + "getblocktemplate-request": "Request object which controls the mode and several parameters", + "getblocktemplate--condition0": "mode=template", + "getblocktemplate--condition1": "mode=proposal, rejected", + "getblocktemplate--condition2": "mode=proposal, accepted", + "getblocktemplate--result1": "An error string which represents why the proposal was rejected or nothing if accepted", + + // GetConnectionCountCmd help. + "getconnectioncount--synopsis": "Returns the number of active connections to other peers.", + "getconnectioncount--result0": "The number of connections", + + // GetCurrentNetCmd help. + "getcurrentnet--synopsis": "Get bitcoin network the server is running on.", + "getcurrentnet--result0": "The network identifer", + + // GetDifficultyCmd help. + "getdifficulty--synopsis": "Returns the proof-of-work difficulty as a multiple of the minimum difficulty.", + "getdifficulty--result0": "The difficulty", + + // GetGenerateCmd help. + "getgenerate--synopsis": "Returns if the server is set to generate coins (mine) or not.", + "getgenerate--result0": "True if mining, false if not", + + // GetHashesPerSecCmd help. + "gethashespersec--synopsis": "Returns a recent hashes per second performance measurement while generating coins (mining).", + "gethashespersec--result0": "The number of hashes per second", + + // InfoChainResult help. + "infochainresult-version": "The version of the server", + "infochainresult-protocolversion": "The latest supported protocol version", + "infochainresult-blocks": "The number of blocks processed", + "infochainresult-timeoffset": "The time offset", + "infochainresult-connections": "The number of connected peers", + "infochainresult-proxy": "The proxy used by the server", + "infochainresult-difficulty": "The current target difficulty", + "infochainresult-testnet": "Whether or not server is using testnet", + "infochainresult-relayfee": "The minimum relay fee for non-free transactions in BTC/KB", + "infochainresult-errors": "Any current errors", + + // InfoWalletResult help. + "infowalletresult-version": "The version of the server", + "infowalletresult-protocolversion": "The latest supported protocol version", + "infowalletresult-walletversion": "The version of the wallet server", + "infowalletresult-balance": "The total bitcoin balance of the wallet", + "infowalletresult-blocks": "The number of blocks processed", + "infowalletresult-timeoffset": "The time offset", + "infowalletresult-connections": "The number of connected peers", + "infowalletresult-proxy": "The proxy used by the server", + "infowalletresult-difficulty": "The current target difficulty", + "infowalletresult-testnet": "Whether or not server is using testnet", + "infowalletresult-keypoololdest": "Seconds since 1 Jan 1970 GMT of the oldest pre-generated key in the key pool", + "infowalletresult-keypoolsize": "The number of new keys that are pre-generated", + "infowalletresult-unlocked_until": "The timestamp in seconds since 1 Jan 1970 GMT that the wallet is unlocked for transfers, or 0 if the wallet is locked", + "infowalletresult-paytxfee": "The transaction fee set in BTC/KB", + "infowalletresult-relayfee": "The minimum relay fee for non-free transactions in BTC/KB", + "infowalletresult-errors": "Any current errors", + + // GetInfoCmd help. + "getinfo--synopsis": "Returns a JSON object containing various state info.", + + // GetMiningInfoResult help. + "getmininginforesult-blocks": "Height of the latest best block", + "getmininginforesult-currentblocksize": "Size of the latest best block", + "getmininginforesult-currentblocktx": "Number of transactions in the latest best block", + "getmininginforesult-difficulty": "Current target difficulty", + "getmininginforesult-errors": "Any current errors", + "getmininginforesult-generate": "Whether or not server is set to generate coins", + "getmininginforesult-genproclimit": "Number of processors to use for coin generation (-1 when disabled)", + "getmininginforesult-hashespersec": "Recent hashes per second performance measurement while generating coins", + "getmininginforesult-networkhashps": "Estimated network hashes per second for the most recent blocks", + "getmininginforesult-pooledtx": "Number of transactions in the memory pool", + "getmininginforesult-testnet": "Whether or not server is using testnet", + + // GetMiningInfoCmd help. + "getmininginfo--synopsis": "Returns a JSON object containing mining-related information.", + + // GetNetworkHashPSCmd help. + "getnetworkhashps--synopsis": "Returns the estimated network hashes per second for the block heights provided by the parameters.", + "getnetworkhashps-blocks": "The number of blocks, or -1 for blocks since last difficulty change", + "getnetworkhashps-height": "Perform estimate ending with this height or -1 for current best chain block height", + "getnetworkhashps--result0": "Estimated hashes per second", + + // GetNetTotalsCmd help. + "getnettotals--synopsis": "Returns a JSON object containing network traffic statistics.", + + // GetNetTotalsResult help. + "getnettotalsresult-totalbytesrecv": "Total bytes received", + "getnettotalsresult-totalbytessent": "Total bytes sent", + "getnettotalsresult-timemillis": "Number of milliseconds since 1 Jan 1970 GMT", + + // GetPeerInfoResult help. + "getpeerinforesult-addr": "The ip address and port of the peer", + "getpeerinforesult-addrlocal": "Local address", + "getpeerinforesult-services": "Services bitmask which represents the services supported by the peer", + "getpeerinforesult-lastsend": "Time the last message was received in seconds since 1 Jan 1970 GMT", + "getpeerinforesult-lastrecv": "Time the last message was sent in seconds since 1 Jan 1970 GMT", + "getpeerinforesult-bytessent": "Total bytes sent", + "getpeerinforesult-bytesrecv": "Total bytes received", + "getpeerinforesult-conntime": "Time the connection was made in seconds since 1 Jan 1970 GMT", + "getpeerinforesult-pingtime": "Number of microseconds the last ping took", + "getpeerinforesult-pingwait": "Number of microseconds a queued ping has been waiting for a response", + "getpeerinforesult-version": "The protocol version of the peer", + "getpeerinforesult-subver": "The user agent of the peer", + "getpeerinforesult-inbound": "Whether or not the peer is an inbound connection", + "getpeerinforesult-startingheight": "The latest block height the peer knew about when the connection was established", + "getpeerinforesult-currentheight": "The current height of the peer", + "getpeerinforesult-banscore": "The ban score", + "getpeerinforesult-syncnode": "Whether or not the peer is the sync peer", + + // GetPeerInfoCmd help. + "getpeerinfo--synopsis": "Returns data about each connected network peer as an array of json objects.", + + // GetRawMempoolVerboseResult help. + "getrawmempoolverboseresult-size": "Transaction size in bytes", + "getrawmempoolverboseresult-fee": "Transaction fee in bitcoins", + "getrawmempoolverboseresult-time": "Local time transaction entered pool in seconds since 1 Jan 1970 GMT", + "getrawmempoolverboseresult-height": "Block height when transaction entered the pool", + "getrawmempoolverboseresult-startingpriority": "Priority when transaction entered the pool", + "getrawmempoolverboseresult-currentpriority": "Current priority", + "getrawmempoolverboseresult-depends": "Unconfirmed transactions used as inputs for this transaction", + + // GetRawMempoolCmd help. + "getrawmempool--synopsis": "Returns information about all of the transactions currently in the memory pool.", + "getrawmempool-verbose": "Returns JSON object when true or an array of transaction hashes when false", + "getrawmempool--condition0": "verbose=false", + "getrawmempool--condition1": "verbose=true", + "getrawmempool--result0": "Array of transaction hashes", + + // GetRawTransactionCmd help. + "getrawtransaction--synopsis": "Returns information about a transaction given its hash.", + "getrawtransaction-txid": "The hash of the transaction", + "getrawtransaction-verbose": "Specifies the transaction is returned as a JSON object instead of a hex-encoded string", + "getrawtransaction--condition0": "verbose=false", + "getrawtransaction--condition1": "verbose=true", + "getrawtransaction--result0": "Hex-encoded bytes of the serialized transaction", + + // GetTxOutResult help. + "gettxoutresult-bestblock": "The block hash that contains the transaction output", + "gettxoutresult-confirmations": "The number of confirmations", + "gettxoutresult-value": "The transaction amount in BTC", + "gettxoutresult-scriptPubKey": "The public key script used to pay coins as a JSON object", + "gettxoutresult-version": "The transaction version", + "gettxoutresult-coinbase": "Whether or not the transaction is a coinbase", + + // GetTxOutCmd help. + "gettxout--synopsis": "Returns information about an unspent transaction output..", + "gettxout-txid": "The hash of the transaction", + "gettxout-vout": "The index of the output", + "gettxout-includemempool": "Include the mempool when true", + + // GetWorkResult help. + "getworkresult-data": "Hex-encoded block data", + "getworkresult-hash1": "(DEPRECATED) Hex-encoded formatted hash buffer", + "getworkresult-midstate": "(DEPRECATED) Hex-encoded precomputed hash state after hashing first half of the data", + "getworkresult-target": "Hex-encoded little-endian hash target", + + // GetWorkCmd help. + "getwork--synopsis": "(DEPRECATED - Use getblocktemplate instead) Returns formatted hash data to work on or checks and submits solved data.", + "getwork-data": "Hex-encoded data to check", + "getwork--condition0": "no data provided", + "getwork--condition1": "data provided", + "getwork--result1": "Whether or not the solved data is valid and was added to the chain", + + // HelpCmd help. + "help--synopsis": "Returns a list of all commands or help for a specified command.", + "help-command": "The command to retrieve help for", + "help--condition0": "no command provided", + "help--condition1": "command specified", + "help--result0": "List of commands", + "help--result1": "Help for specified command", + + // PingCmd help. + "ping--synopsis": "Queues a ping to be sent to each connected peer.\n" + + "Ping times are provided by getpeerinfo via the pingtime and pingwait fields.", + + // SearchRawTransactionsCmd help. + "searchrawtransactions--synopsis": "Returns raw data for transactions involving the passed address.\n" + + "Returned transactions are pulled from both the database, and transactions currently in the mempool.\n" + + "Transactions pulled from the mempool will have the 'confirmations' field set to 0.\n" + + "Usage of this RPC requires the optional --addrindex flag to be activated, otherwise all responses will simply return with an error stating the address index has not yet been built.\n" + + "Similarly, until the address index has caught up with the current best height, all requests will return an error response in order to avoid serving stale data.", + "searchrawtransactions-address": "The Bitcoin address to search for", + "searchrawtransactions-verbose": "Specifies the transaction is returned as a JSON object instead of hex-encoded string", + "searchrawtransactions-skip": "The number of leading transactions to leave out of the final response", + "searchrawtransactions-count": "The maximum number of transactions to return", + "searchrawtransactions--condition0": "verbose=0", + "searchrawtransactions--condition1": "verbose=1", + "searchrawtransactions--result0": "Hex-encoded serialized transaction", + + // SendRawTransactionCmd help. + "sendrawtransaction--synopsis": "Submits the serialized, hex-encoded transaction to the local peer and relays it to the network.", + "sendrawtransaction-hextx": "Serialized, hex-encoded signed transaction", + "sendrawtransaction-allowhighfees": "Whether or not to allow insanely high fees (btcd does not yet implement this parameter, so it has no effect)", + "sendrawtransaction--result0": "The hash of the transaction", + + // SetGenerateCmd help. + "setgenerate--synopsis": "Set the server to generate coins (mine) or not.", + "setgenerate-generate": "Use true to enable generation, false to disable it", + "setgenerate-genproclimit": "The number of processors (cores) to limit generation to or -1 for default", + + // StopCmd help. + "stop--synopsis": "Shutdown btcd.", + "stop--result0": "The string 'btcd stopping.'", + + // SubmitBlockOptions help. + "submitblockoptions-workid": "This parameter is currently ignored", + + // SubmitBlockCmd help. + "submitblock--synopsis": "Attempts to submit a new serialized, hex-encoded block to the network.", + "submitblock-hexblock": "Serialized, hex-encoded block", + "submitblock-options": "This parameter is currently ignored", + "submitblock--condition0": "Block successfully submitted", + "submitblock--condition1": "Block rejected", + "submitblock--result1": "The reason the block was rejected", + + // ValidateAddressResult help. + "validateaddresschainresult-isvalid": "Whether or not the address is valid", + "validateaddresschainresult-address": "The bitcoin address (only when isvalid is true)", + + // ValidateAddressCmd help. + "validateaddress--synopsis": "Verify an address is valid.", + "validateaddress-address": "Bitcoin address to validate", + + // VerifyChainCmd help. + "verifychain--synopsis": "Verifies the block chain database.\n" + + "The actual checks performed by the checklevel parameter are implementation specific.\n" + + "For btcd this is:\n" + + "checklevel=0 - Look up each block and ensure it can be loaded from the database.\n" + + "checklevel=1 - Perform basic context-free sanity checks on each block.", + "verifychain-checklevel": "How thorough the block verification is", + "verifychain-checkdepth": "The number of blocks to check", + "verifychain--result0": "Whether or not the chain verified", + + // VerifyMessageCmd help. + "verifymessage--synopsis": "Verify a signed message.", + "verifymessage-address": "The bitcoin address to use for the signature", + "verifymessage-signature": "The base-64 encoded signature provided by the signer", + "verifymessage-message": "The signed message", + "verifymessage--result0": "Whether or not the signature verified", + + // -------- Websocket-specific help -------- + + // NotifyBlocksCmd help. + "notifyblocks--synopsis": "Request notifications for whenever a block is connected or disconnected from the main (best) chain.", + + // NotifyNewTransactionsCmd help. + "notifynewtransactions--synopsis": "Send either a txaccepted or a txacceptedverbose notification when a new transaction is accepted into the mempool.", + "notifynewtransactions-verbose": "Specifies which type of notification to receive. If verbose is true, then the caller receives txacceptedverbose, otherwise the caller receives txaccepted", + + // NotifyReceivedCmd help. + "notifyreceived--synopsis": "Send a recvtx notification when a transaction added to mempool or appears in a newly-attached block contains a txout pkScript sending to any of the passed addresses.\n" + + "Matching outpoints are automatically registered for redeemingtx notifications.", + "notifyreceived-addresses": "List of address to receive notifications about", + + // OutPoint help. + "outpoint-hash": "The hex-encoded bytes of the outpoint hash", + "outpoint-index": "The index of the outpoint", + + // NotifySpentCmd help. + "notifyspent--synopsis": "Send a redeemingtx notification when a transaction spending an outpoint appears in mempool (if relayed to this btcd instance) and when such a transaction first appears in a newly-attached block.", + "notifyspent-outpoints": "List of transaction outpoints to monitor.", + + // Rescan help. + "rescan--synopsis": "Rescan block chain for transactions to addresses.\n" + + "When the endblock parameter is omitted, the rescan continues through the best block in the main chain.\n" + + "Rescan results are sent as recvtx and redeemingtx notifications.\n" + + "This call returns once the rescan completes.", + "rescan-beginblock": "Hash of the first block to begin rescanning", + "rescan-addresses": "List of addresses to include in the rescan", + "rescan-outpoints": "List of transaction outpoints to include in the rescan", + "rescan-endblock": "Hash of final block to rescan", +} + +// rpcResultTypes specifies the result types that each RPC command can return. +// This information is used to generate the help. Each result type must be a +// pointer to the type (or nil to indicate no return value). +var rpcResultTypes = map[string][]interface{}{ + "addnode": nil, + "createrawtransaction": []interface{}{(*string)(nil)}, + "debuglevel": []interface{}{(*string)(nil), (*string)(nil)}, + "decoderawtransaction": []interface{}{(*btcjson.TxRawDecodeResult)(nil)}, + "decodescript": []interface{}{(*btcjson.DecodeScriptResult)(nil)}, + "getaddednodeinfo": []interface{}{(*[]string)(nil), (*[]btcjson.GetAddedNodeInfoResult)(nil)}, + "getbestblock": []interface{}{(*btcjson.GetBestBlockResult)(nil)}, + "getbestblockhash": []interface{}{(*string)(nil)}, + "getblock": []interface{}{(*string)(nil), (*btcjson.GetBlockVerboseResult)(nil)}, + "getblockcount": []interface{}{(*int64)(nil)}, + "getblockhash": []interface{}{(*string)(nil)}, + "getblocktemplate": []interface{}{(*btcjson.GetBlockTemplateResult)(nil), (*string)(nil), nil}, + "getconnectioncount": []interface{}{(*int32)(nil)}, + "getcurrentnet": []interface{}{(*uint32)(nil)}, + "getdifficulty": []interface{}{(*float64)(nil)}, + "getgenerate": []interface{}{(*bool)(nil)}, + "gethashespersec": []interface{}{(*float64)(nil)}, + "getinfo": []interface{}{(*btcjson.InfoChainResult)(nil)}, + "getmininginfo": []interface{}{(*btcjson.GetMiningInfoResult)(nil)}, + "getnettotals": []interface{}{(*btcjson.GetNetTotalsResult)(nil)}, + "getnetworkhashps": []interface{}{(*int64)(nil)}, + "getpeerinfo": []interface{}{(*[]btcjson.GetPeerInfoResult)(nil)}, + "getrawmempool": []interface{}{(*[]string)(nil), (*btcjson.GetRawMempoolVerboseResult)(nil)}, + "getrawtransaction": []interface{}{(*string)(nil), (*btcjson.TxRawResult)(nil)}, + "gettxout": []interface{}{(*btcjson.GetTxOutResult)(nil)}, + "getwork": []interface{}{(*btcjson.GetWorkResult)(nil), (*bool)(nil)}, + "help": []interface{}{(*string)(nil), (*string)(nil)}, + "ping": nil, + "searchrawtransactions": []interface{}{(*string)(nil), (*[]btcjson.TxRawResult)(nil)}, + "sendrawtransaction": []interface{}{(*string)(nil)}, + "setgenerate": nil, + "stop": []interface{}{(*string)(nil)}, + "submitblock": []interface{}{nil, (*string)(nil)}, + "validateaddress": []interface{}{(*btcjson.ValidateAddressChainResult)(nil)}, + "verifychain": []interface{}{(*bool)(nil)}, + "verifymessage": []interface{}{(*bool)(nil)}, + + // Websocket commands. + "notifyblocks": nil, + "notifynewtransactions": nil, + "notifyreceived": nil, + "notifyspent": nil, + "rescan": nil, +} + +// helpCacher provides a concurrent safe type that provides help and usage for +// the RPC server commands and caches the results for future calls. +type helpCacher struct { + sync.Mutex + usage string + methodHelp map[string]string +} + +// rpcMethodHelp returns an RPC help string for the provided method. +// +// This function is safe for concurrent access. +func (c *helpCacher) rpcMethodHelp(method string) (string, error) { + c.Lock() + defer c.Unlock() + + // Return the cached method help if it exists. + if help, exists := c.methodHelp[method]; exists { + return help, nil + } + + // Look up the result types for the method. + resultTypes, ok := rpcResultTypes[method] + if !ok { + return "", errors.New("no result types specified for method " + + method) + } + + // Generate, cache, and return the help. + help, err := btcjson.GenerateHelp(method, helpDescsEnUS, resultTypes...) + if err != nil { + return "", err + } + c.methodHelp[method] = help + return help, nil +} + +// rpcUsage returns one-line usage for all support RPC commands. +// +// This function is safe for concurrent access. +func (c *helpCacher) rpcUsage(includeWebsockets bool) (string, error) { + c.Lock() + defer c.Unlock() + + // Return the cached usage if it is available. + if c.usage != "" { + return c.usage, nil + } + + // Generate a list of one-line usage for every command. + usageTexts := make([]string, 0, len(rpcHandlers)) + for k := range rpcHandlers { + usage, err := btcjson.MethodUsageText(k) + if err != nil { + return "", err + } + usageTexts = append(usageTexts, usage) + } + + // Include websockets commands if requested. + if includeWebsockets { + for k := range wsHandlers { + usage, err := btcjson.MethodUsageText(k) + if err != nil { + return "", err + } + usageTexts = append(usageTexts, usage) + } + } + + sort.Sort(sort.StringSlice(usageTexts)) + c.usage = strings.Join(usageTexts, "\n") + return c.usage, nil +} + +// newHelpCacher returns a new instance of a help cacher which provides help and +// usage for the RPC server commands and caches the results for future calls. +func newHelpCacher() *helpCacher { + return &helpCacher{ + methodHelp: make(map[string]string), + } +} diff --git a/rpcserverhelp_test.go b/rpcserverhelp_test.go new file mode 100644 index 00000000..03325aba --- /dev/null +++ b/rpcserverhelp_test.go @@ -0,0 +1,65 @@ +// Copyright (c) 2015 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import "testing" + +// TestHelp ensures the help is reasonably accurate by checking that every +// command specified also has result types defined and the one-line usage and +// help text can be generated for them. +func TestHelp(t *testing.T) { + // Ensure there are result types specified for every handler. + for k := range rpcHandlers { + if _, ok := rpcResultTypes[k]; !ok { + t.Errorf("RPC handler defined for method '%v' without "+ + "also specifying result types", k) + continue + } + + } + for k := range wsHandlers { + if _, ok := rpcResultTypes[k]; !ok { + t.Errorf("RPC handler defined for method '%v' without "+ + "also specifying result types", k) + continue + } + + } + + // Ensure the usage for every command can be generated without errors. + helpCacher := newHelpCacher() + if _, err := helpCacher.rpcUsage(true); err != nil { + t.Fatalf("Failed to generate one-line usage: %v", err) + } + if _, err := helpCacher.rpcUsage(true); err != nil { + t.Fatalf("Failed to generate one-line usage (cached): %v", err) + } + + // Ensure the help for every command can be generated without errors. + for k := range rpcHandlers { + if _, err := helpCacher.rpcMethodHelp(k); err != nil { + t.Errorf("Failed to generate help for method '%v': %v", + k, err) + continue + } + if _, err := helpCacher.rpcMethodHelp(k); err != nil { + t.Errorf("Failed to generate help for method '%v'"+ + "(cached): %v", k, err) + continue + } + } + for k := range wsHandlers { + if _, err := helpCacher.rpcMethodHelp(k); err != nil { + t.Errorf("Failed to generate help for method '%v': %v", + k, err) + continue + } + if _, err := helpCacher.rpcMethodHelp(k); err != nil { + t.Errorf("Failed to generate help for method '%v'"+ + "(cached): %v", k, err) + continue + } + } +} diff --git a/rpcwebsocket.go b/rpcwebsocket.go index 71bc2dda..008995bc 100644 --- a/rpcwebsocket.go +++ b/rpcwebsocket.go @@ -1,4 +1,4 @@ -// Copyright (c) 2013-2014 Conformal Systems LLC. +// Copyright (c) 2013-2015 Conformal Systems LLC. // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -19,8 +19,7 @@ import ( "golang.org/x/crypto/ripemd160" - "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcd/btcjson/btcws" + "github.com/btcsuite/btcd/btcjson/v2/btcjson" "github.com/btcsuite/btcd/database" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" @@ -44,11 +43,14 @@ var timeZeroVal time.Time // wsCommandHandler describes a callback function used to handle a specific // command. -type wsCommandHandler func(*wsClient, btcjson.Cmd) (interface{}, *btcjson.Error) +type wsCommandHandler func(*wsClient, interface{}) (interface{}, error) // wsHandlers maps RPC command strings to appropriate websocket handler -// functions. -var wsHandlers = map[string]wsCommandHandler{ +// functions. This is set by init because help references wsHandlers and thus +// causes a dependency loop. +var wsHandlers map[string]wsCommandHandler +var wsHandlersBeforeInit = map[string]wsCommandHandler{ + "help": handleWebsocketHelp, "notifyblocks": handleNotifyBlocks, "notifynewtransactions": handleNotifyNewTransactions, "notifyreceived": handleNotifyReceived, @@ -412,8 +414,8 @@ func (*wsNotificationManager) notifyBlockConnected(clients map[chan struct{}]*ws } // Notify interested websocket clients about the connected block. - ntfn := btcws.NewBlockConnectedNtfn(hash.String(), int32(block.Height())) - marshalledJSON, err := json.Marshal(ntfn) + ntfn := btcjson.NewBlockConnectedNtfn(hash.String(), int32(block.Height())) + marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) if err != nil { rpcsLog.Error("Failed to marshal block connected notification: "+ "%v", err) @@ -442,9 +444,9 @@ func (*wsNotificationManager) notifyBlockDisconnected(clients map[chan struct{}] } // Notify interested websocket clients about the disconnected block. - ntfn := btcws.NewBlockDisconnectedNtfn(hash.String(), + ntfn := btcjson.NewBlockDisconnectedNtfn(hash.String(), int32(block.Height())) - marshalledJSON, err := json.Marshal(ntfn) + marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) if err != nil { rpcsLog.Error("Failed to marshal block disconnected "+ "notification: %v", err) @@ -478,31 +480,36 @@ func (m *wsNotificationManager) notifyForNewTx(clients map[chan struct{}]*wsClie amount += txOut.Value } - ntfn := btcws.NewTxAcceptedNtfn(txShaStr, amount) - marshalledJSON, err := json.Marshal(ntfn) + ntfn := btcjson.NewTxAcceptedNtfn(txShaStr, btcutil.Amount(amount).ToBTC()) + marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) if err != nil { rpcsLog.Errorf("Failed to marshal tx notification: %s", err.Error()) return } - var verboseNtfn *btcws.TxAcceptedVerboseNtfn + var verboseNtfn *btcjson.TxAcceptedVerboseNtfn var marshalledJSONVerbose []byte for _, wsc := range clients { if wsc.verboseTxUpdates { - if verboseNtfn == nil { - net := m.server.server.chainParams - rawTx, err := createTxRawResult(net, txShaStr, - mtx, nil, 0, nil) - if err != nil { - return - } - verboseNtfn = btcws.NewTxAcceptedVerboseNtfn(rawTx) - marshalledJSONVerbose, err = json.Marshal(verboseNtfn) - if err != nil { - rpcsLog.Errorf("Failed to marshal verbose tx notification: %s", err.Error()) - return - } + if marshalledJSONVerbose != nil { + wsc.QueueNotification(marshalledJSONVerbose) + continue + } + net := m.server.server.chainParams + rawTx, err := createTxRawResult(net, txShaStr, mtx, nil, + 0, nil) + if err != nil { + return + } + + verboseNtfn = btcjson.NewTxAcceptedVerboseNtfn(*rawTx) + marshalledJSONVerbose, err = btcjson.MarshalCmd(nil, + verboseNtfn) + if err != nil { + rpcsLog.Errorf("Failed to marshal verbose tx "+ + "notification: %s", err.Error()) + return } wsc.QueueNotification(marshalledJSONVerbose) } else { @@ -590,12 +597,12 @@ func txHexString(tx *btcutil.Tx) string { // blockDetails creates a BlockDetails struct to include in btcws notifications // from a block and a transaction's block index. -func blockDetails(block *btcutil.Block, txIndex int) *btcws.BlockDetails { +func blockDetails(block *btcutil.Block, txIndex int) *btcjson.BlockDetails { if block == nil { return nil } blockSha, _ := block.Sha() // never errors - return &btcws.BlockDetails{ + return &btcjson.BlockDetails{ Height: int32(block.Height()), Hash: blockSha.String(), Index: txIndex, @@ -607,8 +614,8 @@ func blockDetails(block *btcutil.Block, txIndex int) *btcws.BlockDetails { // with the passed parameters. func newRedeemingTxNotification(txHex string, index int, block *btcutil.Block) ([]byte, error) { // Create and marshal the notification. - ntfn := btcws.NewRedeemingTxNtfn(txHex, blockDetails(block, index)) - return json.Marshal(ntfn) + ntfn := btcjson.NewRedeemingTxNtfn(txHex, *blockDetails(block, index)) + return btcjson.MarshalCmd(nil, ntfn) } // notifyForTxOuts examines each transaction output, notifying interested @@ -641,9 +648,10 @@ func (m *wsNotificationManager) notifyForTxOuts(ops map[wire.OutPoint]map[chan s if txHex == "" { txHex = txHexString(tx) } - ntfn := btcws.NewRecvTxNtfn(txHex, blockDetails(block, tx.Index())) + ntfn := btcjson.NewRecvTxNtfn(txHex, *blockDetails(block, + tx.Index())) - marshalledJSON, err := json.Marshal(ntfn) + marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) if err != nil { rpcsLog.Errorf("Failed to marshal processedtx notification: %v", err) continue @@ -833,36 +841,6 @@ type wsResponse struct { doneChan chan bool } -// createMarshalledReply returns a new marshalled btcjson.Reply given the -// passed parameters. It will automatically convert errors that are not of -// the type *btcjson.Error to the appropriate type as needed. -func createMarshalledReply(id, result interface{}, replyErr error) ([]byte, error) { - var jsonErr *btcjson.Error - if replyErr != nil { - if jErr, ok := replyErr.(*btcjson.Error); ok { - jsonErr = jErr - } else { - jsonErr = &btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: replyErr.Error(), - } - } - } - - response := btcjson.Reply{ - Id: &id, - Result: result, - Error: jsonErr, - } - - marshalledJSON, err := json.Marshal(response) - if err != nil { - return nil, err - } - - return marshalledJSON, nil -} - // wsClient provides an abstraction for handling a websocket client. The // overall data flow is split into 3 main goroutines, a possible 4th goroutine // for long-running operations (only started if request is made), and a @@ -914,7 +892,7 @@ type wsClient struct { // Networking infrastructure. asyncStarted bool - asyncChan chan btcjson.Cmd + asyncChan chan *parsedRPCCmd ntfnChan chan []byte sendChan chan wsResponse quit chan struct{} @@ -923,22 +901,27 @@ type wsClient struct { // handleMessage is the main handler for incoming requests. It enforces // authentication, parses the incoming json, looks up and executes handlers -// (including pass through for standard RPC commands), sends the appropriate +// (including pass through for standard RPC commands), and sends the appropriate // response. It also detects commands which are marked as long-running and // sends them off to the asyncHander for processing. func (c *wsClient) handleMessage(msg []byte) { if !c.authenticated { // Disconnect immediately if the provided command fails to // parse when the client is not already authenticated. - cmd, jsonErr := parseCmd(msg) - if jsonErr != nil { + var request btcjson.Request + if err := json.Unmarshal(msg, &request); err != nil { + c.Disconnect() + return + } + parsedCmd := parseCmd(&request) + if parsedCmd.err != nil { c.Disconnect() return } // Disconnect immediately if the first command is not // authenticate when not already authenticated. - authCmd, ok := cmd.(*btcws.AuthenticateCmd) + authCmd, ok := parsedCmd.cmd.(*btcjson.AuthenticateCmd) if !ok { rpcsLog.Warnf("Unauthenticated websocket message " + "received") @@ -959,7 +942,7 @@ func (c *wsClient) handleMessage(msg []byte) { c.authenticated = true // Marshal and send response. - reply, err := createMarshalledReply(authCmd.Id(), nil, nil) + reply, err := createMarshalledReply(parsedCmd.id, nil, nil) if err != nil { rpcsLog.Errorf("Failed to marshal authenticate reply: "+ "%v", err.Error()) @@ -969,21 +952,16 @@ func (c *wsClient) handleMessage(msg []byte) { return } - // Attmpt to parse the raw json into a known btcjson.Cmd. - cmd, jsonErr := parseCmd(msg) - if jsonErr != nil { - // Use the provided id for errors when a valid JSON-RPC message - // was parsed. Requests with no IDs are ignored. - var id interface{} - if cmd != nil { - id = cmd.Id() - if id == nil { - return - } + // Attempt to parse the raw message into a JSON-RPC request. + var request btcjson.Request + if err := json.Unmarshal(msg, &request); err != nil { + jsonErr := &btcjson.RPCError{ + Code: btcjson.ErrRPCParse.Code, + Message: "Failed to parse request: " + err.Error(), } // Marshal and send response. - reply, err := createMarshalledReply(id, nil, jsonErr) + reply, err := createMarshalledReply(nil, nil, jsonErr) if err != nil { rpcsLog.Errorf("Failed to marshal parse failure "+ "reply: %v", err) @@ -992,11 +970,30 @@ func (c *wsClient) handleMessage(msg []byte) { c.SendMessage(reply, nil) return } - rpcsLog.Debugf("Received command <%s> from %s", cmd.Method(), c.addr) + // Requests with no ID (notifications) must not have a response per the + // JSON-RPC spec. + if request.ID == nil { + return + } + + // Attempt to parse the JSON-RPC request into a known concrete command. + cmd := parseCmd(&request) + if cmd.err != nil { + // Marshal and send response. + reply, err := createMarshalledReply(cmd.id, nil, cmd.err) + if err != nil { + rpcsLog.Errorf("Failed to marshal parse failure "+ + "reply: %v", err) + return + } + c.SendMessage(reply, nil) + return + } + rpcsLog.Debugf("Received command <%s> from %s", cmd.method, c.addr) // Disconnect if already authenticated and another authenticate command // is received. - if _, ok := cmd.(*btcws.AuthenticateCmd); ok { + if _, ok := cmd.cmd.(*btcjson.AuthenticateCmd); ok { rpcsLog.Warnf("Websocket client %s is already authenticated", c.addr) c.Disconnect() @@ -1005,7 +1002,7 @@ func (c *wsClient) handleMessage(msg []byte) { // When the command is marked as a long-running command, send it off // to the asyncHander goroutine for processing. - if _, ok := wsAsyncHandlers[cmd.Method()]; ok { + if _, ok := wsAsyncHandlers[cmd.method]; ok { // Start up the async goroutine for handling long-running // requests asynchonrously if needed. if !c.asyncStarted { @@ -1020,28 +1017,28 @@ func (c *wsClient) handleMessage(msg []byte) { // Lookup the websocket extension for the command and if it doesn't // exist fallback to handling the command as a standard command. - wsHandler, ok := wsHandlers[cmd.Method()] + wsHandler, ok := wsHandlers[cmd.method] if !ok { // No websocket-specific handler so handle like a legacy // RPC connection. - response := c.server.standardCmdReply(cmd, nil) - reply, err := json.Marshal(response) + result, jsonErr := c.server.standardCmdResult(cmd, nil) + reply, err := createMarshalledReply(cmd.id, result, jsonErr) if err != nil { rpcsLog.Errorf("Failed to marshal reply for <%s> "+ - "command: %v", cmd.Method(), err) - + "command: %v", cmd.method, err) return } + c.SendMessage(reply, nil) return } // Invoke the handler and marshal and send response. - result, jsonErr := wsHandler(c, cmd) - reply, err := createMarshalledReply(cmd.Id(), result, jsonErr) + result, jsonErr := wsHandler(c, cmd.cmd) + reply, err := createMarshalledReply(cmd.id, result, jsonErr) if err != nil { rpcsLog.Errorf("Failed to marshal reply for <%s> command: %v", - cmd.Method(), err) + cmd.method, err) return } c.SendMessage(reply, nil) @@ -1207,20 +1204,21 @@ func (c *wsClient) asyncHandler() { // runHandler runs the handler for the passed command and sends the // reply. - runHandler := func(cmd btcjson.Cmd) { - wsHandler, ok := wsHandlers[cmd.Method()] + runHandler := func(parsedCmd *parsedRPCCmd) { + wsHandler, ok := wsHandlers[parsedCmd.method] if !ok { rpcsLog.Warnf("No handler for command <%s>", - cmd.Method()) + parsedCmd.method) return } // Invoke the handler and marshal and send response. - result, jsonErr := wsHandler(c, cmd) - reply, err := createMarshalledReply(cmd.Id(), result, jsonErr) + result, jsonErr := wsHandler(c, parsedCmd.cmd) + reply, err := createMarshalledReply(parsedCmd.id, result, + jsonErr) if err != nil { rpcsLog.Errorf("Failed to marshal reply for <%s> "+ - "command: %v", cmd.Method(), err) + "command: %v", parsedCmd.method, err) return } c.SendMessage(reply, nil) @@ -1232,7 +1230,7 @@ out: case cmd := <-c.asyncChan: if !waiting { c.wg.Add(1) - go func(cmd btcjson.Cmd) { + go func(cmd *parsedRPCCmd) { runHandler(cmd) asyncHandlerDoneChan <- struct{}{} c.wg.Done() @@ -1255,11 +1253,11 @@ out: // asynchronously send. element := pendingCmds.Remove(next) c.wg.Add(1) - go func(cmd btcjson.Cmd) { + go func(cmd *parsedRPCCmd) { runHandler(cmd) asyncHandlerDoneChan <- struct{}{} c.wg.Done() - }(element.(btcjson.Cmd)) + }(element.(*parsedRPCCmd)) case <-c.quit: break out @@ -1380,36 +1378,80 @@ func newWebsocketClient(server *rpcServer, conn *websocket.Conn, server: server, addrRequests: make(map[string]struct{}), spentRequests: make(map[wire.OutPoint]struct{}), - ntfnChan: make(chan []byte, 1), // nonblocking sync - asyncChan: make(chan btcjson.Cmd, 1), // nonblocking sync + ntfnChan: make(chan []byte, 1), // nonblocking sync + asyncChan: make(chan *parsedRPCCmd, 1), // nonblocking sync sendChan: make(chan wsResponse, websocketSendBufferSize), quit: make(chan struct{}), } } +// handleWebsocketHelp implements the help command for websocket connections. +func handleWebsocketHelp(wsc *wsClient, icmd interface{}) (interface{}, error) { + cmd, ok := icmd.(*btcjson.HelpCmd) + if !ok { + return nil, btcjson.ErrRPCInternal + } + + // Provide a usage overview of all commands when no specific command + // was specified. + var command string + if cmd.Command != nil { + command = *cmd.Command + } + if command == "" { + usage, err := wsc.server.helpCacher.rpcUsage(true) + if err != nil { + context := "Failed to generate RPC usage" + return nil, internalRPCError(err.Error(), context) + } + return usage, nil + } + + // Check that the command asked for is supported and implemented. + // Search the list of websocket handlers as well as the main list of + // handlers since help should only be provided for those cases. + valid := true + if _, ok := rpcHandlers[command]; !ok { + if _, ok := wsHandlers[command]; !ok { + valid = false + } + } + if !valid { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: "Unknown command: " + command, + } + } + + // Get the help for the command. + help, err := wsc.server.helpCacher.rpcMethodHelp(command) + if err != nil { + context := "Failed to generate help" + return nil, internalRPCError(err.Error(), context) + } + return help, nil +} + // handleNotifyBlocks implements the notifyblocks command extension for // websocket connections. -func handleNotifyBlocks(wsc *wsClient, icmd btcjson.Cmd) (interface{}, *btcjson.Error) { +func handleNotifyBlocks(wsc *wsClient, icmd interface{}) (interface{}, error) { wsc.server.ntfnMgr.RegisterBlockUpdates(wsc) return nil, nil } // handleNotifySpent implements the notifyspent command extension for // websocket connections. -func handleNotifySpent(wsc *wsClient, icmd btcjson.Cmd) (interface{}, *btcjson.Error) { - cmd, ok := icmd.(*btcws.NotifySpentCmd) +func handleNotifySpent(wsc *wsClient, icmd interface{}) (interface{}, error) { + cmd, ok := icmd.(*btcjson.NotifySpentCmd) if !ok { - return nil, &btcjson.ErrInternal + return nil, btcjson.ErrRPCInternal } outpoints := make([]*wire.OutPoint, 0, len(cmd.OutPoints)) for i := range cmd.OutPoints { blockHash, err := wire.NewShaHashFromStr(cmd.OutPoints[i].Hash) if err != nil { - return nil, &btcjson.Error{ - Code: btcjson.ErrParse.Code, - Message: err.Error(), - } + return nil, rpcDecodeHexError(cmd.OutPoints[i].Hash) } index := cmd.OutPoints[i].Index outpoints = append(outpoints, wire.NewOutPoint(blockHash, index)) @@ -1420,23 +1462,23 @@ func handleNotifySpent(wsc *wsClient, icmd btcjson.Cmd) (interface{}, *btcjson.E // handleNotifyNewTransations implements the notifynewtransactions command // extension for websocket connections. -func handleNotifyNewTransactions(wsc *wsClient, icmd btcjson.Cmd) (interface{}, *btcjson.Error) { - cmd, ok := icmd.(*btcws.NotifyNewTransactionsCmd) +func handleNotifyNewTransactions(wsc *wsClient, icmd interface{}) (interface{}, error) { + cmd, ok := icmd.(*btcjson.NotifyNewTransactionsCmd) if !ok { - return nil, &btcjson.ErrInternal + return nil, btcjson.ErrRPCInternal } - wsc.verboseTxUpdates = cmd.Verbose + wsc.verboseTxUpdates = cmd.Verbose != nil && *cmd.Verbose wsc.server.ntfnMgr.RegisterNewMempoolTxsUpdates(wsc) return nil, nil } // handleNotifyReceived implements the notifyreceived command extension for // websocket connections. -func handleNotifyReceived(wsc *wsClient, icmd btcjson.Cmd) (interface{}, *btcjson.Error) { - cmd, ok := icmd.(*btcws.NotifyReceivedCmd) +func handleNotifyReceived(wsc *wsClient, icmd interface{}) (interface{}, error) { + cmd, ok := icmd.(*btcjson.NotifyReceivedCmd) if !ok { - return nil, &btcjson.ErrInternal + return nil, btcjson.ErrRPCInternal } // Decode addresses to validate input, but the strings slice is used @@ -1444,11 +1486,11 @@ func handleNotifyReceived(wsc *wsClient, icmd btcjson.Cmd) (interface{}, *btcjso for _, addr := range cmd.Addresses { _, err := btcutil.DecodeAddress(addr, activeNetParams.Params) if err != nil { - e := btcjson.Error{ - Code: btcjson.ErrInvalidAddressOrKey.Code, - Message: fmt.Sprintf("Invalid address or key: %v", addr), + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: fmt.Sprintf("Invalid address or key: %v", + addr), } - return nil, &e } } wsc.server.ntfnMgr.RegisterTxOutAddressRequests(wsc, cmd.Addresses) @@ -1478,8 +1520,8 @@ func (r *rescanKeys) unspentSlice() []*wire.OutPoint { // ErrRescanReorg defines the error that is returned when an unrecoverable // reorganize is detected during a rescan. -var ErrRescanReorg = btcjson.Error{ - Code: btcjson.ErrDatabase.Code, +var ErrRescanReorg = btcjson.RPCError{ + Code: btcjson.ErrRPCDatabase, Message: "Reorganize", } @@ -1596,9 +1638,10 @@ func rescanBlock(wsc *wsClient, lookups *rescanKeys, blk *btcutil.Block) { if txHex == "" { txHex = txHexString(tx) } - ntfn := btcws.NewRecvTxNtfn(txHex, blockDetails(blk, tx.Index())) + ntfn := btcjson.NewRecvTxNtfn(txHex, + *blockDetails(blk, tx.Index())) - marshalledJSON, err := json.Marshal(ntfn) + marshalledJSON, err := btcjson.MarshalCmd(nil, ntfn) if err != nil { rpcsLog.Errorf("Failed to marshal recvtx notification: %v", err) return @@ -1622,12 +1665,15 @@ func rescanBlock(wsc *wsClient, lookups *rescanKeys, blk *btcutil.Block) { // range of blocks. If this condition does not hold true, the JSON-RPC error // for an unrecoverable reorganize is returned. func recoverFromReorg(db database.Db, minBlock, maxBlock int64, - lastBlock *wire.ShaHash) ([]wire.ShaHash, *btcjson.Error) { + lastBlock *wire.ShaHash) ([]wire.ShaHash, error) { hashList, err := db.FetchHeightRange(minBlock, maxBlock) if err != nil { rpcsLog.Errorf("Error looking up block range: %v", err) - return nil, &btcjson.ErrDatabase + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDatabase, + Message: "Database error: " + err.Error(), + } } if lastBlock == nil || len(hashList) == 0 { return hashList, nil @@ -1636,7 +1682,10 @@ func recoverFromReorg(db database.Db, minBlock, maxBlock int64, if err != nil { rpcsLog.Errorf("Error looking up possibly reorged block: %v", err) - return nil, &btcjson.ErrDatabase + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDatabase, + Message: "Database error: " + err.Error(), + } } jsonErr := descendantBlock(lastBlock, blk) if jsonErr != nil { @@ -1647,7 +1696,7 @@ func recoverFromReorg(db database.Db, minBlock, maxBlock int64, // descendantBlock returns the appropiate JSON-RPC error if a current block // fetched during a reorganize is not a direct child of the parent block hash. -func descendantBlock(prevHash *wire.ShaHash, curBlock *btcutil.Block) *btcjson.Error { +func descendantBlock(prevHash *wire.ShaHash, curBlock *btcutil.Block) error { curHash := &curBlock.MsgBlock().Header.PrevBlock if !prevHash.IsEqual(curHash) { rpcsLog.Errorf("Stopping rescan for reorged block %v "+ @@ -1667,20 +1716,17 @@ func descendantBlock(prevHash *wire.ShaHash, curBlock *btcutil.Block) *btcjson.E // handler erroring. Clients must handle this by finding a block still in // the chain (perhaps from a rescanprogress notification) to resume their // rescan. -func handleRescan(wsc *wsClient, icmd btcjson.Cmd) (interface{}, *btcjson.Error) { - cmd, ok := icmd.(*btcws.RescanCmd) +func handleRescan(wsc *wsClient, icmd interface{}) (interface{}, error) { + cmd, ok := icmd.(*btcjson.RescanCmd) if !ok { - return nil, &btcjson.ErrInternal + return nil, btcjson.ErrRPCInternal } outpoints := make([]*wire.OutPoint, 0, len(cmd.OutPoints)) for i := range cmd.OutPoints { blockHash, err := wire.NewShaHashFromStr(cmd.OutPoints[i].Hash) if err != nil { - return nil, &btcjson.Error{ - Code: btcjson.ErrParse.Code, - Message: err.Error(), - } + return nil, rpcDecodeHexError(cmd.OutPoints[i].Hash) } index := cmd.OutPoints[i].Index outpoints = append(outpoints, wire.NewOutPoint(blockHash, index)) @@ -1707,9 +1753,10 @@ func handleRescan(wsc *wsClient, icmd btcjson.Cmd) (interface{}, *btcjson.Error) for _, addrStr := range cmd.Addresses { addr, err := btcutil.DecodeAddress(addrStr, activeNetParams.Params) if err != nil { - jsonErr := btcjson.Error{ - Code: btcjson.ErrInvalidAddressOrKey.Code, - Message: "Rescan address " + addrStr + ": " + err.Error(), + jsonErr := btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: "Rescan address " + addrStr + ": " + + err.Error(), } return nil, &jsonErr } @@ -1732,8 +1779,8 @@ func handleRescan(wsc *wsClient, icmd btcjson.Cmd) (interface{}, *btcjson.Error) lookups.uncompressedPubkeys[uncompressedPubkey] = struct{}{} default: - jsonErr := btcjson.Error{ - Code: btcjson.ErrInvalidAddressOrKey.Code, + jsonErr := btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, Message: "Pubkey " + addrStr + " is of unknown length", } return nil, &jsonErr @@ -1754,22 +1801,28 @@ func handleRescan(wsc *wsClient, icmd btcjson.Cmd) (interface{}, *btcjson.Error) minBlockSha, err := wire.NewShaHashFromStr(cmd.BeginBlock) if err != nil { - return nil, &btcjson.ErrDecodeHexString + return nil, rpcDecodeHexError(cmd.BeginBlock) } minBlock, err := db.FetchBlockHeightBySha(minBlockSha) if err != nil { - return nil, &btcjson.ErrBlockNotFound + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCBlockNotFound, + Message: "Error getting block: " + err.Error(), + } } maxBlock := database.AllShas - if cmd.EndBlock != "" { - maxBlockSha, err := wire.NewShaHashFromStr(cmd.EndBlock) + if cmd.EndBlock != nil { + maxBlockSha, err := wire.NewShaHashFromStr(*cmd.EndBlock) if err != nil { - return nil, &btcjson.ErrDecodeHexString + return nil, rpcDecodeHexError(*cmd.EndBlock) } maxBlock, err = db.FetchBlockHeightBySha(maxBlockSha) if err != nil { - return nil, &btcjson.ErrBlockNotFound + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCBlockNotFound, + Message: "Error getting block: " + err.Error(), + } } } @@ -1790,7 +1843,10 @@ fetchRange: hashList, err := db.FetchHeightRange(minBlock, maxBlock) if err != nil { rpcsLog.Errorf("Error looking up block range: %v", err) - return nil, &btcjson.ErrDatabase + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDatabase, + Message: "Database error: " + err.Error(), + } } if len(hashList) == 0 { // The rescan is finished if no blocks hashes for this @@ -1825,7 +1881,11 @@ fetchRange: if err != nil { rpcsLog.Errorf("Error fetching best block "+ "hash: %v", err) - return nil, &btcjson.ErrDatabase + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDatabase, + Message: "Database error: " + + err.Error(), + } } if again { continue @@ -1842,7 +1902,11 @@ fetchRange: if err != database.ErrBlockShaMissing { rpcsLog.Errorf("Error looking up "+ "block: %v", err) - return nil, &btcjson.ErrDatabase + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDatabase, + Message: "Database error: " + + err.Error(), + } } // If an absolute max block was specified, don't @@ -1865,11 +1929,10 @@ fetchRange: // before the range was evaluated, as it must be // reevaluated for the new hashList. minBlock += int64(i) - var jsonErr *btcjson.Error - hashList, jsonErr = recoverFromReorg(db, minBlock, + hashList, err = recoverFromReorg(db, minBlock, maxBlock, lastBlockHash) - if jsonErr != nil { - return nil, jsonErr + if err != nil { + return nil, err } if len(hashList) == 0 { break fetchRange @@ -1897,9 +1960,9 @@ fetchRange: lastBlock = blk lastBlockHash, err = blk.Sha() if err != nil { - rpcsLog.Errorf("Unknown problem creating "+ - "block sha: %v", err) - return nil, &btcjson.ErrInternal + context := "Failed to create block hash" + return nil, internalRPCError(err.Error(), + context) } } @@ -1912,10 +1975,10 @@ fetchRange: continue } - n := btcws.NewRescanProgressNtfn(hashList[i].String(), + n := btcjson.NewRescanProgressNtfn(hashList[i].String(), int32(blk.Height()), blk.MsgBlock().Header.Timestamp.Unix()) - mn, err := n.MarshalJSON() + mn, err := btcjson.MarshalCmd(nil, n) if err != nil { rpcsLog.Errorf("Failed to marshal rescan "+ "progress notification: %v", err) @@ -1940,10 +2003,10 @@ fetchRange: // received before the rescan RPC returns. Therefore, another method // is needed to safely inform clients that all rescan notifications have // been sent. - n := btcws.NewRescanFinishedNtfn(lastBlockHash.String(), + n := btcjson.NewRescanFinishedNtfn(lastBlockHash.String(), int32(lastBlock.Height()), lastBlock.MsgBlock().Header.Timestamp.Unix()) - if mn, err := n.MarshalJSON(); err != nil { + if mn, err := btcjson.MarshalCmd(nil, n); err != nil { rpcsLog.Errorf("Failed to marshal rescan finished "+ "notification: %v", err) } else { @@ -1955,3 +2018,7 @@ fetchRange: rpcsLog.Info("Finished rescan") return nil, nil } + +func init() { + wsHandlers = wsHandlersBeforeInit +} diff --git a/server.go b/server.go index f9080f87..383288d3 100644 --- a/server.go +++ b/server.go @@ -21,7 +21,7 @@ import ( "github.com/btcsuite/btcd/addrmgr" "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcjson/v2/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/database" "github.com/btcsuite/btcd/wire"