Currently, timeouts when responding to a websocket ping control message with a pong message cause the websocket client to be disconnected, but these types of failures are nearly always temporary network errors due to things such as congestion and thus should not result in disconnection. In order to make that the case, this modifies the websocket ping handler to ignore timeout network errors when attempting to respond with a pong and also takes this opportunity to increase the timeout to 5 seconds from 1 second to support multi-continent communication.
6598 lines
204 KiB
Go
6598 lines
204 KiB
Go
// Copyright (c) 2013-2016 The btcsuite developers
|
|
// Copyright (c) 2015-2022 The Decred developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package rpcserver
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"hash"
|
|
"io"
|
|
stdlog "log"
|
|
"math"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
"github.com/decred/dcrd/blockchain/stake/v4"
|
|
"github.com/decred/dcrd/blockchain/standalone/v2"
|
|
"github.com/decred/dcrd/blockchain/v4"
|
|
"github.com/decred/dcrd/chaincfg/chainhash"
|
|
"github.com/decred/dcrd/chaincfg/v3"
|
|
"github.com/decred/dcrd/database/v3"
|
|
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
|
|
"github.com/decred/dcrd/dcrjson/v4"
|
|
"github.com/decred/dcrd/dcrutil/v4"
|
|
"github.com/decred/dcrd/internal/mempool"
|
|
"github.com/decred/dcrd/internal/mining"
|
|
"github.com/decred/dcrd/internal/version"
|
|
"github.com/decred/dcrd/rpc/jsonrpc/types/v3"
|
|
"github.com/decred/dcrd/txscript/v4"
|
|
"github.com/decred/dcrd/txscript/v4/stdaddr"
|
|
"github.com/decred/dcrd/txscript/v4/stdscript"
|
|
"github.com/decred/dcrd/wire"
|
|
"github.com/jrick/bitset"
|
|
)
|
|
|
|
// API version constants
|
|
const (
|
|
jsonrpcSemverMajor = 7
|
|
jsonrpcSemverMinor = 0
|
|
jsonrpcSemverPatch = 0
|
|
)
|
|
|
|
const (
|
|
// rpcAuthTimeoutSeconds is the number of seconds a connection to the
|
|
// RPC server is allowed to stay open without authenticating before it
|
|
// is closed.
|
|
rpcAuthTimeoutSeconds = 10
|
|
|
|
// rpcReadLimitAuthenticated is the maximum number of bytes allowed for a
|
|
// JSON-RPC message read from a client.
|
|
rpcReadLimitAuthenticated = 1 << 23 // 8 MiB
|
|
|
|
// uint256Size is the number of bytes needed to represent an unsigned
|
|
// 256-bit integer.
|
|
uint256Size = 32
|
|
|
|
// getworkDataLen is the length of the data field of the getwork RPC.
|
|
// It consists of the serialized block header plus the internal blake256
|
|
// padding. The internal blake256 padding consists of a single 1 bit
|
|
// followed by zeros and a final 1 bit in order to pad the message out
|
|
// to 56 bytes followed by length of the message in bits encoded as a
|
|
// big-endian uint64 (8 bytes). Thus, the resulting length is a
|
|
// multiple of the blake256 block size (64 bytes). Given the padding
|
|
// requires at least a 1 bit and 64 bits for the padding, the following
|
|
// converts the block header length and hash block size to bits in order
|
|
// to ensure the correct number of hash blocks are calculated and then
|
|
// multiplies the result by the block hash block size in bytes.
|
|
getworkDataLen = (1 + ((wire.MaxBlockHeaderPayload*8 + 65) /
|
|
(chainhash.HashBlockSize * 8))) * chainhash.HashBlockSize
|
|
|
|
// getworkExpirationDiff is the number of blocks below the current
|
|
// best block in height to begin pruning out old block work from
|
|
// the template pool.
|
|
getworkExpirationDiff = 3
|
|
|
|
// sstxCommitmentString is the string to insert when a verbose
|
|
// transaction output's pkscript type is a ticket commitment.
|
|
sstxCommitmentString = "sstxcommitment"
|
|
|
|
// merkleRootPairSize is the size in bytes of the merkle root + stake root
|
|
// of a block.
|
|
merkleRootPairSize = 64
|
|
|
|
// syncWait is the maximum time in seconds to wait for an index
|
|
// to sync with the main chain.
|
|
syncWait = time.Second * 3
|
|
)
|
|
|
|
var (
|
|
// jsonrpcSemverString is the RPC server's semantic API version formatted as
|
|
// a string.
|
|
jsonrpcSemverString = fmt.Sprintf("%d.%d.%d", jsonrpcSemverMajor,
|
|
jsonrpcSemverMinor, jsonrpcSemverPatch)
|
|
|
|
// blake256Pad is the extra blake256 internal padding needed for the
|
|
// data of the getwork RPC. It is set in the init routine since it is
|
|
// based on the size of the block header and requires a bit of
|
|
// calculation.
|
|
blake256Pad []byte
|
|
|
|
// JSON 2.0 batched request prefix
|
|
batchedRequestPrefix = []byte("[")
|
|
|
|
// zeroHash is the zero value for a chainhash.Hash and is defined as
|
|
// a package level variable to avoid the need to create a new instance
|
|
// every time a check is needed.
|
|
zeroHash chainhash.Hash
|
|
)
|
|
|
|
// Errors
|
|
var (
|
|
// ErrRPCUnimplemented is an error returned to RPC clients when the
|
|
// provided command is recognized, but not implemented.
|
|
ErrRPCUnimplemented = &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCUnimplemented,
|
|
Message: "Command unimplemented",
|
|
}
|
|
|
|
// ErrRPCNoWallet is an error returned to RPC clients when the provided
|
|
// command is recognized as a wallet command.
|
|
ErrRPCNoWallet = &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCNoWallet,
|
|
Message: "This implementation does not implement wallet commands",
|
|
}
|
|
)
|
|
|
|
type commandHandler func(context.Context, *Server, interface{}) (interface{}, error)
|
|
|
|
// 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[types.Method]commandHandler
|
|
var rpcHandlersBeforeInit = map[types.Method]commandHandler{
|
|
"addnode": handleAddNode,
|
|
"createrawsstx": handleCreateRawSStx,
|
|
"createrawssrtx": handleCreateRawSSRtx,
|
|
"createrawtransaction": handleCreateRawTransaction,
|
|
"debuglevel": handleDebugLevel,
|
|
"decoderawtransaction": handleDecodeRawTransaction,
|
|
"decodescript": handleDecodeScript,
|
|
"estimatefee": handleEstimateFee,
|
|
"estimatesmartfee": handleEstimateSmartFee,
|
|
"estimatestakediff": handleEstimateStakeDiff,
|
|
"existsaddress": handleExistsAddress,
|
|
"existsaddresses": handleExistsAddresses,
|
|
"existsexpiredtickets": handleExistsExpiredTickets,
|
|
"existsliveticket": handleExistsLiveTicket,
|
|
"existslivetickets": handleExistsLiveTickets,
|
|
"existsmempooltxs": handleExistsMempoolTxs,
|
|
"existsmissedtickets": handleExistsMissedTickets,
|
|
"generate": handleGenerate,
|
|
"getaddednodeinfo": handleGetAddedNodeInfo,
|
|
"getbestblock": handleGetBestBlock,
|
|
"getbestblockhash": handleGetBestBlockHash,
|
|
"getblock": handleGetBlock,
|
|
"getblockchaininfo": handleGetBlockchainInfo,
|
|
"getblockcount": handleGetBlockCount,
|
|
"getblockhash": handleGetBlockHash,
|
|
"getblockheader": handleGetBlockHeader,
|
|
"getblocksubsidy": handleGetBlockSubsidy,
|
|
"getcfilterv2": handleGetCFilterV2,
|
|
"getchaintips": handleGetChainTips,
|
|
"getcoinsupply": handleGetCoinSupply,
|
|
"getconnectioncount": handleGetConnectionCount,
|
|
"getcurrentnet": handleGetCurrentNet,
|
|
"getdifficulty": handleGetDifficulty,
|
|
"getgenerate": handleGetGenerate,
|
|
"gethashespersec": handleGetHashesPerSec,
|
|
"getheaders": handleGetHeaders,
|
|
"getinfo": handleGetInfo,
|
|
"getmempoolinfo": handleGetMempoolInfo,
|
|
"getmininginfo": handleGetMiningInfo,
|
|
"getnettotals": handleGetNetTotals,
|
|
"getnetworkhashps": handleGetNetworkHashPS,
|
|
"getnetworkinfo": handleGetNetworkInfo,
|
|
"getpeerinfo": handleGetPeerInfo,
|
|
"getrawmempool": handleGetRawMempool,
|
|
"getrawtransaction": handleGetRawTransaction,
|
|
"getstakedifficulty": handleGetStakeDifficulty,
|
|
"getstakeversioninfo": handleGetStakeVersionInfo,
|
|
"getstakeversions": handleGetStakeVersions,
|
|
"getticketpoolvalue": handleGetTicketPoolValue,
|
|
"gettreasurybalance": handleGetTreasuryBalance,
|
|
"gettreasuryspendvotes": handleGetTreasurySpendVotes,
|
|
"getvoteinfo": handleGetVoteInfo,
|
|
"gettxout": handleGetTxOut,
|
|
"gettxoutsetinfo": handleGetTxOutSetInfo,
|
|
"getwork": handleGetWork,
|
|
"help": handleHelp,
|
|
"invalidateblock": handleInvalidateBlock,
|
|
"livetickets": handleLiveTickets,
|
|
"missedtickets": handleMissedTickets,
|
|
"node": handleNode,
|
|
"ping": handlePing,
|
|
"reconsiderblock": handleReconsiderBlock,
|
|
"regentemplate": handleRegenTemplate,
|
|
"searchrawtransactions": handleSearchRawTransactions,
|
|
"sendrawtransaction": handleSendRawTransaction,
|
|
"setgenerate": handleSetGenerate,
|
|
"stop": handleStop,
|
|
"submitblock": handleSubmitBlock,
|
|
"ticketfeeinfo": handleTicketFeeInfo,
|
|
"ticketsforaddress": handleTicketsForAddress,
|
|
"ticketvwap": handleTicketVWAP,
|
|
"txfeeinfo": handleTxFeeInfo,
|
|
"validateaddress": handleValidateAddress,
|
|
"verifychain": handleVerifyChain,
|
|
"verifymessage": handleVerifyMessage,
|
|
"version": handleVersion,
|
|
}
|
|
|
|
// list of commands that we recognize, but for which dcrd has no support because
|
|
// it lacks support for wallet functionality. For these commands the user
|
|
// should ask a connected instance of dcrwallet.
|
|
var rpcAskWallet = map[string]struct{}{
|
|
"abandontransaction": {},
|
|
"accountaddressindex": {},
|
|
"accountsyncaddressindex": {},
|
|
"addmultisigaddress": {},
|
|
"addtransaction": {},
|
|
"addticket": {},
|
|
"auditreuse": {},
|
|
"consolidate": {},
|
|
"createmultisig": {},
|
|
"createnewaccount": {},
|
|
"createsignature": {},
|
|
"createvotingaccount": {},
|
|
"discoverusage": {},
|
|
"dumpprivkey": {},
|
|
"fundrawtransaction": {},
|
|
"generatevote": {},
|
|
"getaccount": {},
|
|
"getaccountaddress": {},
|
|
"getaddressesbyaccount": {},
|
|
"getbalance": {},
|
|
"getcoinjoinsbyacct": {},
|
|
"getmasterpubkey": {},
|
|
"getmultisigoutinfo": {},
|
|
"getnewaddress": {},
|
|
"getrawchangeaddress": {},
|
|
"getreceivedbyaccount": {},
|
|
"getreceivedbyaddress": {},
|
|
"getstakeinfo": {},
|
|
"gettickets": {},
|
|
"gettransaction": {},
|
|
"getunconfirmedbalance": {},
|
|
"getvotechoices": {},
|
|
"getwalletfee": {},
|
|
"importcfiltersv2": {},
|
|
"importprivkey": {},
|
|
"importscript": {},
|
|
"importxpub": {},
|
|
"listaccounts": {},
|
|
"listaddresstransactions": {},
|
|
"listalltransactions": {},
|
|
"listlockunspent": {},
|
|
"listreceivedbyaccount": {},
|
|
"listreceivedbyaddress": {},
|
|
"listsinceblock": {},
|
|
"listtransactions": {},
|
|
"listunspent": {},
|
|
"lockunspent": {},
|
|
"mixoutput": {},
|
|
"purchaseticket": {},
|
|
"redeemmultisigout": {},
|
|
"redeemmultisigouts": {},
|
|
"renameaccount": {},
|
|
"rescanwallet": {},
|
|
"revoketickets": {},
|
|
"sendfrom": {},
|
|
"sendfromtreasury": {},
|
|
"sendmany": {},
|
|
"sendtoaddress": {},
|
|
"sendtomultisig": {},
|
|
"sendtotreasury": {},
|
|
"setticketfee": {},
|
|
"settxfee": {},
|
|
"setvotechoice": {},
|
|
"signmessage": {},
|
|
"signrawtransaction": {},
|
|
"signrawtransactions": {},
|
|
"stakepooluserinfo": {},
|
|
"sweepaccount": {},
|
|
"ticketinfo": {},
|
|
"ticketsforaddress": {},
|
|
"validatepredcp0005cf": {},
|
|
"verifymessage": {},
|
|
"walletinfo": {},
|
|
"walletislocked": {},
|
|
"walletlock": {},
|
|
"walletpassphrase": {},
|
|
"walletpassphrasechange": {},
|
|
"walletpubpassphrasechange": {},
|
|
}
|
|
|
|
// Commands that are currently unimplemented, but should ultimately be.
|
|
var rpcUnimplemented = map[string]struct{}{
|
|
"estimatepriority": {},
|
|
}
|
|
|
|
// Commands that are available to a limited user
|
|
var rpcLimited = map[string]struct{}{
|
|
// Websockets commands
|
|
"notifyblocks": {},
|
|
"notifynewtransactions": {},
|
|
"notifyreceived": {},
|
|
"notifyspent": {},
|
|
"rescan": {},
|
|
"session": {},
|
|
"rebroadcastmissed": {},
|
|
"rebroadcastwinners": {},
|
|
|
|
// Websockets AND HTTP/S commands
|
|
"help": {},
|
|
|
|
// HTTP/S-only commands
|
|
"createrawsstx": {},
|
|
"createrawssrtx": {},
|
|
"createrawtransaction": {},
|
|
"decoderawtransaction": {},
|
|
"decodescript": {},
|
|
"estimatefee": {},
|
|
"estimatesmartfee": {},
|
|
"estimatestakediff": {},
|
|
"existsaddress": {},
|
|
"existsaddresses": {},
|
|
"existsexpiredtickets": {},
|
|
"existsliveticket": {},
|
|
"existslivetickets": {},
|
|
"existsmempooltxs": {},
|
|
"existsmissedtickets": {},
|
|
"getbestblock": {},
|
|
"getbestblockhash": {},
|
|
"getblock": {},
|
|
"getblockchaininfo": {},
|
|
"getblockcount": {},
|
|
"getblockhash": {},
|
|
"getblockheader": {},
|
|
"getblocksubsidy": {},
|
|
"getcfilterv2": {},
|
|
"getchaintips": {},
|
|
"getcoinsupply": {},
|
|
"getcurrentnet": {},
|
|
"getdifficulty": {},
|
|
"getheaders": {},
|
|
"getinfo": {},
|
|
"getnettotals": {},
|
|
"getnetworkhashps": {},
|
|
"getnetworkinfo": {},
|
|
"getrawmempool": {},
|
|
"getstakedifficulty": {},
|
|
"getstakeversioninfo": {},
|
|
"getstakeversions": {},
|
|
"getrawtransaction": {},
|
|
"gettreasurybalance": {},
|
|
"gettxout": {},
|
|
"getvoteinfo": {},
|
|
"livetickets": {},
|
|
"missedtickets": {},
|
|
"regentemplate": {},
|
|
"searchrawtransactions": {},
|
|
"sendrawtransaction": {},
|
|
"submitblock": {},
|
|
"ticketfeeinfo": {},
|
|
"ticketsforaddress": {},
|
|
"ticketvwap": {},
|
|
"txfeeinfo": {},
|
|
"validateaddress": {},
|
|
"verifymessage": {},
|
|
"version": {},
|
|
}
|
|
|
|
// rpcInternalError 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 rpcInternalError(errStr, context string) *dcrjson.RPCError {
|
|
logStr := errStr
|
|
if context != "" {
|
|
logStr = context + ": " + errStr
|
|
}
|
|
log.Error(logStr)
|
|
return dcrjson.NewRPCError(dcrjson.ErrRPCInternal.Code, errStr)
|
|
}
|
|
|
|
// rpcInvalidError is a convenience function to convert an invalid parameter
|
|
// error to an RPC error with the appropriate code set.
|
|
func rpcInvalidError(fmtStr string, args ...interface{}) *dcrjson.RPCError {
|
|
return dcrjson.NewRPCError(dcrjson.ErrRPCInvalidParameter,
|
|
fmt.Sprintf(fmtStr, args...))
|
|
}
|
|
|
|
// rpcDeserializetionError is a convenience function to convert a
|
|
// deserialization error to an RPC error with the appropriate code set.
|
|
func rpcDeserializationError(fmtStr string, args ...interface{}) *dcrjson.RPCError {
|
|
return dcrjson.NewRPCError(dcrjson.ErrRPCDeserialization,
|
|
fmt.Sprintf(fmtStr, args...))
|
|
}
|
|
|
|
// rpcRuleError is a convenience function to convert a
|
|
// rule error to an RPC error with the appropriate code set.
|
|
func rpcRuleError(fmtStr string, args ...interface{}) *dcrjson.RPCError {
|
|
return dcrjson.NewRPCError(dcrjson.ErrRPCMisc,
|
|
fmt.Sprintf(fmtStr, args...))
|
|
}
|
|
|
|
// rpcDuplicateTxError is a convenience function to convert a
|
|
// rejected duplicate tx error to an RPC error with the appropriate code set.
|
|
func rpcDuplicateTxError(fmtStr string, args ...interface{}) *dcrjson.RPCError {
|
|
return dcrjson.NewRPCError(dcrjson.ErrRPCDuplicateTx,
|
|
fmt.Sprintf(fmtStr, args...))
|
|
}
|
|
|
|
// rpcAddressKeyError is a convenience function to convert an address/key 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 rpcAddressKeyError(fmtStr string, args ...interface{}) *dcrjson.RPCError {
|
|
return dcrjson.NewRPCError(dcrjson.ErrRPCInvalidAddressOrKey,
|
|
fmt.Sprintf(fmtStr, args...))
|
|
}
|
|
|
|
// 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) *dcrjson.RPCError {
|
|
return dcrjson.NewRPCError(dcrjson.ErrRPCDecodeHexString,
|
|
fmt.Sprintf("Argument must be hexadecimal string (not %q)",
|
|
gotHex))
|
|
}
|
|
|
|
// rpcNoTxInfoError is a convenience function for returning a nicely formatted
|
|
// RPC error which indicates there is no information available for the provided
|
|
// transaction hash.
|
|
func rpcNoTxInfoError(txHash *chainhash.Hash) *dcrjson.RPCError {
|
|
return dcrjson.NewRPCError(dcrjson.ErrRPCNoTxInfo,
|
|
fmt.Sprintf("No information available about transaction %v",
|
|
txHash))
|
|
}
|
|
|
|
// rpcBlockNotFoundError is a convenience function for returning a nicely
|
|
// formatted RPC error which indicates that the provided block was not found in
|
|
// the blockchain.
|
|
func rpcBlockNotFoundError(blockHash chainhash.Hash) *dcrjson.RPCError {
|
|
return dcrjson.NewRPCError(dcrjson.ErrRPCBlockNotFound,
|
|
fmt.Sprintf("No information available about block %v",
|
|
blockHash))
|
|
}
|
|
|
|
// rpcMiscError is a convenience function for returning a nicely formatted RPC
|
|
// error which indicates there is an unquantifiable error. Use this sparingly;
|
|
// misc return codes are a cop out.
|
|
func rpcMiscError(message string) *dcrjson.RPCError {
|
|
return dcrjson.NewRPCError(dcrjson.ErrRPCMisc, message)
|
|
}
|
|
|
|
// directionString is a helper function that returns a string that represents
|
|
// the direction of a connection (inbound or outbound).
|
|
func directionString(inbound bool) string {
|
|
if inbound {
|
|
return "inbound"
|
|
}
|
|
return "outbound"
|
|
}
|
|
|
|
// normalizeAddress returns addr with the passed default port appended if
|
|
// there is not already a port specified.
|
|
//
|
|
// If the host portion of the address is an interface name, the first IP address
|
|
// associated with the interface is used. Otherwise, the name is interpreted as
|
|
// a hostname.
|
|
func normalizeAddress(addr, defaultPort string) string {
|
|
port := defaultPort
|
|
if a, p, err := net.SplitHostPort(addr); err == nil {
|
|
addr = a
|
|
port = p
|
|
}
|
|
iface, _ := net.InterfaceByName(addr)
|
|
if iface == nil {
|
|
return net.JoinHostPort(addr, port)
|
|
}
|
|
ifaceAddrs, err := iface.Addrs()
|
|
if err != nil {
|
|
return net.JoinHostPort(addr, port)
|
|
}
|
|
for _, a := range ifaceAddrs {
|
|
switch a := a.(type) {
|
|
case *net.IPNet:
|
|
dialAddr := a.IP.String()
|
|
if a.IP.To4() == nil { // IPv6
|
|
zoned := a.IP.IsLinkLocalUnicast() ||
|
|
a.IP.IsLinkLocalMulticast()
|
|
if zoned {
|
|
dialAddr += "%" + addr
|
|
}
|
|
}
|
|
return net.JoinHostPort(dialAddr, port)
|
|
}
|
|
}
|
|
|
|
return net.JoinHostPort(addr, port)
|
|
}
|
|
|
|
// workState houses state that is used in between multiple RPC invocations to
|
|
// getwork.
|
|
type workState struct {
|
|
sync.Mutex
|
|
prevHash *chainhash.Hash
|
|
templatePool map[[merkleRootPairSize]byte]*wire.MsgBlock
|
|
}
|
|
|
|
// newWorkState returns a new instance of a workState with all internal fields
|
|
// initialized and ready to use.
|
|
func newWorkState() *workState {
|
|
return &workState{
|
|
templatePool: make(map[[merkleRootPairSize]byte]*wire.MsgBlock),
|
|
}
|
|
}
|
|
|
|
// handleAddNode handles addnode commands.
|
|
func handleAddNode(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.AddNodeCmd)
|
|
|
|
addr := normalizeAddress(c.Addr, s.cfg.ChainParams.DefaultPort)
|
|
connMgr := s.cfg.ConnMgr
|
|
var err error
|
|
switch c.SubCmd {
|
|
case "add":
|
|
err = connMgr.Connect(addr, true)
|
|
case "remove":
|
|
err = connMgr.RemoveByAddr(addr)
|
|
case "onetry":
|
|
err = connMgr.Connect(addr, false)
|
|
default:
|
|
return nil, rpcInvalidError("Invalid subcommand for addnode")
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, rpcInvalidError("%v: %v", c.SubCmd, err)
|
|
}
|
|
|
|
// no data returned unless an error.
|
|
return nil, nil
|
|
}
|
|
|
|
// handleNode handles node commands.
|
|
func handleNode(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.NodeCmd)
|
|
|
|
connMgr := s.cfg.ConnMgr
|
|
|
|
var addr string
|
|
var nodeID uint64
|
|
var errN, err error
|
|
params := s.cfg.ChainParams
|
|
switch c.SubCmd {
|
|
case "disconnect":
|
|
// If we have a valid uint disconnect by node id. Otherwise,
|
|
// attempt to disconnect by address, returning an error if a
|
|
// valid IP address is not supplied.
|
|
if nodeID, errN = strconv.ParseUint(c.Target, 10, 32); errN == nil {
|
|
err = connMgr.DisconnectByID(int32(nodeID))
|
|
} else {
|
|
if _, _, errP := net.SplitHostPort(c.Target); errP == nil ||
|
|
net.ParseIP(c.Target) != nil {
|
|
addr = normalizeAddress(c.Target, params.DefaultPort)
|
|
err = connMgr.DisconnectByAddr(addr)
|
|
} else {
|
|
return nil, rpcInvalidError("%v: Invalid "+
|
|
"address or node ID", c.SubCmd)
|
|
}
|
|
}
|
|
if err != nil && peerExists(connMgr, addr, int32(nodeID)) {
|
|
return nil, rpcMiscError("can't disconnect a permanent peer, " +
|
|
"use remove")
|
|
}
|
|
|
|
case "remove":
|
|
// If we have a valid uint disconnect by node id. Otherwise,
|
|
// attempt to disconnect by address, returning an error if a
|
|
// valid IP address is not supplied.
|
|
if nodeID, errN = strconv.ParseUint(c.Target, 10, 32); errN == nil {
|
|
err = connMgr.RemoveByID(int32(nodeID))
|
|
} else {
|
|
if _, _, errP := net.SplitHostPort(c.Target); errP == nil ||
|
|
net.ParseIP(c.Target) != nil {
|
|
addr = normalizeAddress(c.Target, params.DefaultPort)
|
|
err = connMgr.RemoveByAddr(addr)
|
|
} else {
|
|
return nil, rpcInvalidError("%v: invalid "+
|
|
"address or node ID", c.SubCmd)
|
|
}
|
|
}
|
|
if err != nil && peerExists(connMgr, addr, int32(nodeID)) {
|
|
return nil, rpcMiscError("can't remove a temporary peer, " +
|
|
"use disconnect")
|
|
}
|
|
|
|
case "connect":
|
|
addr = normalizeAddress(c.Target, params.DefaultPort)
|
|
|
|
// Default to temporary connections.
|
|
subCmd := "temp"
|
|
if c.ConnectSubCmd != nil {
|
|
subCmd = *c.ConnectSubCmd
|
|
}
|
|
|
|
switch subCmd {
|
|
case "perm", "temp":
|
|
err = connMgr.Connect(addr, subCmd == "perm")
|
|
default:
|
|
return nil, rpcInvalidError("%v: invalid subcommand "+
|
|
"for node connect", subCmd)
|
|
}
|
|
default:
|
|
return nil, rpcInvalidError("%v: invalid subcommand for node", c.SubCmd)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, rpcInvalidError("%v: %v", c.SubCmd, err)
|
|
}
|
|
|
|
// no data returned unless an error.
|
|
return nil, nil
|
|
}
|
|
|
|
// peerExists determines if a certain peer is currently connected given
|
|
// information about all currently connected peers. Peer existence is
|
|
// determined using either a target address or node id.
|
|
func peerExists(connMgr ConnManager, addr string, nodeID int32) bool {
|
|
for _, p := range connMgr.ConnectedPeers() {
|
|
if p.ID() == nodeID || p.Addr() == addr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// messageToHex serializes a message to the wire protocol encoding using the
|
|
// latest protocol version and returns a hex-encoded string of the result.
|
|
func (s *Server) messageToHex(msg wire.Message) (string, error) {
|
|
var buf bytes.Buffer
|
|
if err := msg.BtcEncode(&buf, s.cfg.MaxProtocolVersion); err != nil {
|
|
context := fmt.Sprintf("Failed to encode msg of type %T", msg)
|
|
return "", rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
return hex.EncodeToString(buf.Bytes()), nil
|
|
}
|
|
|
|
// newTxOut returns a new transaction output with the given parameters.
|
|
func newTxOut(amount int64, pkScriptVer uint16, pkScript []byte) *wire.TxOut {
|
|
return &wire.TxOut{
|
|
Value: amount,
|
|
Version: pkScriptVer,
|
|
PkScript: pkScript,
|
|
}
|
|
}
|
|
|
|
// handleCreateRawTransaction handles createrawtransaction commands.
|
|
func handleCreateRawTransaction(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.CreateRawTransactionCmd)
|
|
|
|
// Validate expiry, if given.
|
|
if c.Expiry != nil && *c.Expiry < 0 {
|
|
return nil, rpcInvalidError("Expiry out of range")
|
|
}
|
|
|
|
// Validate the locktime, if given.
|
|
if c.LockTime != nil &&
|
|
(*c.LockTime < 0 ||
|
|
*c.LockTime > int64(wire.MaxTxInSequenceNum)) {
|
|
return nil, rpcInvalidError("Locktime out of range")
|
|
}
|
|
|
|
// Add all transaction inputs to a new transaction after performing
|
|
// some validity checks.
|
|
mtx := wire.NewMsgTx()
|
|
for _, input := range c.Inputs {
|
|
txHash, err := chainhash.NewHashFromStr(input.Txid)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(input.Txid)
|
|
}
|
|
|
|
if !(input.Tree == wire.TxTreeRegular ||
|
|
input.Tree == wire.TxTreeStake) {
|
|
return nil, rpcInvalidError("Tx tree must be regular " +
|
|
"or stake")
|
|
}
|
|
|
|
prevOutV := wire.NullValueIn
|
|
if input.Amount > 0 {
|
|
amt, err := dcrutil.NewAmount(input.Amount)
|
|
if err != nil {
|
|
return nil, rpcInvalidError(err.Error())
|
|
}
|
|
prevOutV = int64(amt)
|
|
}
|
|
|
|
prevOut := wire.NewOutPoint(txHash, input.Vout, input.Tree)
|
|
txIn := wire.NewTxIn(prevOut, prevOutV, []byte{})
|
|
if c.LockTime != nil && *c.LockTime != 0 {
|
|
txIn.Sequence = wire.MaxTxInSequenceNum - 1
|
|
}
|
|
mtx.AddTxIn(txIn)
|
|
}
|
|
|
|
// Add all transaction outputs to the transaction after performing
|
|
// some validity checks.
|
|
for encodedAddr, amount := range c.Amounts {
|
|
atoms, err := dcrutil.NewAmount(amount)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"New amount")
|
|
}
|
|
|
|
// Ensure amount is in the valid range for monetary amounts.
|
|
if atoms <= 0 || atoms > dcrutil.MaxAmount {
|
|
return nil, rpcInvalidError("Invalid amount: 0 >= %v "+
|
|
"> %v", amount, dcrutil.MaxAmount)
|
|
}
|
|
|
|
// Decode the provided address. This also ensures the network encoded
|
|
// with the address matches the network the server is currently on.
|
|
addr, err := stdaddr.DecodeAddress(encodedAddr, s.cfg.ChainParams)
|
|
if err != nil {
|
|
return nil, rpcAddressKeyError("Could not decode address: %v", err)
|
|
}
|
|
|
|
// Ensure the address is one of the supported types.
|
|
if _, ok := addr.(stdaddr.StakeAddress); !ok {
|
|
return nil, rpcAddressKeyError("Invalid type: %T", addr)
|
|
}
|
|
|
|
// Create a new script which pays to the provided address.
|
|
pkScriptVer, pkScript := addr.PaymentScript()
|
|
mtx.AddTxOut(newTxOut(int64(atoms), pkScriptVer, pkScript))
|
|
}
|
|
|
|
// Set the Locktime, if given.
|
|
if c.LockTime != nil {
|
|
mtx.LockTime = uint32(*c.LockTime)
|
|
}
|
|
|
|
// Set the Expiry, if given.
|
|
if c.Expiry != nil {
|
|
mtx.Expiry = uint32(*c.Expiry)
|
|
}
|
|
|
|
// 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 := s.messageToHex(mtx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return mtxHex, nil
|
|
}
|
|
|
|
// handleCreateRawSStx handles createrawsstx commands.
|
|
func handleCreateRawSStx(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.CreateRawSStxCmd)
|
|
|
|
// Basic sanity checks for the information coming from the cmd.
|
|
if len(c.Inputs) != len(c.COuts) {
|
|
return nil, rpcInvalidError("Number of inputs should be equal "+
|
|
"to the number of future commitment/change outs for "+
|
|
"any sstx; %v inputs given, but %v COuts",
|
|
len(c.Inputs), len(c.COuts))
|
|
}
|
|
if len(c.Amount) != 1 {
|
|
return nil, rpcInvalidError("Only one SSGen tagged output is "+
|
|
"allowed per sstx; len ssgenout %v", len(c.Amount))
|
|
}
|
|
|
|
// Add all transaction inputs to a new transaction after performing
|
|
// some validity checks.
|
|
mtx := wire.NewMsgTx()
|
|
for _, input := range c.Inputs {
|
|
txHash, err := chainhash.NewHashFromStr(input.Txid)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(input.Txid)
|
|
}
|
|
|
|
if !(input.Tree == wire.TxTreeRegular ||
|
|
input.Tree == wire.TxTreeStake) {
|
|
return nil, rpcInvalidError("Tx tree must be regular or stake")
|
|
}
|
|
|
|
prevOut := wire.NewOutPoint(txHash, input.Vout, input.Tree)
|
|
txIn := wire.NewTxIn(prevOut, input.Amt, nil)
|
|
mtx.AddTxIn(txIn)
|
|
}
|
|
|
|
// Add all transaction outputs to the transaction after performing
|
|
// some validity checks.
|
|
amtTicket := int64(0)
|
|
|
|
for encodedAddr, amount := range c.Amount {
|
|
// Ensure amount is in the valid range for monetary amounts.
|
|
if amount <= 0 || amount > dcrutil.MaxAmount {
|
|
return nil, rpcInvalidError("Invalid SSTx commitment "+
|
|
"amount: 0 >= %v > %v", amount,
|
|
dcrutil.MaxAmount)
|
|
}
|
|
|
|
// Decode the provided address. This also ensures the network encoded
|
|
// with the address matches the network the server is currently on.
|
|
addr, err := stdaddr.DecodeAddress(encodedAddr, s.cfg.ChainParams)
|
|
if err != nil {
|
|
return nil, rpcAddressKeyError("Could not decode address: %v", err)
|
|
}
|
|
|
|
// Ensure the address is one of the supported types.
|
|
stakeAddr, ok := addr.(stdaddr.StakeAddress)
|
|
if !ok {
|
|
return nil, rpcAddressKeyError("Invalid address type: %T", addr)
|
|
}
|
|
|
|
// Create the necessary voting rights script.
|
|
pkScriptVer, pkScript := stakeAddr.VotingRightsScript()
|
|
mtx.AddTxOut(newTxOut(amount, pkScriptVer, pkScript))
|
|
|
|
amtTicket += amount
|
|
}
|
|
|
|
// Calculated the commitment amounts, then create the
|
|
// addresses and payout proportions as null data
|
|
// outputs.
|
|
inputAmts := make([]int64, len(c.Inputs))
|
|
for i, input := range c.Inputs {
|
|
inputAmts[i] = input.Amt
|
|
}
|
|
changeAmts := make([]int64, len(c.COuts))
|
|
for i, cout := range c.COuts {
|
|
changeAmts[i] = cout.ChangeAmt
|
|
}
|
|
|
|
// Check and make sure none of the change overflows
|
|
// the input amounts.
|
|
for i, amt := range inputAmts {
|
|
if changeAmts[i] >= amt {
|
|
return nil, rpcInvalidError("input %v >= amount %v",
|
|
changeAmts[i], amt)
|
|
}
|
|
}
|
|
|
|
// Obtain the commitment amounts.
|
|
_, amountsCommitted, err := stake.SStxNullOutputAmounts(inputAmts,
|
|
changeAmts, amtTicket)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"Invalid SSTx output amounts")
|
|
}
|
|
|
|
for i, cout := range c.COuts {
|
|
// Append future commitment output. This also ensures the network
|
|
// encoded with the address matches the network the server is currently
|
|
// on.
|
|
addr, err := stdaddr.DecodeAddress(cout.Addr, s.cfg.ChainParams)
|
|
if err != nil {
|
|
return nil, rpcAddressKeyError("Could not decode address: %v", err)
|
|
}
|
|
|
|
// Ensure the address is one of the supported types.
|
|
stakeAddr, ok := addr.(stdaddr.StakeAddress)
|
|
if !ok {
|
|
return nil, rpcAddressKeyError("Invalid type: %T", addr)
|
|
}
|
|
|
|
// Create the reward commitment script.
|
|
//
|
|
// TODO: Allow fee limits to be specified with an argument.
|
|
const voteFeeLimit = 0
|
|
const revokeFeeLimit = 0
|
|
cmtScriptVer, cmtScript := stakeAddr.RewardCommitmentScript(
|
|
amountsCommitted[i], voteFeeLimit, revokeFeeLimit)
|
|
mtx.AddTxOut(newTxOut(0, cmtScriptVer, cmtScript))
|
|
|
|
// 2. Append change output.
|
|
|
|
// Ensure amount is in the valid range for monetary amounts.
|
|
if cout.ChangeAmt < 0 || cout.ChangeAmt > dcrutil.MaxAmount {
|
|
return nil, rpcInvalidError("Invalid change amount: 0 "+
|
|
"> %v > %v", cout.ChangeAmt, dcrutil.MaxAmount)
|
|
}
|
|
|
|
// Decode the provided address. This also ensures the network encoded
|
|
// with the address matches the network the server is currently on.
|
|
addr, err = stdaddr.DecodeAddress(cout.ChangeAddr, s.cfg.ChainParams)
|
|
if err != nil {
|
|
return nil, rpcAddressKeyError("Wrong network: %v",
|
|
addr)
|
|
}
|
|
|
|
// Ensure the address is one of the supported types.
|
|
stakeAddr, ok = addr.(stdaddr.StakeAddress)
|
|
if !ok {
|
|
return nil, rpcAddressKeyError("Invalid type: %T", addr)
|
|
}
|
|
|
|
// Create a new script which pays change to the provided address.
|
|
changeScriptVer, changeScript := stakeAddr.StakeChangeScript()
|
|
mtx.AddTxOut(newTxOut(cout.ChangeAmt, changeScriptVer, changeScript))
|
|
}
|
|
|
|
// Make sure we generated a valid SStx.
|
|
if err := stake.CheckSStx(mtx); err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"Invalid SStx")
|
|
}
|
|
|
|
// Return the serialized and hex-encoded transaction.
|
|
mtxHex, err := s.messageToHex(mtx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return mtxHex, nil
|
|
}
|
|
|
|
// handleCreateRawSSRtx handles createrawssrtx commands.
|
|
func handleCreateRawSSRtx(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.CreateRawSSRtxCmd)
|
|
|
|
// Only a single SStx should be given
|
|
if len(c.Inputs) != 1 {
|
|
return nil, rpcInvalidError("SSRtx invalid number of inputs")
|
|
}
|
|
|
|
// The input must be in the stake tree.
|
|
input := c.Inputs[0]
|
|
if input.Tree != wire.TxTreeStake {
|
|
return nil, rpcInvalidError("Input tree is not TxTreeStake type")
|
|
}
|
|
|
|
// The input must be a ticket submission output.
|
|
const ticketSubmissionOutput = 0
|
|
if input.Vout != ticketSubmissionOutput {
|
|
return nil, rpcInvalidError("Input is not a ticket submission output " +
|
|
"(output index 0)")
|
|
}
|
|
|
|
// 1. Fetch the SStx, then calculate all the values we'll need later
|
|
// for the generation of the SSRtx tx outputs.
|
|
//
|
|
// Convert the provided transaction hash hex to a chainhash.Hash.
|
|
txHash, err := chainhash.NewHashFromStr(input.Txid)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(input.Txid)
|
|
}
|
|
|
|
// Try to fetch the ticket from the block database.
|
|
outpoint := wire.OutPoint{Hash: *txHash, Index: input.Vout, Tree: input.Tree}
|
|
ticketUtxo, err := s.cfg.Chain.FetchUtxoEntry(outpoint)
|
|
if ticketUtxo == nil || err != nil {
|
|
return nil, rpcNoTxInfoError(txHash)
|
|
}
|
|
if t := ticketUtxo.TransactionType(); t != stake.TxTypeSStx {
|
|
return nil, rpcDeserializationError("Invalid Tx type: %v", t)
|
|
}
|
|
|
|
// Store the sstx pubkeyhashes and amounts as found in the transaction
|
|
// outputs.
|
|
minimalOutputs := ticketUtxo.TicketMinimalOutputs()
|
|
if minimalOutputs == nil {
|
|
return nil, rpcInternalError("Missing ticket minimal outputs", "")
|
|
}
|
|
|
|
// The input amount must be the ticket submission amount.
|
|
ticketSubmission := minimalOutputs[ticketSubmissionOutput]
|
|
ticketSubmissionAmount := dcrutil.Amount(ticketSubmission.Value)
|
|
inputAmount, err := dcrutil.NewAmount(input.Amount)
|
|
if err != nil {
|
|
return nil, rpcInvalidError(err.Error())
|
|
}
|
|
if inputAmount != ticketSubmissionAmount {
|
|
return nil, rpcInvalidError("Input amount %v is not equal to ticket "+
|
|
"submission amount %v", inputAmount, ticketSubmissionAmount)
|
|
}
|
|
|
|
// Decode the fee as coins.
|
|
var feeAmt dcrutil.Amount
|
|
if c.Fee != nil {
|
|
var err error
|
|
feeAmt, err = dcrutil.NewAmount(*c.Fee)
|
|
if err != nil {
|
|
return nil, rpcInvalidError("Invalid fee amount: %v", err)
|
|
}
|
|
}
|
|
|
|
// Determine if the automatic ticket revocations agenda is active.
|
|
prevBlkHash := s.cfg.Chain.BestSnapshot().Hash
|
|
isAutoRevocationsEnabled, err := s.isAutoRevocationsAgendaActive(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If the automatic ticket revocations agenda is active, validate that the fee
|
|
// amount is zero and set the transaction version to 2.
|
|
revocationTxVersion := uint16(1)
|
|
if isAutoRevocationsEnabled {
|
|
if feeAmt != 0 {
|
|
return nil, rpcInvalidError("Fee amount must be 0 when the automatic " +
|
|
"ticket revocations agenda is active")
|
|
}
|
|
revocationTxVersion = stake.TxVersionAutoRevocations
|
|
}
|
|
|
|
// Get the previous header bytes.
|
|
prevHeader, err := s.cfg.Chain.HeaderByHash(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, rpcBlockNotFoundError(prevBlkHash)
|
|
}
|
|
prevHeaderBytes, err := prevHeader.Bytes()
|
|
if err != nil {
|
|
str := fmt.Sprintf("Failed to serialize header for block %v", prevBlkHash)
|
|
return nil, rpcInternalError(err.Error(), str)
|
|
}
|
|
|
|
mtx, err := stake.CreateRevocationFromTicket(txHash, minimalOutputs, feeAmt,
|
|
revocationTxVersion, s.cfg.ChainParams, prevHeaderBytes,
|
|
isAutoRevocationsEnabled)
|
|
if err != nil {
|
|
return nil, rpcInvalidError(err.Error(), "Invalid SSRtx")
|
|
}
|
|
|
|
// Check to make sure our SSRtx was created correctly.
|
|
err = stake.CheckSSRtx(mtx, isAutoRevocationsEnabled)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Invalid SSRtx")
|
|
}
|
|
|
|
// Return the serialized and hex-encoded transaction.
|
|
mtxHex, err := s.messageToHex(mtx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return mtxHex, nil
|
|
}
|
|
|
|
// handleDebugLevel handles debuglevel commands.
|
|
func handleDebugLevel(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.DebugLevelCmd)
|
|
|
|
// Special show command to list supported subsystems.
|
|
if c.LevelSpec == "show" {
|
|
return fmt.Sprintf("Supported subsystems %v",
|
|
s.cfg.LogManager.SupportedSubsystems()), nil
|
|
}
|
|
|
|
err := s.cfg.LogManager.ParseAndSetDebugLevels(c.LevelSpec)
|
|
if err != nil {
|
|
return nil, rpcInvalidError("Invalid debug level %v: %v",
|
|
c.LevelSpec, err)
|
|
}
|
|
|
|
return "Done.", nil
|
|
}
|
|
|
|
// createVinList returns a slice of JSON objects for the inputs of the passed
|
|
// transaction.
|
|
func createVinList(mtx *wire.MsgTx, isTreasuryEnabled bool) []types.Vin {
|
|
// Treasurybase transactions only have a single txin by definition.
|
|
//
|
|
// NOTE: This check MUST come before the coinbase check because a
|
|
// treasurybase will be identified as a coinbase as well.
|
|
vinList := make([]types.Vin, len(mtx.TxIn))
|
|
if isTreasuryEnabled && standalone.IsTreasuryBase(mtx) {
|
|
txIn := mtx.TxIn[0]
|
|
vinEntry := &vinList[0]
|
|
vinEntry.Treasurybase = true
|
|
vinEntry.Sequence = txIn.Sequence
|
|
vinEntry.AmountIn = dcrutil.Amount(txIn.ValueIn).ToCoin()
|
|
vinEntry.BlockHeight = txIn.BlockHeight
|
|
vinEntry.BlockIndex = txIn.BlockIndex
|
|
return vinList
|
|
}
|
|
|
|
// Coinbase transactions only have a single txin by definition.
|
|
if standalone.IsCoinBaseTx(mtx, isTreasuryEnabled) {
|
|
txIn := mtx.TxIn[0]
|
|
vinEntry := &vinList[0]
|
|
vinEntry.Coinbase = hex.EncodeToString(txIn.SignatureScript)
|
|
vinEntry.Sequence = txIn.Sequence
|
|
vinEntry.AmountIn = dcrutil.Amount(txIn.ValueIn).ToCoin()
|
|
vinEntry.BlockHeight = txIn.BlockHeight
|
|
vinEntry.BlockIndex = txIn.BlockIndex
|
|
return vinList
|
|
}
|
|
|
|
// Treasury spend transactions only have a single txin by definition.
|
|
if isTreasuryEnabled && stake.IsTSpend(mtx) {
|
|
txIn := mtx.TxIn[0]
|
|
vinEntry := &vinList[0]
|
|
vinEntry.TreasurySpend = hex.EncodeToString(txIn.SignatureScript)
|
|
vinEntry.Sequence = txIn.Sequence
|
|
vinEntry.AmountIn = dcrutil.Amount(txIn.ValueIn).ToCoin()
|
|
vinEntry.BlockHeight = txIn.BlockHeight
|
|
vinEntry.BlockIndex = txIn.BlockIndex
|
|
return vinList
|
|
}
|
|
|
|
// Stakebase transactions (votes) have two inputs: a null stake base
|
|
// followed by an input consuming a ticket's stakesubmission.
|
|
isSSGen := stake.IsSSGen(mtx, isTreasuryEnabled)
|
|
|
|
for i, txIn := range mtx.TxIn {
|
|
// Handle only the null input of a stakebase differently.
|
|
if isSSGen && i == 0 {
|
|
vinEntry := &vinList[0]
|
|
vinEntry.Stakebase = hex.EncodeToString(txIn.SignatureScript)
|
|
vinEntry.Sequence = txIn.Sequence
|
|
vinEntry.AmountIn = dcrutil.Amount(txIn.ValueIn).ToCoin()
|
|
vinEntry.BlockHeight = txIn.BlockHeight
|
|
vinEntry.BlockIndex = txIn.BlockIndex
|
|
continue
|
|
}
|
|
|
|
// The disassembled string will contain [error] inline
|
|
// if the script doesn't fully parse, so ignore the
|
|
// error here.
|
|
disbuf, _ := txscript.DisasmString(txIn.SignatureScript)
|
|
|
|
vinEntry := &vinList[i]
|
|
vinEntry.Txid = txIn.PreviousOutPoint.Hash.String()
|
|
vinEntry.Vout = txIn.PreviousOutPoint.Index
|
|
vinEntry.Tree = txIn.PreviousOutPoint.Tree
|
|
vinEntry.Sequence = txIn.Sequence
|
|
vinEntry.AmountIn = dcrutil.Amount(txIn.ValueIn).ToCoin()
|
|
vinEntry.BlockHeight = txIn.BlockHeight
|
|
vinEntry.BlockIndex = txIn.BlockIndex
|
|
vinEntry.ScriptSig = &types.ScriptSig{
|
|
Asm: disbuf,
|
|
Hex: hex.EncodeToString(txIn.SignatureScript),
|
|
}
|
|
}
|
|
|
|
return vinList
|
|
}
|
|
|
|
// createVoutList returns a slice of JSON objects for the outputs of the passed
|
|
// transaction.
|
|
func createVoutList(mtx *wire.MsgTx, chainParams *chaincfg.Params,
|
|
filterAddrMap map[string]struct{}, isTreasuryEnabled,
|
|
isAutoRevocationsEnabled bool) []types.Vout {
|
|
|
|
txType := stake.DetermineTxType(mtx, isTreasuryEnabled,
|
|
isAutoRevocationsEnabled)
|
|
voutList := make([]types.Vout, 0, len(mtx.TxOut))
|
|
for i, v := range mtx.TxOut {
|
|
// The disassembled string will contain [error] inline if the
|
|
// script doesn't fully parse, so ignore the error here.
|
|
disbuf, _ := txscript.DisasmString(v.PkScript)
|
|
|
|
// Attempt to extract addresses from the public key script. In
|
|
// the case of stake submission transactions, the odd outputs
|
|
// contain a commitment address, so detect that case
|
|
// accordingly.
|
|
var addrs []stdaddr.Address
|
|
var scriptType string
|
|
var reqSigs uint16
|
|
var commitAmt *dcrutil.Amount
|
|
if txType == stake.TxTypeSStx && (i%2 != 0) {
|
|
scriptType = sstxCommitmentString
|
|
addr, err := stake.AddrFromSStxPkScrCommitment(v.PkScript,
|
|
chainParams)
|
|
if err != nil {
|
|
log.Warnf("failed to decode ticket "+
|
|
"commitment addr output for tx hash "+
|
|
"%v, output idx %v", mtx.TxHash(), i)
|
|
} else {
|
|
addrs = []stdaddr.Address{addr}
|
|
}
|
|
amt, err := stake.AmountFromSStxPkScrCommitment(v.PkScript)
|
|
if err != nil {
|
|
log.Warnf("failed to decode ticket "+
|
|
"commitment amt output for tx hash %v"+
|
|
", output idx %v", mtx.TxHash(), i)
|
|
} else {
|
|
commitAmt = &amt
|
|
}
|
|
} else {
|
|
// Attempt to extract known addresses associated with the script.
|
|
var st stdscript.ScriptType
|
|
st, addrs = stdscript.ExtractAddrs(v.Version, v.PkScript, chainParams)
|
|
scriptType = st.String()
|
|
|
|
// Determine the number of required signatures for known standard
|
|
// types.
|
|
reqSigs = stdscript.DetermineRequiredSigs(v.Version, v.PkScript)
|
|
}
|
|
|
|
// Encode the addresses while checking if the address passes the
|
|
// filter when needed.
|
|
passesFilter := len(filterAddrMap) == 0
|
|
encodedAddrs := make([]string, len(addrs))
|
|
for j, addr := range addrs {
|
|
encodedAddr := addr.String()
|
|
encodedAddrs[j] = encodedAddr
|
|
|
|
// No need to check the map again if the filter already
|
|
// passes.
|
|
if passesFilter {
|
|
continue
|
|
}
|
|
if _, exists := filterAddrMap[encodedAddr]; exists {
|
|
passesFilter = true
|
|
}
|
|
}
|
|
|
|
if !passesFilter {
|
|
continue
|
|
}
|
|
|
|
var vout types.Vout
|
|
voutSPK := &vout.ScriptPubKey
|
|
vout.N = uint32(i)
|
|
vout.Value = dcrutil.Amount(v.Value).ToCoin()
|
|
vout.Version = v.Version
|
|
voutSPK.Addresses = encodedAddrs
|
|
voutSPK.Asm = disbuf
|
|
voutSPK.Hex = hex.EncodeToString(v.PkScript)
|
|
voutSPK.Type = scriptType
|
|
voutSPK.ReqSigs = int32(reqSigs)
|
|
if commitAmt != nil {
|
|
voutSPK.CommitAmt = dcrjson.Float64(commitAmt.ToCoin())
|
|
}
|
|
voutSPK.Version = v.Version
|
|
|
|
voutList = append(voutList, vout)
|
|
}
|
|
|
|
return voutList
|
|
}
|
|
|
|
// createTxRawResult converts the passed transaction and associated parameters
|
|
// to a raw transaction JSON object.
|
|
func (s *Server) createTxRawResult(chainParams *chaincfg.Params,
|
|
mtx *wire.MsgTx, txHash string, blkIdx uint32, blkHeader *wire.BlockHeader,
|
|
blkHash string, blkHeight int64, confirmations int64, isTreasuryEnabled,
|
|
isAutoRevocationsEnabled bool) (*types.TxRawResult, error) {
|
|
|
|
mtxHex, err := s.messageToHex(mtx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if txHash != mtx.TxHash().String() {
|
|
return nil, rpcInvalidError("Tx hash does not match: got %v "+
|
|
"expected %v", txHash, mtx.TxHash())
|
|
}
|
|
|
|
txReply := &types.TxRawResult{
|
|
Hex: mtxHex,
|
|
Txid: txHash,
|
|
Vin: createVinList(mtx, isTreasuryEnabled),
|
|
Vout: createVoutList(mtx, chainParams, nil, isTreasuryEnabled,
|
|
isAutoRevocationsEnabled),
|
|
Version: int32(mtx.Version),
|
|
LockTime: mtx.LockTime,
|
|
Expiry: mtx.Expiry,
|
|
BlockHeight: blkHeight,
|
|
BlockIndex: blkIdx,
|
|
}
|
|
|
|
if blkHeader != nil {
|
|
// This is not a typo, they are identical in bitcoind as well.
|
|
txReply.Time = blkHeader.Timestamp.Unix()
|
|
txReply.Blocktime = blkHeader.Timestamp.Unix()
|
|
txReply.BlockHash = blkHash
|
|
txReply.Confirmations = confirmations
|
|
}
|
|
|
|
return txReply, nil
|
|
}
|
|
|
|
// handleDecodeRawTransaction handles decoderawtransaction commands.
|
|
func handleDecodeRawTransaction(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.DecodeRawTransactionCmd)
|
|
|
|
// Deserialize the transaction.
|
|
hexStr := c.HexTx
|
|
if len(hexStr)%2 != 0 {
|
|
hexStr = "0" + hexStr
|
|
}
|
|
serializedTx, err := hex.DecodeString(hexStr)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(hexStr)
|
|
}
|
|
var mtx wire.MsgTx
|
|
err = mtx.Deserialize(bytes.NewReader(serializedTx))
|
|
if err != nil {
|
|
return nil, rpcDeserializationError("Could not decode Tx: %v",
|
|
err)
|
|
}
|
|
|
|
// Determine if the treasury rules are active as of the current best tip.
|
|
prevBlkHash := s.cfg.Chain.BestSnapshot().Hash
|
|
isTreasuryEnabled, err := s.isTreasuryAgendaActive(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Determine if the automatic ticket revocations agenda is active.
|
|
isAutoRevocationsEnabled, err := s.isAutoRevocationsAgendaActive(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create and return the result.
|
|
txReply := types.TxRawDecodeResult{
|
|
Txid: mtx.TxHash().String(),
|
|
Version: int32(mtx.Version),
|
|
Locktime: mtx.LockTime,
|
|
Expiry: mtx.Expiry,
|
|
Vin: createVinList(&mtx, isTreasuryEnabled),
|
|
Vout: createVoutList(&mtx, s.cfg.ChainParams, nil, isTreasuryEnabled,
|
|
isAutoRevocationsEnabled),
|
|
}
|
|
return txReply, nil
|
|
}
|
|
|
|
// handleDecodeScript handles decodescript commands.
|
|
func handleDecodeScript(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.DecodeScriptCmd)
|
|
|
|
// Convert the hex script to bytes.
|
|
hexStr := c.HexScript
|
|
if len(hexStr)%2 != 0 {
|
|
hexStr = "0" + hexStr
|
|
}
|
|
script, err := hex.DecodeString(hexStr)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(hexStr)
|
|
}
|
|
|
|
// Fetch the script version if provided.
|
|
scriptVersion := uint16(0)
|
|
if c.Version != nil {
|
|
scriptVersion = *c.Version
|
|
}
|
|
|
|
// The disassembled string will contain [error] inline if the script
|
|
// doesn't fully parse, so ignore the error here.
|
|
disbuf, _ := txscript.DisasmString(script)
|
|
|
|
// Attempt to extract known addresses associated with the script.
|
|
scriptType, addrs := stdscript.ExtractAddrs(scriptVersion, script,
|
|
s.cfg.ChainParams)
|
|
addresses := make([]string, len(addrs))
|
|
for i, addr := range addrs {
|
|
if pkHasher, ok := addr.(stdaddr.AddressPubKeyHasher); ok {
|
|
addr = pkHasher.AddressPubKeyHash()
|
|
}
|
|
addresses[i] = addr.String()
|
|
}
|
|
|
|
// Determine the number of required signatures for known standard types.
|
|
reqSigs := stdscript.DetermineRequiredSigs(scriptVersion, script)
|
|
|
|
// Convert the script itself to a pay-to-script-hash address.
|
|
p2sh, err := stdaddr.NewAddressScriptHash(scriptVersion, script,
|
|
s.cfg.ChainParams)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"Failed to convert script to pay-to-script-hash")
|
|
}
|
|
|
|
// Generate and return the reply.
|
|
reply := types.DecodeScriptResult{
|
|
Asm: disbuf,
|
|
ReqSigs: int32(reqSigs),
|
|
Type: scriptType.String(),
|
|
Addresses: addresses,
|
|
}
|
|
if scriptType != stdscript.STScriptHash {
|
|
reply.P2sh = p2sh.String()
|
|
}
|
|
return reply, nil
|
|
}
|
|
|
|
// handleEstimateFee implements the estimatefee command.
|
|
// TODO this is a very basic implementation. It should be
|
|
// modified to match the bitcoin-core one.
|
|
func handleEstimateFee(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
return s.cfg.MinRelayTxFee.ToCoin(), nil
|
|
}
|
|
|
|
// handleEstimateSmartFee implements the estimatesmartfee command.
|
|
//
|
|
// The default estimation mode when unset is assumed as "conservative". As of
|
|
// 2018-12, the only supported mode is "conservative".
|
|
func handleEstimateSmartFee(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.EstimateSmartFeeCmd)
|
|
|
|
mode := types.EstimateSmartFeeConservative
|
|
if c.Mode != nil {
|
|
mode = *c.Mode
|
|
}
|
|
|
|
if mode != types.EstimateSmartFeeConservative {
|
|
return nil, rpcInvalidError("Only the default and conservative modes " +
|
|
"are supported for smart fee estimation at the moment")
|
|
}
|
|
|
|
fee, err := s.cfg.FeeEstimator.EstimateFee(int32(c.Confirmations))
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Could not estimate fee")
|
|
}
|
|
|
|
return &types.EstimateSmartFeeResult{
|
|
FeeRate: fee.ToCoin(),
|
|
Blocks: c.Confirmations,
|
|
}, nil
|
|
}
|
|
|
|
// handleEstimateStakeDiff implements the estimatestakediff command.
|
|
func handleEstimateStakeDiff(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.EstimateStakeDiffCmd)
|
|
|
|
// Minimum possible stake difficulty.
|
|
chain := s.cfg.Chain
|
|
best := chain.BestSnapshot()
|
|
min, err := chain.EstimateNextStakeDifficulty(&best.Hash, 0, false)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Could not "+
|
|
"estimate next minimum stake difficulty")
|
|
}
|
|
|
|
// Maximum possible stake difficulty.
|
|
max, err := chain.EstimateNextStakeDifficulty(&best.Hash, 0, true)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Could not "+
|
|
"estimate next maximum stake difficulty")
|
|
}
|
|
|
|
// The expected stake difficulty. Average the number of fresh stake
|
|
// since the last retarget to get the number of tickets per block,
|
|
// then use that to estimate the next stake difficulty.
|
|
params := s.cfg.ChainParams
|
|
bestHeight := best.Height
|
|
lastAdjustment := (bestHeight / params.StakeDiffWindowSize) *
|
|
params.StakeDiffWindowSize
|
|
nextAdjustment := ((bestHeight / params.StakeDiffWindowSize) + 1) *
|
|
params.StakeDiffWindowSize
|
|
totalTickets := 0
|
|
for i := lastAdjustment; i <= bestHeight; i++ {
|
|
bh, err := chain.HeaderByHeight(i)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Could not "+
|
|
"estimate next stake difficulty")
|
|
}
|
|
totalTickets += int(bh.FreshStake)
|
|
}
|
|
blocksSince := float64(bestHeight - lastAdjustment + 1)
|
|
remaining := float64(nextAdjustment - bestHeight - 1)
|
|
averagePerBlock := float64(totalTickets) / blocksSince
|
|
expectedTickets := int64(math.Floor(averagePerBlock * remaining))
|
|
expected, err := chain.EstimateNextStakeDifficulty(&best.Hash,
|
|
expectedTickets, false)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Could not "+
|
|
"estimate next stake difficulty")
|
|
}
|
|
|
|
// User-specified stake difficulty, if they asked for one.
|
|
var userEstFltPtr *float64
|
|
if c.Tickets != nil {
|
|
userEst, err := chain.EstimateNextStakeDifficulty(&best.Hash,
|
|
int64(*c.Tickets), false)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Could not "+
|
|
"estimate next user specified stake difficulty")
|
|
}
|
|
userEstFlt := dcrutil.Amount(userEst).ToCoin()
|
|
userEstFltPtr = &userEstFlt
|
|
}
|
|
|
|
return &types.EstimateStakeDiffResult{
|
|
Min: dcrutil.Amount(min).ToCoin(),
|
|
Max: dcrutil.Amount(max).ToCoin(),
|
|
Expected: dcrutil.Amount(expected).ToCoin(),
|
|
User: userEstFltPtr,
|
|
}, nil
|
|
}
|
|
|
|
// handleExistsAddress implements the existsaddress command.
|
|
func handleExistsAddress(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
if s.cfg.ExistsAddresser == nil {
|
|
return nil, rpcInternalError("Exists address index disabled",
|
|
"Configuration")
|
|
}
|
|
|
|
c := cmd.(*types.ExistsAddressCmd)
|
|
|
|
// Decode the provided address. This also ensures the network encoded with
|
|
// the address matches the network the server is currently on.
|
|
addr, err := stdaddr.DecodeAddress(c.Address, s.cfg.ChainParams)
|
|
if err != nil {
|
|
return nil, rpcAddressKeyError("Could not decode address: %v",
|
|
err)
|
|
}
|
|
|
|
// Ensure the exists address index is synced.
|
|
existsAddrIndex := s.cfg.ExistsAddresser
|
|
tHeight, tHash, err := existsAddrIndex.Tip()
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Tip")
|
|
}
|
|
|
|
chain := s.cfg.Chain
|
|
|
|
// Return an out-of-sync error if index is lagging a
|
|
// maximum reorg depth (6) blocks or more from the chain tip.
|
|
if chain.BestSnapshot().Height > (tHeight + 5) {
|
|
msg := fmt.Sprintf("%s: index not synced", existsAddrIndex.Name())
|
|
return nil, rpcInternalError(msg, "Sync")
|
|
}
|
|
|
|
sync:
|
|
for !chain.BestSnapshot().Hash.IsEqual(tHash) {
|
|
select {
|
|
case <-time.After(syncWait):
|
|
msg := fmt.Sprintf("%s: index not synced", existsAddrIndex.Name())
|
|
return nil, rpcInternalError(msg, "Sync")
|
|
case <-existsAddrIndex.WaitForSync():
|
|
break sync
|
|
}
|
|
}
|
|
|
|
exists, err := existsAddrIndex.ExistsAddress(addr)
|
|
if err != nil {
|
|
return nil, rpcInvalidError("Could not query address: %v", err)
|
|
}
|
|
|
|
return exists, nil
|
|
}
|
|
|
|
// handleExistsAddresses implements the existsaddresses command.
|
|
//
|
|
// TODO: Add an upper bound to the number of addresses that can be checked.
|
|
// This will come with a major RPC version bump.
|
|
func handleExistsAddresses(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
if s.cfg.ExistsAddresser == nil {
|
|
return nil, rpcInternalError("Exists address index disabled",
|
|
"Configuration")
|
|
}
|
|
|
|
c := cmd.(*types.ExistsAddressesCmd)
|
|
addresses := make([]stdaddr.Address, len(c.Addresses))
|
|
for i := range c.Addresses {
|
|
// Decode the provided address. This also ensures the network encoded
|
|
// with the address matches the network the server is currently on.
|
|
addr, err := stdaddr.DecodeAddress(c.Addresses[i], s.cfg.ChainParams)
|
|
if err != nil {
|
|
return nil, rpcAddressKeyError("Could not decode address: %v", err)
|
|
}
|
|
addresses[i] = addr
|
|
}
|
|
|
|
// Ensure the exists address index is synced.
|
|
existsAddrIndex := s.cfg.ExistsAddresser
|
|
tHeight, tHash, err := existsAddrIndex.Tip()
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Tip")
|
|
}
|
|
|
|
chain := s.cfg.Chain
|
|
|
|
// Return an out-of-sync error if index is lagging a
|
|
// maximum reorg depth (6) blocks or more from the chain tip.
|
|
if chain.BestSnapshot().Height > (tHeight + 5) {
|
|
msg := fmt.Sprintf("%s: index not synced", existsAddrIndex.Name())
|
|
return nil, rpcInternalError(msg, "Sync")
|
|
}
|
|
|
|
sync:
|
|
for !chain.BestSnapshot().Hash.IsEqual(tHash) {
|
|
select {
|
|
case <-time.After(syncWait):
|
|
msg := fmt.Sprintf("%s: index not synced", existsAddrIndex.Name())
|
|
return nil, rpcInternalError(msg, "Sync")
|
|
case <-existsAddrIndex.WaitForSync():
|
|
break sync
|
|
}
|
|
}
|
|
|
|
exists, err := existsAddrIndex.ExistsAddresses(addresses)
|
|
if err != nil {
|
|
return nil, rpcInvalidError("Could not query address: %v", err)
|
|
}
|
|
|
|
// Convert the slice of bools into a compacted set of bit flags.
|
|
set := bitset.NewBytes(len(c.Addresses))
|
|
for i := range exists {
|
|
if exists[i] {
|
|
set.Set(i)
|
|
}
|
|
}
|
|
|
|
return hex.EncodeToString([]byte(set)), nil
|
|
}
|
|
|
|
func decodeHashes(strs []string) ([]chainhash.Hash, error) {
|
|
hashes := make([]chainhash.Hash, len(strs))
|
|
for i, s := range strs {
|
|
if len(s) != 2*chainhash.HashSize {
|
|
return nil, rpcDecodeHexError(s)
|
|
}
|
|
_, err := hex.Decode(hashes[i][:], []byte(s))
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(s)
|
|
}
|
|
// unreverse hash string bytes
|
|
for j := 0; j < 16; j++ {
|
|
hashes[i][j], hashes[i][31-j] = hashes[i][31-j], hashes[i][j]
|
|
}
|
|
}
|
|
return hashes, nil
|
|
}
|
|
|
|
func decodeHashPointers(strs []string) ([]*chainhash.Hash, error) {
|
|
hashes := make([]*chainhash.Hash, len(strs))
|
|
for i, s := range strs {
|
|
h, err := chainhash.NewHashFromStr(s)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(s)
|
|
}
|
|
hashes[i] = h
|
|
}
|
|
return hashes, nil
|
|
}
|
|
|
|
// handleExistsMissedTickets implements the existsmissedtickets command.
|
|
//
|
|
// TODO: Add an upper bound to the number of hashes that can be checked. This
|
|
// will come with a major RPC version bump.
|
|
func handleExistsMissedTickets(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.ExistsMissedTicketsCmd)
|
|
|
|
hashes, err := decodeHashes(c.TxHashes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
exists := s.cfg.Chain.CheckMissedTickets(hashes)
|
|
if len(exists) != len(hashes) {
|
|
return nil, rpcInvalidError("Invalid missed ticket count "+
|
|
"got %v, want %v", len(exists), len(hashes))
|
|
}
|
|
|
|
// Convert the slice of bools into a compacted set of bit flags.
|
|
set := bitset.NewBytes(len(hashes))
|
|
for i := range exists {
|
|
if exists[i] {
|
|
set.Set(i)
|
|
}
|
|
}
|
|
|
|
return hex.EncodeToString([]byte(set)), nil
|
|
}
|
|
|
|
// handleExistsExpiredTickets implements the existsexpiredtickets command.
|
|
//
|
|
// TODO: Add an upper bound to the number of hashes that can be checked. This
|
|
// will come with a major RPC version bump.
|
|
func handleExistsExpiredTickets(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.ExistsExpiredTicketsCmd)
|
|
|
|
hashes, err := decodeHashes(c.TxHashes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
exists := s.cfg.Chain.CheckExpiredTickets(hashes)
|
|
if len(exists) != len(hashes) {
|
|
return nil, rpcInvalidError("Invalid expired ticket count "+
|
|
"got %v, want %v", len(exists), len(hashes))
|
|
}
|
|
|
|
// Convert the slice of bools into a compacted set of bit flags.
|
|
set := bitset.NewBytes(len(hashes))
|
|
for i := range exists {
|
|
if exists[i] {
|
|
set.Set(i)
|
|
}
|
|
}
|
|
|
|
return hex.EncodeToString([]byte(set)), nil
|
|
}
|
|
|
|
// handleExistsLiveTicket implements the existsliveticket command.
|
|
func handleExistsLiveTicket(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.ExistsLiveTicketCmd)
|
|
|
|
hash, err := chainhash.NewHashFromStr(c.TxHash)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(c.TxHash)
|
|
}
|
|
|
|
return s.cfg.Chain.CheckLiveTicket(*hash), nil
|
|
}
|
|
|
|
// handleExistsLiveTickets implements the existslivetickets command.
|
|
//
|
|
// TODO: Add an upper bound to the number of hashes that can be checked. This
|
|
// will come with a major RPC version bump.
|
|
func handleExistsLiveTickets(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.ExistsLiveTicketsCmd)
|
|
|
|
hashes, err := decodeHashes(c.TxHashes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
exists := s.cfg.Chain.CheckLiveTickets(hashes)
|
|
if len(exists) != len(hashes) {
|
|
return nil, rpcInvalidError("Invalid live ticket count got "+
|
|
"%v, want %v", len(exists), len(hashes))
|
|
}
|
|
|
|
// Convert the slice of bools into a compacted set of bit flags.
|
|
set := bitset.NewBytes(len(hashes))
|
|
for i := range exists {
|
|
if exists[i] {
|
|
set.Set(i)
|
|
}
|
|
}
|
|
|
|
return hex.EncodeToString([]byte(set)), nil
|
|
}
|
|
|
|
// handleExistsMempoolTxs implements the existsmempooltxs command.
|
|
//
|
|
// TODO: Add an upper bound to the number of hashes that can be checked. This
|
|
// will come with a major RPC version bump.
|
|
func handleExistsMempoolTxs(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.ExistsMempoolTxsCmd)
|
|
|
|
hashes, err := decodeHashPointers(c.TxHashes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
exists := s.cfg.TxMempooler.HaveTransactions(hashes)
|
|
if len(exists) != len(hashes) {
|
|
return nil, rpcInternalError(fmt.Sprintf("got %v, want %v",
|
|
len(exists), len(hashes)),
|
|
"Invalid mempool Tx ticket count")
|
|
}
|
|
|
|
// Convert the slice of bools into a compacted set of bit flags.
|
|
set := bitset.NewBytes(len(hashes))
|
|
for i := range exists {
|
|
if exists[i] {
|
|
set.Set(i)
|
|
}
|
|
}
|
|
|
|
return hex.EncodeToString([]byte(set)), nil
|
|
}
|
|
|
|
// handleGenerate handles generate commands.
|
|
func handleGenerate(ctx context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
// Respond with an error if there are no addresses to pay the
|
|
// created blocks to.
|
|
if len(s.cfg.MiningAddrs) == 0 {
|
|
return nil, rpcInternalError("No payment addresses specified "+
|
|
"via --miningaddr", "Configuration")
|
|
}
|
|
|
|
// Respond with an error if there's virtually 0 chance of CPU-mining a block.
|
|
params := s.cfg.ChainParams
|
|
if !params.GenerateSupported {
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCDifficulty,
|
|
Message: fmt.Sprintf("No support for `generate` on the current "+
|
|
"network, %s, as it's unlikely to be possible to mine a block "+
|
|
"with the CPU.", params.Net),
|
|
}
|
|
}
|
|
|
|
c := cmd.(*types.GenerateCmd)
|
|
|
|
// Respond with an error if the client is requesting 0 blocks to be generated.
|
|
if c.NumBlocks == 0 {
|
|
return nil, rpcInternalError("Invalid number of blocks",
|
|
"Configuration")
|
|
}
|
|
|
|
// Mine the correct number of blocks, assigning the hex representation of
|
|
// the hash of each one to its place in the reply.
|
|
blockHashes, err := s.cfg.CPUMiner.GenerateNBlocks(ctx, c.NumBlocks)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Could not generate blocks")
|
|
}
|
|
reply := make([]string, 0, len(blockHashes))
|
|
for _, hash := range blockHashes {
|
|
reply = append(reply, hash.String())
|
|
}
|
|
return reply, nil
|
|
}
|
|
|
|
// handleGetAddedNodeInfo handles getaddednodeinfo commands.
|
|
func handleGetAddedNodeInfo(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.GetAddedNodeInfoCmd)
|
|
|
|
// Retrieve a list of persistent (added) peers from the Decred server
|
|
// and filter the list of peers per the specified address (if any).
|
|
peers := s.cfg.ConnMgr.AddedNodeInfo()
|
|
if c.Node != nil {
|
|
found := false
|
|
for i, peer := range peers {
|
|
if peer.Addr() == *c.Node {
|
|
peers = peers[i : i+1]
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return nil, rpcInternalError("Node not found", "")
|
|
}
|
|
}
|
|
|
|
// Without the dns flag, the result is just a slice of the addresses as
|
|
// strings.
|
|
if !c.DNS {
|
|
results := make([]string, 0, len(peers))
|
|
for _, peer := range peers {
|
|
results = append(results, peer.Addr())
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// With the dns flag, the result is an array of JSON objects which
|
|
// include the result of DNS lookups for each peer.
|
|
results := make([]*types.GetAddedNodeInfoResult, 0, len(peers))
|
|
for _, peer := range peers {
|
|
// Set the "address" of the peer which could be an ip address
|
|
// or a domain name.
|
|
var result types.GetAddedNodeInfoResult
|
|
result.AddedNode = peer.Addr()
|
|
result.Connected = dcrjson.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
|
|
// the address, just use the address as the host.
|
|
host, _, err := net.SplitHostPort(peer.Addr())
|
|
if err != nil {
|
|
host = peer.Addr()
|
|
}
|
|
|
|
// Do a DNS lookup for the address. If the lookup fails, just
|
|
// use the host.
|
|
var ipList []string
|
|
ips, err := s.cfg.ConnMgr.Lookup(host)
|
|
if err == nil {
|
|
ipList = make([]string, 0, len(ips))
|
|
for _, ip := range ips {
|
|
ipList = append(ipList, ip.String())
|
|
}
|
|
} else {
|
|
ipList = make([]string, 1)
|
|
ipList[0] = host
|
|
}
|
|
|
|
// Add the addresses and connection info to the result.
|
|
addrs := make([]types.GetAddedNodeInfoResultAddr, 0,
|
|
len(ipList))
|
|
for _, ip := range ipList {
|
|
var addr types.GetAddedNodeInfoResultAddr
|
|
addr.Address = ip
|
|
addr.Connected = "false"
|
|
if ip == host && peer.Connected() {
|
|
addr.Connected = directionString(peer.Inbound())
|
|
}
|
|
addrs = append(addrs, addr)
|
|
}
|
|
result.Addresses = &addrs
|
|
results = append(results, &result)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// handleGetBestBlock implements the getbestblock command.
|
|
func handleGetBestBlock(_ context.Context, s *Server, cmd interface{}) (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.
|
|
best := s.cfg.Chain.BestSnapshot()
|
|
result := &types.GetBestBlockResult{
|
|
Hash: best.Hash.String(),
|
|
Height: best.Height,
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// handleGetBestBlockHash implements the getbestblockhash command.
|
|
func handleGetBestBlockHash(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
best := s.cfg.Chain.BestSnapshot()
|
|
return best.Hash.String(), nil
|
|
}
|
|
|
|
// getDifficultyRatio returns the proof-of-work difficulty as a multiple of the
|
|
// minimum difficulty using the passed bits field from the header of a block.
|
|
func getDifficultyRatio(bits uint32, params *chaincfg.Params) float64 {
|
|
// The minimum difficulty is the max possible proof-of-work limit bits
|
|
// converted back to a number. Note this is not the same as the proof
|
|
// of work limit directly because the block difficulty is encoded in a
|
|
// block with the compact form which loses precision.
|
|
max := standalone.CompactToBig(params.PowLimitBits)
|
|
target := standalone.CompactToBig(bits)
|
|
|
|
difficulty := new(big.Rat).SetFrac(max, target)
|
|
outString := difficulty.FloatString(8)
|
|
diff, err := strconv.ParseFloat(outString, 64)
|
|
if err != nil {
|
|
log.Errorf("Cannot get difficulty: %v", err)
|
|
return 0
|
|
}
|
|
return diff
|
|
}
|
|
|
|
// handleGetBlock implements the getblock command.
|
|
func handleGetBlock(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.GetBlockCmd)
|
|
|
|
// Load the raw block bytes from the database.
|
|
hash, err := chainhash.NewHashFromStr(c.Hash)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(c.Hash)
|
|
}
|
|
|
|
chain := s.cfg.Chain
|
|
blk, err := chain.BlockByHash(hash)
|
|
if err != nil {
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCBlockNotFound,
|
|
Message: fmt.Sprintf("Block not found: %v", hash),
|
|
}
|
|
}
|
|
|
|
// When the verbose flag isn't set, simply return the
|
|
// network-serialized block as a hex-encoded string.
|
|
if c.Verbose != nil && !*c.Verbose {
|
|
blkBytes, err := blk.Bytes()
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"Could not serialize block")
|
|
}
|
|
|
|
return hex.EncodeToString(blkBytes), nil
|
|
}
|
|
|
|
chainWork, err := chain.ChainWork(hash)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Failed to retrieve work")
|
|
}
|
|
|
|
best := chain.BestSnapshot()
|
|
|
|
// Get next block hash unless there are none.
|
|
var nextHashString string
|
|
blockHeader := &blk.MsgBlock().Header
|
|
confirmations := int64(-1)
|
|
if chain.MainChainHasBlock(hash) {
|
|
if int64(blockHeader.Height) < best.Height {
|
|
nextHash, err := chain.BlockHashByHeight(int64(blockHeader.Height + 1))
|
|
if err != nil {
|
|
context := "No next block"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
nextHashString = nextHash.String()
|
|
}
|
|
confirmations = 1 + best.Height - int64(blockHeader.Height)
|
|
}
|
|
|
|
sbitsFloat := float64(blockHeader.SBits) / dcrutil.AtomsPerCoin
|
|
|
|
medianTime, err := chain.MedianTimeByHash(hash)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Unable to retrieve median block time")
|
|
}
|
|
|
|
blockReply := types.GetBlockVerboseResult{
|
|
Hash: c.Hash,
|
|
Version: blockHeader.Version,
|
|
MerkleRoot: blockHeader.MerkleRoot.String(),
|
|
StakeRoot: blockHeader.StakeRoot.String(),
|
|
PreviousHash: blockHeader.PrevBlock.String(),
|
|
Nonce: blockHeader.Nonce,
|
|
VoteBits: blockHeader.VoteBits,
|
|
FinalState: hex.EncodeToString(blockHeader.FinalState[:]),
|
|
Voters: blockHeader.Voters,
|
|
FreshStake: blockHeader.FreshStake,
|
|
Revocations: blockHeader.Revocations,
|
|
PoolSize: blockHeader.PoolSize,
|
|
Time: blockHeader.Timestamp.Unix(),
|
|
MedianTime: medianTime.Unix(),
|
|
StakeVersion: blockHeader.StakeVersion,
|
|
Confirmations: confirmations,
|
|
Height: int64(blockHeader.Height),
|
|
Size: int32(blk.MsgBlock().Header.Size),
|
|
Bits: strconv.FormatInt(int64(blockHeader.Bits), 16),
|
|
SBits: sbitsFloat,
|
|
Difficulty: getDifficultyRatio(blockHeader.Bits, s.cfg.ChainParams),
|
|
ChainWork: fmt.Sprintf("%064x", chainWork),
|
|
ExtraData: hex.EncodeToString(blockHeader.ExtraData[:]),
|
|
NextHash: nextHashString,
|
|
}
|
|
|
|
// Determine if the treasury rules are active for the block.
|
|
isTreasuryEnabled, err := s.isTreasuryAgendaActive(&blockHeader.PrevBlock)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Determine if the automatic ticket revocations agenda is active for the
|
|
// block.
|
|
isAutoRevocationsEnabled, err :=
|
|
s.isAutoRevocationsAgendaActive(&blockHeader.PrevBlock)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if c.VerboseTx == nil || !*c.VerboseTx {
|
|
transactions := blk.Transactions()
|
|
txNames := make([]string, len(transactions))
|
|
for i, tx := range transactions {
|
|
txNames[i] = tx.Hash().String()
|
|
}
|
|
|
|
blockReply.Tx = txNames
|
|
|
|
stransactions := blk.STransactions()
|
|
stxNames := make([]string, len(stransactions))
|
|
for i, tx := range stransactions {
|
|
stxNames[i] = tx.Hash().String()
|
|
}
|
|
|
|
blockReply.STx = stxNames
|
|
} else {
|
|
txns := blk.Transactions()
|
|
chainParams := s.cfg.ChainParams
|
|
rawTxns := make([]types.TxRawResult, len(txns))
|
|
for i, tx := range txns {
|
|
rawTxn, err := s.createTxRawResult(chainParams,
|
|
tx.MsgTx(), tx.Hash().String(), uint32(i),
|
|
blockHeader, blk.Hash().String(),
|
|
int64(blockHeader.Height), confirmations,
|
|
isTreasuryEnabled, isAutoRevocationsEnabled)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"Could not create transaction")
|
|
}
|
|
rawTxns[i] = *rawTxn
|
|
}
|
|
blockReply.RawTx = rawTxns
|
|
|
|
stxns := blk.STransactions()
|
|
rawSTxns := make([]types.TxRawResult, len(stxns))
|
|
for i, tx := range stxns {
|
|
rawSTxn, err := s.createTxRawResult(chainParams,
|
|
tx.MsgTx(), tx.Hash().String(), uint32(i),
|
|
blockHeader, blk.Hash().String(),
|
|
int64(blockHeader.Height), confirmations,
|
|
isTreasuryEnabled, isAutoRevocationsEnabled)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"Could not create stake transaction")
|
|
}
|
|
rawSTxns[i] = *rawSTxn
|
|
}
|
|
blockReply.RawSTx = rawSTxns
|
|
}
|
|
|
|
return blockReply, nil
|
|
}
|
|
|
|
// handleGetBlockchainInfo implements the getblockchaininfo command.
|
|
func handleGetBlockchainInfo(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
chain := s.cfg.Chain
|
|
best := chain.BestSnapshot()
|
|
_, bestHeaderHeight := chain.BestHeader()
|
|
|
|
// Fetch the current chain work using the best block hash.
|
|
chainWork, err := chain.ChainWork(&best.Hash)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Could not fetch chain work.")
|
|
}
|
|
|
|
// Estimate the verification progress of the node.
|
|
var verifyProgress float64
|
|
if bestHeaderHeight > 0 {
|
|
progress := float64(best.Height) / float64(bestHeaderHeight)
|
|
verifyProgress = math.Min(progress, 1.0)
|
|
}
|
|
|
|
// Fetch the maximum allowed block size for all blocks other than the
|
|
// genesis block.
|
|
params := s.cfg.ChainParams
|
|
maxBlockSize := int64(params.MaximumBlockSizes[0])
|
|
if best.PrevHash != zeroHash {
|
|
maxBlockSize, err = chain.MaxBlockSize(&best.PrevHash)
|
|
if err != nil {
|
|
context := "Could not fetch max block size"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
}
|
|
|
|
// Fetch the agendas of the consensus deployments as well as their
|
|
// threshold states and state activation heights.
|
|
dInfo := make(map[string]types.AgendaInfo)
|
|
defaultStatus := blockchain.ThresholdStateTuple{
|
|
State: blockchain.ThresholdDefined,
|
|
}.String()
|
|
for version, deployments := range params.Deployments {
|
|
for _, agenda := range deployments {
|
|
aInfo := types.AgendaInfo{
|
|
StartTime: agenda.StartTime,
|
|
ExpireTime: agenda.ExpireTime,
|
|
Status: defaultStatus,
|
|
}
|
|
|
|
// If the best block is the genesis block, continue without attempting to
|
|
// query the threshold state or state changed height.
|
|
if best.PrevHash == zeroHash {
|
|
dInfo[agenda.Vote.Id] = aInfo
|
|
continue
|
|
}
|
|
|
|
state, err := chain.NextThresholdState(&best.PrevHash, version,
|
|
agenda.Vote.Id)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
fmt.Sprintf("Could not fetch threshold state "+
|
|
"for agenda with id (%v).", agenda.Vote.Id))
|
|
}
|
|
|
|
stateChangedHeight, err := chain.StateLastChangedHeight(
|
|
&best.Hash, version, agenda.Vote.Id)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
fmt.Sprintf("Could not fetch state last changed "+
|
|
"height for agenda with id (%v).", agenda.Vote.Id))
|
|
}
|
|
|
|
aInfo.Since = stateChangedHeight
|
|
aInfo.Status = state.String()
|
|
dInfo[agenda.Vote.Id] = aInfo
|
|
}
|
|
}
|
|
|
|
// Generate rpc response.
|
|
response := types.GetBlockChainInfoResult{
|
|
Chain: params.Name,
|
|
Blocks: best.Height,
|
|
Headers: bestHeaderHeight,
|
|
SyncHeight: s.cfg.SyncMgr.SyncHeight(),
|
|
ChainWork: fmt.Sprintf("%064x", chainWork),
|
|
InitialBlockDownload: !chain.IsCurrent(),
|
|
VerificationProgress: verifyProgress,
|
|
BestBlockHash: best.Hash.String(),
|
|
Difficulty: best.Bits,
|
|
DifficultyRatio: getDifficultyRatio(best.Bits, params),
|
|
MaxBlockSize: maxBlockSize,
|
|
Deployments: dInfo,
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
// handleGetBlockCount implements the getblockcount command.
|
|
func handleGetBlockCount(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
best := s.cfg.Chain.BestSnapshot()
|
|
return best.Height, nil
|
|
}
|
|
|
|
// handleGetBlockHash implements the getblockhash command.
|
|
func handleGetBlockHash(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.GetBlockHashCmd)
|
|
hash, err := s.cfg.Chain.BlockHashByHeight(c.Index)
|
|
if err != nil {
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCOutOfRange,
|
|
Message: fmt.Sprintf("Block number out of range: %v",
|
|
c.Index),
|
|
}
|
|
}
|
|
|
|
return hash.String(), nil
|
|
}
|
|
|
|
// handleGetBlockHeader implements the getblockheader command.
|
|
func handleGetBlockHeader(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.GetBlockHeaderCmd)
|
|
|
|
// Fetch the header from chain.
|
|
hash, err := chainhash.NewHashFromStr(c.Hash)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(c.Hash)
|
|
}
|
|
|
|
chain := s.cfg.Chain
|
|
blockHeader, err := chain.HeaderByHash(hash)
|
|
if err != nil {
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCBlockNotFound,
|
|
Message: fmt.Sprintf("Block not found: %v", c.Hash),
|
|
}
|
|
}
|
|
|
|
// When the verbose flag isn't set, simply return the serialized block
|
|
// header as a hex-encoded string.
|
|
if c.Verbose != nil && !*c.Verbose {
|
|
var headerBuf bytes.Buffer
|
|
err := blockHeader.Serialize(&headerBuf)
|
|
if err != nil {
|
|
context := "Failed to serialize block header"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
return hex.EncodeToString(headerBuf.Bytes()), nil
|
|
}
|
|
|
|
// The verbose flag is set, so generate the JSON object and return it.
|
|
|
|
chainWork, err := chain.ChainWork(hash)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Failed to retrieve work")
|
|
}
|
|
|
|
best := chain.BestSnapshot()
|
|
|
|
// Get next block hash unless there are none.
|
|
var nextHashString string
|
|
confirmations := int64(-1)
|
|
height := int64(blockHeader.Height)
|
|
if chain.MainChainHasBlock(hash) {
|
|
if height < best.Height {
|
|
nextHash, err := chain.BlockHashByHeight(height + 1)
|
|
if err != nil {
|
|
context := "No next block"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
nextHashString = nextHash.String()
|
|
}
|
|
confirmations = 1 + best.Height - height
|
|
}
|
|
|
|
medianTime, err := chain.MedianTimeByHash(hash)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Unable to retrieve median block time")
|
|
}
|
|
|
|
blockHeaderReply := types.GetBlockHeaderVerboseResult{
|
|
Hash: c.Hash,
|
|
Confirmations: confirmations,
|
|
Version: blockHeader.Version,
|
|
MerkleRoot: blockHeader.MerkleRoot.String(),
|
|
StakeRoot: blockHeader.StakeRoot.String(),
|
|
VoteBits: blockHeader.VoteBits,
|
|
FinalState: hex.EncodeToString(blockHeader.FinalState[:]),
|
|
Voters: blockHeader.Voters,
|
|
FreshStake: blockHeader.FreshStake,
|
|
Revocations: blockHeader.Revocations,
|
|
PoolSize: blockHeader.PoolSize,
|
|
Bits: strconv.FormatInt(int64(blockHeader.Bits), 16),
|
|
SBits: dcrutil.Amount(blockHeader.SBits).ToCoin(),
|
|
Height: uint32(height),
|
|
Size: blockHeader.Size,
|
|
Time: blockHeader.Timestamp.Unix(),
|
|
MedianTime: medianTime.Unix(),
|
|
Nonce: blockHeader.Nonce,
|
|
ExtraData: hex.EncodeToString(blockHeader.ExtraData[:]),
|
|
StakeVersion: blockHeader.StakeVersion,
|
|
Difficulty: getDifficultyRatio(blockHeader.Bits, s.cfg.ChainParams),
|
|
ChainWork: fmt.Sprintf("%064x", chainWork),
|
|
PreviousHash: blockHeader.PrevBlock.String(),
|
|
NextHash: nextHashString,
|
|
}
|
|
|
|
return blockHeaderReply, nil
|
|
}
|
|
|
|
// handleGetBlockSubsidy implements the getblocksubsidy command.
|
|
func handleGetBlockSubsidy(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.GetBlockSubsidyCmd)
|
|
|
|
height := c.Height
|
|
voters := c.Voters
|
|
|
|
// Determine if the treasury rules are active as of the provided height when
|
|
// that height exists in the main chain or as of the current best tip
|
|
// otherwise.
|
|
chain := s.cfg.Chain
|
|
best := chain.BestSnapshot()
|
|
prevBlkHash := best.Hash
|
|
if height <= best.Height {
|
|
header, err := chain.HeaderByHeight(height)
|
|
if err != nil {
|
|
context := fmt.Sprintf("Failed to retrieve header for height %d",
|
|
height)
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
prevBlkHash = header.PrevBlock
|
|
}
|
|
isTreasuryEnabled, err := s.isTreasuryAgendaActive(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
isSubsidyEnabled, err := s.isSubsidySplitAgendaActive(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
subsidyCache := s.cfg.SubsidyCache
|
|
dev := subsidyCache.CalcTreasurySubsidy(height, voters, isTreasuryEnabled)
|
|
pos := subsidyCache.CalcStakeVoteSubsidyV2(height-1, isSubsidyEnabled) *
|
|
int64(voters)
|
|
pow := subsidyCache.CalcWorkSubsidyV2(height, voters, isSubsidyEnabled)
|
|
total := dev + pos + pow
|
|
|
|
rep := types.GetBlockSubsidyResult{
|
|
Developer: dev,
|
|
PoS: pos,
|
|
PoW: pow,
|
|
Total: total,
|
|
}
|
|
|
|
return rep, nil
|
|
}
|
|
|
|
// handleGetChainTips implements the getchaintips command.
|
|
func handleGetChainTips(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
chainTips := s.cfg.Chain.ChainTips()
|
|
result := make([]types.GetChainTipsResult, 0, len(chainTips))
|
|
for _, tip := range chainTips {
|
|
result = append(result, types.GetChainTipsResult{
|
|
Height: tip.Height,
|
|
Hash: tip.Hash.String(),
|
|
BranchLen: tip.BranchLen,
|
|
Status: tip.Status,
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// handleGetCoinSupply implements the getcoinsupply command.
|
|
func handleGetCoinSupply(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
return s.cfg.Chain.BestSnapshot().TotalSubsidy, nil
|
|
}
|
|
|
|
// handleGetConnectionCount implements the getconnectioncount command.
|
|
func handleGetConnectionCount(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
return s.cfg.ConnMgr.ConnectedCount(), nil
|
|
}
|
|
|
|
// handleGetCurrentNet implements the getcurrentnet command.
|
|
func handleGetCurrentNet(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
return s.cfg.ChainParams.Net, nil
|
|
}
|
|
|
|
// handleGetDifficulty implements the getdifficulty command.
|
|
func handleGetDifficulty(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
best := s.cfg.Chain.BestSnapshot()
|
|
return getDifficultyRatio(best.Bits, s.cfg.ChainParams), nil
|
|
}
|
|
|
|
// handleGetGenerate implements the getgenerate command.
|
|
func handleGetGenerate(_ context.Context, s *Server, _ interface{}) (interface{}, error) {
|
|
return s.cfg.CPUMiner.IsMining(), nil
|
|
}
|
|
|
|
// handleGetHashesPerSec implements the gethashespersec command.
|
|
func handleGetHashesPerSec(_ context.Context, s *Server, _ interface{}) (interface{}, error) {
|
|
return int64(s.cfg.CPUMiner.HashesPerSecond()), nil
|
|
}
|
|
|
|
// handleGetHeaders implements the getheaders command.
|
|
func handleGetHeaders(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.GetHeadersCmd)
|
|
blockLocators, err := decodeHashes(c.BlockLocators)
|
|
if err != nil {
|
|
// Already a *dcrjson.RPCError
|
|
return nil, err
|
|
}
|
|
var hashStop chainhash.Hash
|
|
if c.HashStop != "" {
|
|
err := chainhash.Decode(&hashStop, c.HashStop)
|
|
if err != nil {
|
|
return nil, rpcInvalidError("Failed to decode "+
|
|
"hashstop: %v", err)
|
|
}
|
|
}
|
|
|
|
// Until wire.MsgGetHeaders uses []Hash instead of the []*Hash, this
|
|
// conversion is necessary. The wire protocol getheaders is (probably)
|
|
// called much more often than this RPC, so chain.LocateHeaders is
|
|
// optimized for that and this is given the performance penalty.
|
|
locators := make(blockchain.BlockLocator, len(blockLocators))
|
|
for i := range blockLocators {
|
|
locators[i] = &blockLocators[i]
|
|
}
|
|
|
|
chain := s.cfg.Chain
|
|
headers := chain.LocateHeaders(locators, &hashStop)
|
|
|
|
// Return the serialized block headers as hex-encoded strings.
|
|
hexBlockHeaders := make([]string, len(headers))
|
|
var buf bytes.Buffer
|
|
buf.Grow(wire.MaxBlockHeaderPayload)
|
|
for i, h := range headers {
|
|
err := h.Serialize(&buf)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"Failed to serialize block header")
|
|
}
|
|
hexBlockHeaders[i] = hex.EncodeToString(buf.Bytes())
|
|
buf.Reset()
|
|
}
|
|
return &types.GetHeadersResult{Headers: hexBlockHeaders}, nil
|
|
}
|
|
|
|
// handleGetCFilterV2 implements the getcfilterv2 command.
|
|
func handleGetCFilterV2(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.GetCFilterV2Cmd)
|
|
hash, err := chainhash.NewHashFromStr(c.BlockHash)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(c.BlockHash)
|
|
}
|
|
|
|
filter, err := s.cfg.FiltererV2.FilterByBlockHash(hash)
|
|
if err != nil {
|
|
if errors.Is(err, blockchain.ErrNoFilter) {
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCBlockNotFound,
|
|
Message: fmt.Sprintf("Block not found: %v", hash),
|
|
}
|
|
}
|
|
|
|
context := fmt.Sprintf("Failed to load filter for block %s", hash)
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
// NOTE: When more header commitments are added, this will need to load the
|
|
// inclusion proof for the filter from the database. However, since there
|
|
// is only currently a single commitment, there is only a single leaf in the
|
|
// commitment merkle tree, and hence the proof hashes will always be empty
|
|
// given there are no siblings. Adding an additional header commitment will
|
|
// require a consensus vote anyway and this can be updated at that time.
|
|
result := &types.GetCFilterV2Result{
|
|
BlockHash: c.BlockHash,
|
|
Data: hex.EncodeToString(filter.Bytes()),
|
|
ProofIndex: blockchain.HeaderCmtFilterIndex,
|
|
ProofHashes: nil,
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// handleGetInfo implements the getinfo command. We only return the fields
|
|
// that are not related to wallet functionality.
|
|
func handleGetInfo(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
best := s.cfg.Chain.BestSnapshot()
|
|
ret := &types.InfoChainResult{
|
|
Version: int32(1000000*version.Major + 10000*version.Minor +
|
|
100*version.Patch),
|
|
ProtocolVersion: int32(s.cfg.MaxProtocolVersion),
|
|
Blocks: best.Height,
|
|
TimeOffset: int64(s.cfg.TimeSource.Offset().Seconds()),
|
|
Connections: s.cfg.ConnMgr.ConnectedCount(),
|
|
Proxy: s.cfg.Proxy,
|
|
Difficulty: getDifficultyRatio(best.Bits, s.cfg.ChainParams),
|
|
TestNet: s.cfg.TestNet,
|
|
RelayFee: s.cfg.MinRelayTxFee.ToCoin(),
|
|
AddrIndex: s.cfg.AddrIndexer != nil,
|
|
TxIndex: s.cfg.TxIndexer != nil,
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// handleGetMempoolInfo implements the getmempoolinfo command.
|
|
func handleGetMempoolInfo(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
mempoolTxns := s.cfg.TxMempooler.TxDescs()
|
|
|
|
var numBytes int64
|
|
for _, txD := range mempoolTxns {
|
|
numBytes += int64(txD.Tx.MsgTx().SerializeSize())
|
|
}
|
|
|
|
ret := &types.GetMempoolInfoResult{
|
|
Size: int64(len(mempoolTxns)),
|
|
Bytes: numBytes,
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// handleGetMiningInfo implements the getmininginfo command. We only return the
|
|
// fields that are not related to wallet functionality.
|
|
func handleGetMiningInfo(ctx context.Context, s *Server, _ interface{}) (interface{}, error) {
|
|
// Create a default getnetworkhashps command to use defaults and make
|
|
// use of the existing getnetworkhashps handler.
|
|
gnhpsCmd := types.NewGetNetworkHashPSCmd(nil, nil)
|
|
networkHashesPerSecIface, err := handleGetNetworkHashPS(ctx, s, gnhpsCmd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
networkHashesPerSec, ok := networkHashesPerSecIface.(int64)
|
|
if !ok {
|
|
return nil, rpcInternalError("invalid network hashes per sec",
|
|
fmt.Sprintf("Invalid type: %q",
|
|
networkHashesPerSecIface))
|
|
}
|
|
|
|
best := s.cfg.Chain.BestSnapshot()
|
|
result := types.GetMiningInfoResult{
|
|
Blocks: best.Height,
|
|
CurrentBlockSize: best.BlockSize,
|
|
CurrentBlockTx: best.NumTxns,
|
|
Difficulty: getDifficultyRatio(best.Bits, s.cfg.ChainParams),
|
|
StakeDifficulty: best.NextStakeDiff,
|
|
Generate: s.cfg.CPUMiner.IsMining(),
|
|
GenProcLimit: s.cfg.CPUMiner.NumWorkers(),
|
|
HashesPerSec: int64(s.cfg.CPUMiner.HashesPerSecond()),
|
|
NetworkHashPS: networkHashesPerSec,
|
|
PooledTx: uint64(s.cfg.TxMempooler.Count()),
|
|
TestNet: s.cfg.TestNet,
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
// handleGetNetTotals implements the getnettotals command.
|
|
func handleGetNetTotals(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
totalBytesRecv, totalBytesSent := s.cfg.ConnMgr.NetTotals()
|
|
reply := &types.GetNetTotalsResult{
|
|
TotalBytesRecv: totalBytesRecv,
|
|
TotalBytesSent: totalBytesSent,
|
|
TimeMillis: s.cfg.Clock.Now().UTC().UnixNano() / int64(time.Millisecond),
|
|
}
|
|
return reply, nil
|
|
}
|
|
|
|
// handleGetNetworkHashPS implements the getnetworkhashps command.
|
|
func handleGetNetworkHashPS(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
// Note: All valid error return paths should return an int64. Literal
|
|
// zeros are inferred as int, and won't coerce to int64 because the
|
|
// return value is an interface{}.
|
|
|
|
c := cmd.(*types.GetNetworkHashPSCmd)
|
|
|
|
// 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.
|
|
chain := s.cfg.Chain
|
|
best := chain.BestSnapshot()
|
|
endHeight := int64(-1)
|
|
if c.Height != nil {
|
|
endHeight = int64(*c.Height)
|
|
}
|
|
if endHeight > best.Height || endHeight == 0 {
|
|
return int64(0), nil
|
|
}
|
|
if endHeight < 0 {
|
|
endHeight = best.Height
|
|
}
|
|
|
|
// Calculate the number of blocks per retarget interval based on the
|
|
// chain parameters.
|
|
params := s.cfg.ChainParams
|
|
blocksPerRetarget := int64(params.TargetTimespan / params.TargetTimePerBlock)
|
|
|
|
// Calculate the starting block height based on the passed number of
|
|
// 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.
|
|
|
|
numBlocks := int64(120)
|
|
if c.Blocks != nil {
|
|
numBlocks = int64(*c.Blocks)
|
|
}
|
|
|
|
var startHeight int64
|
|
if numBlocks <= 0 {
|
|
startHeight = endHeight - ((endHeight % blocksPerRetarget) + 1)
|
|
} else {
|
|
startHeight = endHeight - numBlocks
|
|
}
|
|
if startHeight < 0 {
|
|
startHeight = 0
|
|
}
|
|
log.Debugf("Calculating network hashes per second from %d to %d",
|
|
startHeight, endHeight)
|
|
|
|
// Find the min and max block timestamps as well as calculate the total
|
|
// amount of work that happened between the start and end blocks.
|
|
var minTimestamp, maxTimestamp time.Time
|
|
totalWork := big.NewInt(0)
|
|
for curHeight := startHeight; curHeight <= endHeight; curHeight++ {
|
|
hash, err := chain.BlockHashByHeight(curHeight)
|
|
if err != nil {
|
|
context := "Failed to fetch block hash"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
// Fetch the header from chain.
|
|
header, err := chain.HeaderByHash(hash)
|
|
if err != nil {
|
|
context := "Failed to fetch block header"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
if curHeight == startHeight {
|
|
minTimestamp = header.Timestamp
|
|
maxTimestamp = minTimestamp
|
|
} else {
|
|
totalWork.Add(totalWork, standalone.CalcWork(header.Bits))
|
|
|
|
if minTimestamp.After(header.Timestamp) {
|
|
minTimestamp = header.Timestamp
|
|
}
|
|
if maxTimestamp.Before(header.Timestamp) {
|
|
maxTimestamp = header.Timestamp
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate the difference in seconds between the min and max block
|
|
// timestamps and avoid division by zero in the case where there is no
|
|
// time difference.
|
|
timeDiff := int64(maxTimestamp.Sub(minTimestamp) / time.Second)
|
|
if timeDiff == 0 {
|
|
return int64(0), nil
|
|
}
|
|
|
|
hashesPerSec := new(big.Int).Div(totalWork, big.NewInt(timeDiff))
|
|
return hashesPerSec.Int64(), nil
|
|
}
|
|
|
|
// handleGetNetworkInfo implements the getnetworkinfo command.
|
|
func handleGetNetworkInfo(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
lAddrs := s.cfg.AddrManager.LocalAddresses()
|
|
localAddrs := make([]types.LocalAddressesResult, len(lAddrs))
|
|
for idx, entry := range lAddrs {
|
|
addr := types.LocalAddressesResult{
|
|
Address: entry.Address,
|
|
Port: entry.Port,
|
|
}
|
|
localAddrs[idx] = addr
|
|
}
|
|
|
|
info := types.GetNetworkInfoResult{
|
|
Version: int32(1000000*version.Major + 10000*version.Minor +
|
|
100*version.Patch),
|
|
SubVersion: s.cfg.UserAgentVersion,
|
|
ProtocolVersion: int32(s.cfg.MaxProtocolVersion),
|
|
TimeOffset: int64(s.cfg.TimeSource.Offset().Seconds()),
|
|
Connections: s.cfg.ConnMgr.ConnectedCount(),
|
|
RelayFee: s.cfg.MinRelayTxFee.ToCoin(),
|
|
Networks: s.cfg.NetInfo,
|
|
LocalAddresses: localAddrs,
|
|
LocalServices: fmt.Sprintf("%016x", uint64(s.cfg.Services)),
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
// handleGetPeerInfo implements the getpeerinfo command.
|
|
func handleGetPeerInfo(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
peers := s.cfg.ConnMgr.ConnectedPeers()
|
|
syncPeerID := s.cfg.SyncMgr.SyncPeerID()
|
|
infos := make([]*types.GetPeerInfoResult, 0, len(peers))
|
|
for _, p := range peers {
|
|
statsSnap := p.StatsSnapshot()
|
|
var addrLocalStr string
|
|
if addrLocal := p.LocalAddr(); addrLocal != nil {
|
|
addrLocalStr = addrLocal.String()
|
|
}
|
|
info := &types.GetPeerInfoResult{
|
|
ID: statsSnap.ID,
|
|
Addr: statsSnap.Addr,
|
|
AddrLocal: addrLocalStr,
|
|
Services: fmt.Sprintf("%08d", uint64(statsSnap.Services)),
|
|
RelayTxes: !p.IsTxRelayDisabled(),
|
|
LastSend: statsSnap.LastSend.Unix(),
|
|
LastRecv: statsSnap.LastRecv.Unix(),
|
|
BytesSent: statsSnap.BytesSent,
|
|
BytesRecv: statsSnap.BytesRecv,
|
|
ConnTime: statsSnap.ConnTime.Unix(),
|
|
PingTime: float64(statsSnap.LastPingMicros),
|
|
TimeOffset: statsSnap.TimeOffset,
|
|
Version: statsSnap.Version,
|
|
SubVer: statsSnap.UserAgent,
|
|
Inbound: statsSnap.Inbound,
|
|
StartingHeight: statsSnap.StartingHeight,
|
|
CurrentHeight: statsSnap.LastBlock,
|
|
BanScore: int32(p.BanScore()),
|
|
SyncNode: p.ID() == syncPeerID,
|
|
}
|
|
if p.LastPingNonce() != 0 {
|
|
wait := float64(s.cfg.Clock.Since(statsSnap.LastPingTime).Nanoseconds())
|
|
// We actually want microseconds.
|
|
info.PingWait = wait / 1000
|
|
}
|
|
infos = append(infos, info)
|
|
}
|
|
sort.Slice(infos, func(i, j int) bool {
|
|
return infos[i].ID < infos[j].ID
|
|
})
|
|
return infos, nil
|
|
}
|
|
|
|
// handleGetRawMempool implements the getrawmempool command.
|
|
func handleGetRawMempool(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.GetRawMempoolCmd)
|
|
|
|
// Choose the type to filter the results by based on the provided param.
|
|
// A filter type of nil means no filtering.
|
|
var filterType *stake.TxType
|
|
if c.TxType != nil {
|
|
switch types.GetRawMempoolTxTypeCmd(*c.TxType) {
|
|
case types.GRMRegular:
|
|
filterType = new(stake.TxType)
|
|
*filterType = stake.TxTypeRegular
|
|
case types.GRMTickets:
|
|
filterType = new(stake.TxType)
|
|
*filterType = stake.TxTypeSStx
|
|
case types.GRMVotes:
|
|
filterType = new(stake.TxType)
|
|
*filterType = stake.TxTypeSSGen
|
|
case types.GRMRevocations:
|
|
filterType = new(stake.TxType)
|
|
*filterType = stake.TxTypeSSRtx
|
|
case types.GRMTSpend:
|
|
filterType = new(stake.TxType)
|
|
*filterType = stake.TxTypeTSpend
|
|
case types.GRMTAdd:
|
|
filterType = new(stake.TxType)
|
|
*filterType = stake.TxTypeTAdd
|
|
case types.GRMAll:
|
|
// Nothing to do
|
|
default:
|
|
supported := []types.GetRawMempoolTxTypeCmd{types.GRMRegular,
|
|
types.GRMTickets, types.GRMVotes, types.GRMRevocations,
|
|
types.GRMTSpend, types.GRMTAdd, types.GRMAll}
|
|
return nil, rpcInvalidError("Invalid transaction type: %s -- "+
|
|
"supported types: %v", *c.TxType, supported)
|
|
}
|
|
}
|
|
|
|
// Return verbose results if requested.
|
|
if c.Verbose != nil && *c.Verbose {
|
|
descs := s.cfg.TxMempooler.VerboseTxDescs()
|
|
result := make(map[string]*types.GetRawMempoolVerboseResult, len(descs))
|
|
for i := range descs {
|
|
desc := descs[i]
|
|
if filterType != nil && desc.Type != *filterType {
|
|
continue
|
|
}
|
|
|
|
tx := desc.Tx
|
|
mpd := &types.GetRawMempoolVerboseResult{
|
|
Size: int32(tx.MsgTx().SerializeSize()),
|
|
Fee: dcrutil.Amount(desc.Fee).ToCoin(),
|
|
Time: desc.Added.Unix(),
|
|
Height: desc.Height,
|
|
StartingPriority: desc.StartingPriority,
|
|
CurrentPriority: desc.CurrentPriority,
|
|
Depends: make([]string, len(desc.Depends)),
|
|
}
|
|
for j, depDesc := range desc.Depends {
|
|
mpd.Depends[j] = depDesc.Tx.Hash().String()
|
|
}
|
|
|
|
result[tx.Hash().String()] = mpd
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// The response is simply an array of the transaction hashes if the
|
|
// verbose flag is not set.
|
|
descs := s.cfg.TxMempooler.TxDescs()
|
|
hashStrings := make([]string, 0, len(descs))
|
|
for i := range descs {
|
|
if filterType != nil && descs[i].Type != *filterType {
|
|
continue
|
|
}
|
|
hashStrings = append(hashStrings, descs[i].Tx.Hash().String())
|
|
}
|
|
return hashStrings, nil
|
|
}
|
|
|
|
// handleGetRawTransaction implements the getrawtransaction command.
|
|
func handleGetRawTransaction(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.GetRawTransactionCmd)
|
|
|
|
// Convert the provided transaction hash hex to a Hash.
|
|
txHash, err := chainhash.NewHashFromStr(c.Txid)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(c.Txid)
|
|
}
|
|
|
|
verbose := false
|
|
if c.Verbose != nil {
|
|
verbose = *c.Verbose != 0
|
|
}
|
|
|
|
// Try to fetch the transaction from the memory pool and if that fails,
|
|
// try the block database.
|
|
var mtx *wire.MsgTx
|
|
var blkHash *chainhash.Hash
|
|
var blkHeight int64
|
|
var blkIndex uint32
|
|
chain := s.cfg.Chain
|
|
txIndex := s.cfg.TxIndexer
|
|
tx, err := s.cfg.TxMempooler.FetchTransaction(txHash)
|
|
if err != nil {
|
|
if txIndex == nil {
|
|
return nil, rpcInternalError("The transaction index "+
|
|
"must be enabled to query the blockchain "+
|
|
"(specify --txindex)", "Configuration")
|
|
}
|
|
|
|
// Ensure the tx index is synced.
|
|
tHeight, tHash, err := txIndex.Tip()
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Tip")
|
|
}
|
|
|
|
chain := s.cfg.Chain
|
|
|
|
// Return an out-of-sync error if index is lagging a
|
|
// maximum reorg depth (6) blocks or more from the chain tip.
|
|
if chain.BestSnapshot().Height > (tHeight + 5) {
|
|
msg := fmt.Sprintf("%s: index not synced", txIndex.Name())
|
|
return nil, rpcInternalError(msg, "Sync")
|
|
}
|
|
|
|
sync:
|
|
for !chain.BestSnapshot().Hash.IsEqual(tHash) {
|
|
select {
|
|
case <-time.After(syncWait):
|
|
msg := fmt.Sprintf("%s: index not synced", txIndex.Name())
|
|
return nil, rpcInternalError(msg, "Sync")
|
|
case <-txIndex.WaitForSync():
|
|
break sync
|
|
}
|
|
}
|
|
|
|
// Look up the location of the transaction.
|
|
idxEntry, err := txIndex.Entry(txHash)
|
|
if err != nil {
|
|
context := "Failed to retrieve transaction location"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
if idxEntry == nil {
|
|
return nil, rpcNoTxInfoError(txHash)
|
|
}
|
|
blockRegion := &idxEntry.BlockRegion
|
|
|
|
// Load the raw transaction bytes from the database.
|
|
var txBytes []byte
|
|
err = s.cfg.DB.View(func(dbTx database.Tx) error {
|
|
var err error
|
|
txBytes, err = dbTx.FetchBlockRegion(blockRegion)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, rpcNoTxInfoError(txHash)
|
|
}
|
|
|
|
// When the verbose flag isn't set, simply return the serialized
|
|
// transaction as a hex-encoded string. This is done here to
|
|
// avoid deserializing it only to reserialize it again later.
|
|
if !verbose {
|
|
return hex.EncodeToString(txBytes), nil
|
|
}
|
|
|
|
// Grab the block details.
|
|
blkHash = blockRegion.Hash
|
|
blkHeight, err = chain.BlockHeightByHash(blkHash)
|
|
if err != nil {
|
|
context := "Failed to retrieve block height"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
blkIndex = idxEntry.BlockIndex
|
|
|
|
// Deserialize the transaction
|
|
var msgTx wire.MsgTx
|
|
err = msgTx.Deserialize(bytes.NewReader(txBytes))
|
|
if err != nil {
|
|
context := "Failed to deserialize transaction"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
mtx = &msgTx
|
|
} else {
|
|
// When the verbose flag isn't set, simply return the
|
|
// network-serialized transaction as a hex-encoded string.
|
|
if !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.
|
|
mtxHex, err := s.messageToHex(tx.MsgTx())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return mtxHex, nil
|
|
}
|
|
|
|
mtx = tx.MsgTx()
|
|
}
|
|
|
|
// The verbose flag is set, so generate the JSON object and return it.
|
|
var (
|
|
blkHeader *wire.BlockHeader
|
|
prevBlkHash chainhash.Hash
|
|
blkHashStr string
|
|
confirmations int64
|
|
)
|
|
if blkHash != nil {
|
|
// Fetch the header from chain.
|
|
header, err := chain.HeaderByHash(blkHash)
|
|
if err != nil {
|
|
context := "Failed to fetch block header"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
blkHeader = &header
|
|
prevBlkHash = header.PrevBlock
|
|
blkHashStr = blkHash.String()
|
|
confirmations = 1 + chain.BestSnapshot().Height - blkHeight
|
|
} else {
|
|
// The transaction was obtained from the mempool when there is no block
|
|
// hash set, so the previous block hash is the current best chain tip in
|
|
// that case.
|
|
prevBlkHash = chain.BestSnapshot().Hash
|
|
}
|
|
|
|
// Determine if the treasury rules are active as of either the block the
|
|
// contains the transaction or the current best tip when it is in the
|
|
// mempool.
|
|
isTreasuryEnabled, err := s.isTreasuryAgendaActive(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Treasury Status")
|
|
}
|
|
|
|
// Determine if the automatic ticket revocations agenda is active.
|
|
isAutoRevocationsEnabled, err := s.isAutoRevocationsAgendaActive(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rawTxn, err := s.createTxRawResult(s.cfg.ChainParams, mtx, txHash.String(),
|
|
blkIndex, blkHeader, blkHashStr, blkHeight, confirmations,
|
|
isTreasuryEnabled, isAutoRevocationsEnabled)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return *rawTxn, nil
|
|
}
|
|
|
|
// handleGetStakeDifficulty implements the getstakedifficulty command.
|
|
func handleGetStakeDifficulty(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
chain := s.cfg.Chain
|
|
best := chain.BestSnapshot()
|
|
blockHeader, err := chain.HeaderByHeight(best.Height)
|
|
if err != nil {
|
|
log.Errorf("Error getting block: %v", err)
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCDifficulty,
|
|
Message: "Error getting stake difficulty: " + err.Error(),
|
|
}
|
|
}
|
|
result := types.GetStakeDifficultyResult{
|
|
CurrentStakeDifficulty: dcrutil.Amount(blockHeader.SBits).ToCoin(),
|
|
NextStakeDifficulty: dcrutil.Amount(best.NextStakeDiff).ToCoin(),
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// convertVersionMap translates a map[int]int into a sorted array of
|
|
// VersionCount that contains the same information.
|
|
func convertVersionMap(m map[int]int) []types.VersionCount {
|
|
sorted := make([]types.VersionCount, 0, len(m))
|
|
order := make([]int, 0, len(m))
|
|
for k := range m {
|
|
order = append(order, k)
|
|
}
|
|
sort.Ints(order)
|
|
|
|
for _, v := range order {
|
|
sorted = append(sorted, types.VersionCount{Version: uint32(v),
|
|
Count: uint32(m[v])})
|
|
}
|
|
|
|
return sorted
|
|
}
|
|
|
|
// handleGetStakeVersionInfo implements the getstakeversioninfo command.
|
|
func handleGetStakeVersionInfo(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.GetStakeVersionInfoCmd)
|
|
|
|
chain := s.cfg.Chain
|
|
snapshot := chain.BestSnapshot()
|
|
interval := s.cfg.ChainParams.StakeVersionInterval
|
|
|
|
count := int32(1)
|
|
if c.Count != nil {
|
|
count = *c.Count
|
|
if count <= 0 {
|
|
return nil, rpcInvalidError("Count must be > 0")
|
|
}
|
|
|
|
// Limit the count to the total possible available intervals.
|
|
totalIntervals := (snapshot.Height + interval - 1) / interval
|
|
if int64(count) > totalIntervals {
|
|
count = int32(totalIntervals)
|
|
}
|
|
}
|
|
|
|
// Assemble JSON result.
|
|
result := types.GetStakeVersionInfoResult{
|
|
CurrentHeight: snapshot.Height,
|
|
Hash: snapshot.Hash.String(),
|
|
Intervals: make([]types.VersionInterval, 0, count),
|
|
}
|
|
|
|
startHeight := snapshot.Height
|
|
endHeight := chain.CalcWantHeight(interval, snapshot.Height) + 1
|
|
hash := &snapshot.Hash
|
|
adjust := int32(1) // We are off by one on the initial iteration.
|
|
for i := int32(0); i < count; i++ {
|
|
numBlocks := int32(startHeight - endHeight)
|
|
if numBlocks <= 0 {
|
|
// Just return what we got.
|
|
break
|
|
}
|
|
sv, err := chain.GetStakeVersions(hash, numBlocks+adjust)
|
|
if err != nil {
|
|
context := fmt.Sprintf("Failed to get stake versions starting "+
|
|
"from hash %v", hash)
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
posVersions := make(map[int]int)
|
|
voteVersions := make(map[int]int)
|
|
for _, v := range sv {
|
|
posVersions[int(v.StakeVersion)]++
|
|
for _, vote := range v.Votes {
|
|
voteVersions[int(vote.Version)]++
|
|
}
|
|
}
|
|
versionInterval := types.VersionInterval{
|
|
StartHeight: endHeight,
|
|
EndHeight: startHeight,
|
|
PoSVersions: convertVersionMap(posVersions),
|
|
VoteVersions: convertVersionMap(voteVersions),
|
|
}
|
|
result.Intervals = append(result.Intervals, versionInterval)
|
|
|
|
// Adjust interval.
|
|
endHeight -= interval
|
|
startHeight = endHeight + interval
|
|
adjust = 0
|
|
|
|
// Get prior block hash.
|
|
hash, err = chain.BlockHashByHeight(startHeight - 1)
|
|
if err != nil {
|
|
context := fmt.Sprintf("Failed to get block hash for height %d",
|
|
startHeight-1)
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// handleGetStakeVersions implements the getstakeversions command.
|
|
func handleGetStakeVersions(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.GetStakeVersionsCmd)
|
|
|
|
hash, err := chainhash.NewHashFromStr(c.Hash)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(c.Hash)
|
|
}
|
|
if c.Count <= 0 {
|
|
return nil, rpcInvalidError("Invalid parameter, count must " +
|
|
"be > 0")
|
|
}
|
|
|
|
sv, err := s.cfg.Chain.GetStakeVersions(hash, c.Count)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"Could not obtain stake versions")
|
|
}
|
|
|
|
result := types.GetStakeVersionsResult{
|
|
StakeVersions: make([]types.StakeVersions, 0, len(sv)),
|
|
}
|
|
for _, v := range sv {
|
|
nsv := types.StakeVersions{
|
|
Hash: v.Hash.String(),
|
|
Height: v.Height,
|
|
BlockVersion: v.BlockVersion,
|
|
StakeVersion: v.StakeVersion,
|
|
Votes: make([]types.VersionBits, 0,
|
|
len(v.Votes)),
|
|
}
|
|
for _, vote := range v.Votes {
|
|
nsv.Votes = append(nsv.Votes,
|
|
types.VersionBits{Version: vote.Version, Bits: vote.Bits})
|
|
}
|
|
|
|
result.StakeVersions = append(result.StakeVersions, nsv)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// handleGetTicketPoolValue implements the getticketpoolvalue command.
|
|
func handleGetTicketPoolValue(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
amt, err := s.cfg.Chain.TicketPoolValue()
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"Could not obtain ticket pool value")
|
|
}
|
|
|
|
return amt.ToCoin(), nil
|
|
}
|
|
|
|
// handleGetTreasuryBalance implements the gettreasurybalance command.
|
|
func handleGetTreasuryBalance(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.GetTreasuryBalanceCmd)
|
|
|
|
// Either parse the provided hash or use the current best tip hash when none
|
|
// is provided.
|
|
var hash chainhash.Hash
|
|
if c.Hash == nil || *c.Hash == "" {
|
|
hash = s.cfg.Chain.BestSnapshot().Hash
|
|
} else {
|
|
parsedHash, err := chainhash.NewHashFromStr(*c.Hash)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(*c.Hash)
|
|
}
|
|
hash = *parsedHash
|
|
}
|
|
|
|
balanceInfo, err := s.cfg.Chain.TreasuryBalance(&hash)
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, blockchain.ErrUnknownBlock):
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCBlockNotFound,
|
|
Message: fmt.Sprintf("Block not found: %s", hash),
|
|
}
|
|
|
|
case errors.Is(err, blockchain.ErrNoTreasuryBalance):
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCNoTreasury,
|
|
Message: fmt.Sprintf("Treasury inactive for block %s", hash),
|
|
}
|
|
}
|
|
|
|
context := "Failed to obtain treasury balance"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
tbr := types.GetTreasuryBalanceResult{
|
|
Hash: hash.String(),
|
|
Height: balanceInfo.BlockHeight,
|
|
Balance: balanceInfo.Balance,
|
|
}
|
|
if c.Verbose != nil && *c.Verbose {
|
|
tbr.Updates = balanceInfo.Updates
|
|
}
|
|
return tbr, nil
|
|
}
|
|
|
|
// handleGetTreasurySpendVotes implements the gettreasuryspendvotes command.
|
|
func handleGetTreasurySpendVotes(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.GetTreasurySpendVotesCmd)
|
|
|
|
// Shorter version of relevant parameters.
|
|
chain := s.cfg.Chain
|
|
tvi := s.cfg.ChainParams.TreasuryVoteInterval
|
|
mul := s.cfg.ChainParams.TreasuryVoteIntervalMultiplier
|
|
mempool := s.cfg.TxMempooler
|
|
|
|
// Either parse the provided hash or use the current best tip hash when
|
|
// none is provided.
|
|
var block chainhash.Hash
|
|
var blockHeight int64
|
|
var checkingMainChain bool
|
|
if c.Block == nil || *c.Block == "" {
|
|
best := s.cfg.Chain.BestSnapshot()
|
|
block = best.Hash
|
|
blockHeight = best.Height
|
|
checkingMainChain = true
|
|
} else {
|
|
if err := chainhash.Decode(&block, *c.Block); err != nil {
|
|
return nil, rpcDecodeHexError(*c.Block)
|
|
}
|
|
|
|
// Using HeaderByHash allows querying both the mainchain and
|
|
// any sidechains.
|
|
hdr, err := chain.HeaderByHash(&block)
|
|
if err != nil {
|
|
return nil, rpcBlockNotFoundError(block)
|
|
}
|
|
|
|
blockHeight = int64(hdr.Height)
|
|
checkingMainChain = chain.MainChainHasBlock(&block)
|
|
}
|
|
|
|
// When tallying votes on mainchain and for mined tspends, we'll only
|
|
// count votes up to when the tspend was mined. Thus we need to
|
|
// maintain some local information to be able to correctly identify the
|
|
// ending block for those types of tspends.
|
|
endBlocks := make(map[chainhash.Hash]chainhash.Hash)
|
|
|
|
// Determine whether to use the specified tspends or all the ones in
|
|
// the mempool.
|
|
var tspends []*dcrutil.Tx
|
|
if c.TSpends != nil && len(*c.TSpends) > 0 {
|
|
// Using client-specified tspends, they may be in the mempool,
|
|
// mined, or completely unknown, so handle each case.
|
|
tspends = make([]*dcrutil.Tx, len(*c.TSpends))
|
|
for i, s := range *c.TSpends {
|
|
var hash chainhash.Hash
|
|
if err := chainhash.Decode(&hash, s); err != nil {
|
|
return nil, rpcDecodeHexError(s)
|
|
}
|
|
|
|
// Check if this tspend is in the mempool.
|
|
var err error
|
|
tspends[i], err = mempool.FetchTransaction(&hash)
|
|
if err == nil {
|
|
// Sanity check this is actually a tspend.
|
|
if !stake.IsTSpend(tspends[i].MsgTx()) {
|
|
return nil, rpcInvalidError("mempool tx %s "+
|
|
"is not a tspend", hash)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Not in the mempool. Check if it is mined.
|
|
blocks, err := chain.FetchTSpend(hash)
|
|
if err != nil || len(blocks) == 0 {
|
|
// TSpend does not exist mined or in mempool.
|
|
return nil, rpcNoTxInfoError(&hash)
|
|
}
|
|
|
|
// TSpend exists mined in at least one block. Fetch the
|
|
// first one and extract the tspend.
|
|
fullBlock, err := chain.BlockByHash(&blocks[0])
|
|
if err != nil {
|
|
// Shouldn't happen unless tspend db is hosed.
|
|
context := "block containing mined tspend not found"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
// TSpends live in the stake tree.
|
|
var found bool
|
|
for _, tx := range fullBlock.STransactions() {
|
|
if tx.Hash().IsEqual(&hash) {
|
|
tspends[i] = tx
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
// Shouldn't happen unless tspend db is hosed
|
|
// or the assumption about tspends living in
|
|
// the stake tree is wrong.
|
|
context := "block did not contain tspend tx in stake tree"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
// If we're meant to tally main chain votes, figure out
|
|
// which (if any) of the blocks the tspend is found are
|
|
// in the main chain so we can count only up to that
|
|
// block (since it doesn't make sense to count
|
|
// additional votes _after_ the tspend was mined). This
|
|
// doesn't apply if we're not tallying main chain votes
|
|
// because we don't know the relationship between the
|
|
// requested branch and the branches that include the
|
|
// tspend so we just use the requested end block.
|
|
if !checkingMainChain {
|
|
continue
|
|
}
|
|
for _, block := range blocks {
|
|
if !chain.MainChainHasBlock(&block) {
|
|
continue
|
|
}
|
|
|
|
// Fetch the header to discover this block's
|
|
// height.
|
|
hdr, err := chain.HeaderByHash(&block)
|
|
if err != nil {
|
|
// Shouldn't happen.
|
|
context := "block without associated header"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
// Given this tspend was mined in the main
|
|
// chain, it doesn't make sense to count votes
|
|
// after it was mined. So stop early if the
|
|
// target block height is greater than or equal
|
|
// to the tspend's mined height. We need to
|
|
// count votes only up to the block _before_
|
|
// the tspend was mined.
|
|
if blockHeight >= int64(hdr.Height) {
|
|
endBlocks[hash] = hdr.PrevBlock
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
// Fetch vote counts for all mempool tspends.
|
|
hashes := mempool.TSpendHashes()
|
|
tspends = make([]*dcrutil.Tx, len(hashes))
|
|
for i, h := range hashes {
|
|
var err error
|
|
tspends[i], err = mempool.FetchTransaction(&h)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"could not fetch tspend from mempool")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fetch the vote counts from the blockchain.
|
|
votes := make([]types.TreasurySpendVotes, len(tspends))
|
|
for i, tx := range tspends {
|
|
txHash := tx.Hash()
|
|
|
|
// Early check to ensure this tx has a valid expiry and other
|
|
// functions will behave properly.
|
|
expiry := tx.MsgTx().Expiry
|
|
if !standalone.IsTreasuryVoteInterval(uint64(expiry-2), tvi) {
|
|
errStr := fmt.Sprintf("tspend %s has incorrect expiry %d", tx.Hash(),
|
|
expiry)
|
|
context := "tspend without correct expiry"
|
|
return nil, rpcInternalError(errStr, context)
|
|
}
|
|
|
|
// We only count votes for tspends that are inside their voting
|
|
// window. Otherwise we just return the appropriate vote start
|
|
// and end heights for it.
|
|
var yes, no int64
|
|
insideWindow := standalone.InsideTSpendWindow(blockHeight, expiry, tvi, mul)
|
|
minedBlock, isMined := endBlocks[*txHash]
|
|
if insideWindow || isMined {
|
|
// Determine whether to use the originally requested
|
|
// stop block or a custom one in case of mainchain
|
|
// mined tspends.
|
|
checkBlock := block
|
|
if isMined {
|
|
checkBlock = minedBlock
|
|
}
|
|
|
|
var err error
|
|
yes, no, err = chain.TSpendCountVotes(&checkBlock, tx)
|
|
if err != nil {
|
|
if errors.Is(err, blockchain.ErrUnknownBlock) {
|
|
return nil, rpcBlockNotFoundError(block)
|
|
}
|
|
|
|
context := "failed to obtain tspend votes"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
}
|
|
|
|
// The following error can be ignored because the expiry was verified to
|
|
// be in a TVI earlier.
|
|
start, end, _ := standalone.CalcTSpendWindow(expiry, tvi, mul)
|
|
|
|
votes[i] = types.TreasurySpendVotes{
|
|
Hash: txHash.String(),
|
|
Expiry: int64(expiry),
|
|
VoteStart: int64(start),
|
|
VoteEnd: int64(end),
|
|
YesVotes: yes,
|
|
NoVotes: no,
|
|
}
|
|
}
|
|
|
|
return types.GetTreasurySpendVotesResult{
|
|
Hash: block.String(),
|
|
Height: blockHeight,
|
|
Votes: votes,
|
|
}, nil
|
|
}
|
|
|
|
// handleGetVoteInfo implements the getvoteinfo command.
|
|
func handleGetVoteInfo(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.GetVoteInfoCmd)
|
|
|
|
// Shorter versions of some parameters for convenience.
|
|
interval := int64(s.cfg.ChainParams.RuleChangeActivationInterval)
|
|
quorum := s.cfg.ChainParams.RuleChangeActivationQuorum
|
|
chain := s.cfg.Chain
|
|
snapshot := chain.BestSnapshot()
|
|
|
|
vi, err := chain.GetVoteInfo(&snapshot.Hash, c.Version)
|
|
if err != nil {
|
|
if errors.Is(err, blockchain.ErrUnknownDeploymentVersion) {
|
|
return nil, rpcInvalidError("%d: unrecognized vote version",
|
|
c.Version)
|
|
}
|
|
return nil, rpcInternalError(err.Error(), "could not obtain vote info")
|
|
}
|
|
|
|
// Assemble JSON result.
|
|
result := types.GetVoteInfoResult{
|
|
CurrentHeight: snapshot.Height,
|
|
StartHeight: chain.CalcWantHeight(interval, snapshot.Height) + 1,
|
|
EndHeight: chain.CalcWantHeight(interval, snapshot.Height) + interval,
|
|
Hash: snapshot.Hash.String(),
|
|
VoteVersion: c.Version,
|
|
Quorum: quorum,
|
|
}
|
|
|
|
// We don't fail, we try to return the totals for this version.
|
|
result.TotalVotes, err = chain.CountVoteVersion(c.Version)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"could not count voter versions")
|
|
}
|
|
|
|
result.Agendas = make([]types.Agenda, 0, len(vi.Agendas))
|
|
for _, agenda := range vi.Agendas {
|
|
// Obtain status of agenda.
|
|
state, err := chain.NextThresholdState(&snapshot.Hash, c.Version,
|
|
agenda.Vote.Id)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"could not fetch next threshold state")
|
|
}
|
|
|
|
a := types.Agenda{
|
|
ID: agenda.Vote.Id,
|
|
Description: agenda.Vote.Description,
|
|
Mask: agenda.Vote.Mask,
|
|
Choices: make([]types.Choice, 0, len(agenda.Vote.Choices)),
|
|
StartTime: agenda.StartTime,
|
|
ExpireTime: agenda.ExpireTime,
|
|
Status: state.String(),
|
|
}
|
|
|
|
// Handle choices.
|
|
for _, choice := range agenda.Vote.Choices {
|
|
a.Choices = append(a.Choices, types.Choice{
|
|
ID: choice.Id,
|
|
Description: choice.Description,
|
|
Bits: choice.Bits,
|
|
IsAbstain: choice.IsAbstain,
|
|
IsNo: choice.IsNo,
|
|
})
|
|
}
|
|
|
|
if state.State != blockchain.ThresholdStarted {
|
|
// Append transformed agenda without progress.
|
|
result.Agendas = append(result.Agendas, a)
|
|
continue
|
|
}
|
|
|
|
counts, err := s.cfg.Chain.GetVoteCounts(c.Version, agenda.Vote.Id)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"could not obtain vote count")
|
|
}
|
|
|
|
// Calculate quorum.
|
|
qmin := quorum
|
|
totalNonAbstain := counts.Total - counts.TotalAbstain
|
|
if totalNonAbstain < quorum {
|
|
qmin = totalNonAbstain
|
|
}
|
|
a.QuorumProgress = float64(qmin) / float64(quorum)
|
|
|
|
// Calculate choice progress.
|
|
for k := range a.Choices {
|
|
a.Choices[k].Count = counts.VoteChoices[k]
|
|
a.Choices[k].Progress = float64(counts.VoteChoices[k]) /
|
|
float64(counts.Total)
|
|
}
|
|
|
|
// Append transformed agenda.
|
|
result.Agendas = append(result.Agendas, a)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// bigToLEUint256 returns the passed big integer as an unsigned 256-bit integer
|
|
// encoded as little-endian bytes. Numbers which are larger than the max
|
|
// unsigned 256-bit integer are truncated.
|
|
func bigToLEUint256(n *big.Int) [uint256Size]byte {
|
|
// Pad or truncate the big-endian big int to correct number of bytes.
|
|
nBytes := n.Bytes()
|
|
nlen := len(nBytes)
|
|
pad := 0
|
|
start := 0
|
|
if nlen <= uint256Size {
|
|
pad = uint256Size - nlen
|
|
} else {
|
|
start = nlen - uint256Size
|
|
}
|
|
var buf [uint256Size]byte
|
|
copy(buf[pad:], nBytes[start:])
|
|
|
|
// Reverse the bytes to little endian and return them.
|
|
for i := 0; i < uint256Size/2; i++ {
|
|
buf[i], buf[uint256Size-1-i] = buf[uint256Size-1-i], buf[i]
|
|
}
|
|
return buf
|
|
}
|
|
|
|
// handleGetTxOut handles gettxout commands.
|
|
func handleGetTxOut(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.GetTxOutCmd)
|
|
|
|
// Convert the provided transaction hash hex to a Hash.
|
|
txHash, err := chainhash.NewHashFromStr(c.Txid)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(c.Txid)
|
|
}
|
|
|
|
if !(c.Tree == wire.TxTreeRegular || c.Tree == wire.TxTreeStake) {
|
|
return nil, rpcInvalidError("Tx tree must be regular or stake")
|
|
}
|
|
|
|
chain := s.cfg.Chain
|
|
best := chain.BestSnapshot()
|
|
|
|
// 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 bestBlockHash string
|
|
var confirmations int64
|
|
var value int64
|
|
var scriptVersion uint16
|
|
var pkScript []byte
|
|
var isCoinbase bool
|
|
var isTreasuryEnabled bool
|
|
includeMempool := true
|
|
if c.IncludeMempool != nil {
|
|
includeMempool = *c.IncludeMempool
|
|
}
|
|
var txFromMempool *dcrutil.Tx
|
|
if includeMempool {
|
|
txFromMempool, _ = s.cfg.TxMempooler.FetchTransaction(txHash)
|
|
|
|
// Set as nil if the tx tree does not match the tree param that was passed.
|
|
// This is set as nil rather than returning immediately here since it is
|
|
// technically possible (though extremely unlikely) that the tx exists
|
|
// elsewhere, so it should still continue and check elsewhere below.
|
|
if txFromMempool != nil && txFromMempool.Tree() != c.Tree {
|
|
txFromMempool = nil
|
|
}
|
|
}
|
|
if txFromMempool != nil {
|
|
mtx := txFromMempool.MsgTx()
|
|
if c.Vout > uint32(len(mtx.TxOut)-1) {
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCInvalidTxVout,
|
|
Message: "Output index number (vout) does not " +
|
|
"exist for transaction.",
|
|
}
|
|
}
|
|
|
|
txOut := mtx.TxOut[c.Vout]
|
|
if txOut == nil {
|
|
errStr := fmt.Sprintf("Output index: %d for txid: %s "+
|
|
"does not exist", c.Vout, txHash)
|
|
return nil, rpcInternalError(errStr, "")
|
|
}
|
|
|
|
// The transaction output in question is from the mempool, so determine
|
|
// if the treasury rules are active from the point of view of the
|
|
// current best tip.
|
|
isTreasuryEnabled, err = s.isTreasuryAgendaActive(&best.PrevHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bestBlockHash = best.Hash.String()
|
|
confirmations = 0
|
|
value = txOut.Value
|
|
scriptVersion = txOut.Version
|
|
pkScript = txOut.PkScript
|
|
isCoinbase = standalone.IsCoinBaseTx(mtx, isTreasuryEnabled)
|
|
} else {
|
|
outpoint := wire.OutPoint{Hash: *txHash, Index: c.Vout, Tree: c.Tree}
|
|
entry, err := chain.FetchUtxoEntry(outpoint)
|
|
if err != nil {
|
|
context := "Failed to retrieve utxo entry"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
// To match the behavior of the reference client, return nil
|
|
// (JSON null) if the transaction output could not be found
|
|
// (never existed or was pruned) or is spent by another
|
|
// transaction already in the main chain. Mined transactions
|
|
// that are spent by a mempool transaction are not affected by
|
|
// this.
|
|
if entry == nil || entry.IsSpent() {
|
|
return nil, nil
|
|
}
|
|
|
|
bestBlockHash = best.Hash.String()
|
|
confirmations = 1 + best.Height - entry.BlockHeight()
|
|
value = entry.Amount()
|
|
scriptVersion = entry.ScriptVersion()
|
|
pkScript = entry.PkScript()
|
|
isCoinbase = entry.IsCoinBase()
|
|
}
|
|
|
|
// Disassemble script into single line printable format. The
|
|
// disassembled string will contain [error] inline if the script
|
|
// doesn't fully parse, so ignore the error here.
|
|
script := pkScript
|
|
disbuf, _ := txscript.DisasmString(script)
|
|
|
|
// Attempt to extract known addresses associated with the script.
|
|
scriptType, addrs := stdscript.ExtractAddrs(scriptVersion, script,
|
|
s.cfg.ChainParams)
|
|
addresses := make([]string, len(addrs))
|
|
for i, addr := range addrs {
|
|
addresses[i] = addr.String()
|
|
}
|
|
|
|
// Determine the number of required signatures for known standard types.
|
|
reqSigs := stdscript.DetermineRequiredSigs(scriptVersion, script)
|
|
|
|
txOutReply := &types.GetTxOutResult{
|
|
BestBlock: bestBlockHash,
|
|
Confirmations: confirmations,
|
|
Value: dcrutil.Amount(value).ToUnit(dcrutil.AmountCoin),
|
|
ScriptPubKey: types.ScriptPubKeyResult{
|
|
Asm: disbuf,
|
|
Hex: hex.EncodeToString(pkScript),
|
|
ReqSigs: int32(reqSigs),
|
|
Type: scriptType.String(),
|
|
Addresses: addresses,
|
|
Version: scriptVersion,
|
|
},
|
|
Coinbase: isCoinbase,
|
|
}
|
|
return txOutReply, nil
|
|
}
|
|
|
|
// handleGetTxOutSetInfo returns statistics on the current unspent transaction output set.
|
|
func handleGetTxOutSetInfo(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
best := s.cfg.Chain.BestSnapshot()
|
|
stats, err := s.cfg.Chain.FetchUtxoStats()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return types.GetTxOutSetInfoResult{
|
|
Height: best.Height,
|
|
BestBlock: best.Hash.String(),
|
|
Transactions: stats.Transactions,
|
|
TxOuts: stats.Utxos,
|
|
DiskSize: stats.Size,
|
|
TotalAmount: stats.Total,
|
|
SerializedHash: stats.SerializedHash.String(),
|
|
}, nil
|
|
}
|
|
|
|
// pruneOldBlockTemplates prunes all old block templates from the templatePool
|
|
// map.
|
|
//
|
|
// This function MUST be called with the RPC workstate locked.
|
|
func (s *workState) pruneOldBlockTemplates(bestHeight int64) {
|
|
pruneHeight := bestHeight - getworkExpirationDiff
|
|
for key, block := range s.templatePool {
|
|
height := int64(block.Header.Height)
|
|
if height < pruneHeight {
|
|
delete(s.templatePool, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// getWorkTemplateKey returns the key to use for the template pool that houses
|
|
// the information necessary to construct full blocks from getwork submissions.
|
|
func getWorkTemplateKey(header *wire.BlockHeader) [merkleRootPairSize]byte {
|
|
// Create the new template key which is the MerkleRoot + StakeRoot fields.
|
|
// Note that with DCP0005 active, the stake root field will be merged with
|
|
// the merkle root field and the stake root field will be a commitment
|
|
// root. Since the commitment root changes with the contents of the block
|
|
// it will still be a good piece of information to add to the key.
|
|
var merkleRootPair [merkleRootPairSize]byte
|
|
copy(merkleRootPair[:chainhash.HashSize], header.MerkleRoot[:])
|
|
copy(merkleRootPair[chainhash.HashSize:], header.StakeRoot[:])
|
|
return merkleRootPair
|
|
}
|
|
|
|
// handleGetWorkRequest is a helper for handleGetWork which deals with
|
|
// generating and returning work to the caller.
|
|
//
|
|
// This function MUST be called with the RPC workstate locked.
|
|
func handleGetWorkRequest(s *Server) (interface{}, error) {
|
|
// Prune old templates and wait for updated templates when the current best
|
|
// chain changes.
|
|
state := s.workState
|
|
best := s.cfg.Chain.BestSnapshot()
|
|
bt := s.cfg.BlockTemplater
|
|
var template *mining.BlockTemplate
|
|
if state.prevHash == nil || *state.prevHash != best.Hash {
|
|
// Prune old templates from the pool when the best block changes.
|
|
state.pruneOldBlockTemplates(best.Height)
|
|
state.prevHash = &best.Hash
|
|
|
|
// Wait until a new template is generated. Since the subscription
|
|
// immediately sends the current template, the template might not have
|
|
// been updated yet (for example, it might be waiting on votes). In
|
|
// that case, wait for the updated template with an eventual timeout
|
|
// in case the new tip never gets enough votes and no other events
|
|
// that trigger a new template have happened.
|
|
templateSub := bt.Subscribe()
|
|
templateNtfn := <-templateSub.C()
|
|
template = templateNtfn.Template
|
|
templateKey := getWorkTemplateKey(&template.Block.Header)
|
|
if _, ok := state.templatePool[templateKey]; ok {
|
|
const maxTemplateTimeoutDuration = time.Millisecond * 5500
|
|
select {
|
|
case templateNtfn = <-templateSub.C():
|
|
template = templateNtfn.Template
|
|
|
|
case <-time.After(maxTemplateTimeoutDuration):
|
|
template = nil
|
|
}
|
|
}
|
|
templateSub.Stop()
|
|
}
|
|
|
|
// Grab the current template from the background generator immediately when
|
|
// it was not already obtained above. Return any errors that might have
|
|
// happened when generating the template.
|
|
if template == nil {
|
|
var err error
|
|
template, err = bt.CurrentTemplate()
|
|
if err != nil {
|
|
context := "Unable to retrieve work due to invalid template"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
if template == nil {
|
|
return nil, rpcMiscError("no work is available during a chain " +
|
|
"reorganization")
|
|
}
|
|
}
|
|
|
|
// Update the time of the block template to the current time while
|
|
// accounting for the median time of the past several blocks per the chain
|
|
// consensus rules. Note that the header is copied to avoid mutating the
|
|
// shared block template.
|
|
headerCopy := template.Block.Header
|
|
err := bt.UpdateBlockTime(&headerCopy)
|
|
if err != nil {
|
|
context := "Failed to update block time"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
// Serialize the block header into a buffer large enough to hold the
|
|
// the block header and the internal blake256 padding that is added and
|
|
// retuned as part of the data below.
|
|
//
|
|
// For reference (0-index based, end value is exclusive):
|
|
// data[115:119] --> Bits
|
|
// data[135:139] --> Timestamp
|
|
// data[139:143] --> Nonce
|
|
// data[143:151] --> ExtraNonce
|
|
data := make([]byte, 0, getworkDataLen)
|
|
buf := bytes.NewBuffer(data)
|
|
err = headerCopy.Serialize(buf)
|
|
if err != nil {
|
|
context := "Failed to serialize data"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
// Add the template to the template pool. Since the key is a combination
|
|
// of the merkle and stake root fields, this will not add duplicate entries
|
|
// for the templates with modified timestamps and/or difficulty bits.
|
|
templateKey := getWorkTemplateKey(&headerCopy)
|
|
state.templatePool[templateKey] = template.Block
|
|
|
|
// Expand the data slice to include the full data buffer and apply the
|
|
// internal blake256 padding. This makes the data ready for callers to
|
|
// make use of only the final chunk along with the midstate for the
|
|
// rest.
|
|
data = data[:getworkDataLen]
|
|
copy(data[wire.MaxBlockHeaderPayload:], blake256Pad)
|
|
|
|
// The target is in big endian, but it is treated as a uint256 and byte
|
|
// swapped to little endian in the final result. Even though there is
|
|
// really no reason for it to be swapped, it is a holdover from legacy code
|
|
// and is now required for compatibility.
|
|
target := bigToLEUint256(standalone.CompactToBig(headerCopy.Bits))
|
|
reply := &types.GetWorkResult{
|
|
Data: hex.EncodeToString(data),
|
|
Target: hex.EncodeToString(target[:]),
|
|
}
|
|
return reply, nil
|
|
}
|
|
|
|
// handleGetWorkSubmission is a helper for handleGetWork which deals with
|
|
// the calling submitting work to be verified and processed.
|
|
//
|
|
// This function MUST be called with the RPC workstate locked.
|
|
func handleGetWorkSubmission(_ context.Context, s *Server, hexData string) (interface{}, error) {
|
|
// Ensure the provided data is sane.
|
|
if len(hexData)%2 != 0 {
|
|
hexData = "0" + hexData
|
|
}
|
|
data, err := hex.DecodeString(hexData)
|
|
if err != nil {
|
|
return false, rpcDecodeHexError(hexData)
|
|
}
|
|
if len(data) != getworkDataLen {
|
|
return nil, rpcInvalidError("Argument must be %d bytes (not "+
|
|
"%d)", getworkDataLen, len(data))
|
|
}
|
|
|
|
// Deserialize the block header from the data.
|
|
var submittedHeader wire.BlockHeader
|
|
bhBuf := bytes.NewReader(data[0:wire.MaxBlockHeaderPayload])
|
|
err = submittedHeader.Deserialize(bhBuf)
|
|
if err != nil {
|
|
return false, rpcInvalidError("Invalid block header: %v", err)
|
|
}
|
|
|
|
// Ensure the submitted block hash is less than the target difficulty.
|
|
blockHash := submittedHeader.BlockHash()
|
|
err = standalone.CheckProofOfWork(&blockHash, submittedHeader.Bits,
|
|
s.cfg.ChainParams.PowLimit)
|
|
if err != nil {
|
|
// Anything other than a rule violation is an unexpected error, so
|
|
// return that error as an internal error.
|
|
var rErr standalone.RuleError
|
|
if !errors.As(err, &rErr) {
|
|
context := "Unexpected error while checking proof of work"
|
|
return false, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
log.Errorf("Block submitted via getwork does not meet the "+
|
|
"required proof of work: %v", err)
|
|
return false, nil
|
|
}
|
|
|
|
// Look up the full block for the provided data based on the merkle and
|
|
// stake roots. Return false to indicate the solve failed if it's not
|
|
// available.
|
|
templateKey := getWorkTemplateKey(&submittedHeader)
|
|
templateBlock, ok := s.workState.templatePool[templateKey]
|
|
if !ok || templateBlock == nil {
|
|
log.Errorf("Block submitted via getwork has no matching template "+
|
|
"for merkle root %s, stake root %s",
|
|
submittedHeader.MerkleRoot, submittedHeader.StakeRoot)
|
|
return false, nil
|
|
}
|
|
|
|
// Reconstruct the block using the submitted header stored block info. Note
|
|
// that the block template is shallow copied to avoid mutating the header
|
|
// of the shared block template.
|
|
msgBlock := *templateBlock
|
|
msgBlock.Header = submittedHeader
|
|
block := dcrutil.NewBlock(&msgBlock)
|
|
|
|
// Process this block using the same rules as blocks coming from other
|
|
// nodes. This will in turn relay it to the network like normal.
|
|
err = s.cfg.SyncMgr.SubmitBlock(block)
|
|
if err != nil {
|
|
if errors.Is(err, blockchain.ErrMissingParent) {
|
|
log.Infof("Block submitted via getwork rejected: orphan building "+
|
|
"on parent %v", block.MsgBlock().Header.PrevBlock)
|
|
return false, nil
|
|
}
|
|
|
|
// Anything other than a rule violation is an unexpected error,
|
|
// so return that error as an internal error.
|
|
var rErr blockchain.RuleError
|
|
if !errors.As(err, &rErr) {
|
|
context := "Unexpected error while processing block"
|
|
return false, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
log.Infof("Block submitted via getwork rejected: %v", err)
|
|
return false, nil
|
|
}
|
|
|
|
// The block was accepted.
|
|
log.Infof("Block submitted via getwork accepted: %s (height %d)",
|
|
block.Hash(), msgBlock.Header.Height)
|
|
return true, nil
|
|
}
|
|
|
|
// handleGetWork implements the getwork command.
|
|
func handleGetWork(ctx context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
if s.cfg.CPUMiner.IsMining() {
|
|
return nil, rpcMiscError("getwork polling is disallowed " +
|
|
"while CPU mining is enabled. Please disable CPU " +
|
|
"mining and try again.")
|
|
}
|
|
|
|
// Respond with an error if there are no addresses to pay the created
|
|
// blocks to.
|
|
if len(s.cfg.MiningAddrs) == 0 {
|
|
return nil, rpcInternalError("No payment addresses specified "+
|
|
"via --miningaddr", "Configuration")
|
|
}
|
|
|
|
// Return an error if there are no peers connected since there is no way to
|
|
// relay a found block or receive transactions to work on unless
|
|
// unsynchronized mining has specifically been allowed.
|
|
if !s.cfg.AllowUnsyncedMining && s.cfg.ConnMgr.ConnectedCount() == 0 {
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCClientNotConnected,
|
|
Message: "Decred is not connected",
|
|
}
|
|
}
|
|
|
|
// No point in generating or accepting work before the chain is synced
|
|
// unless unsynchronized mining has specifically been allowed.
|
|
bestHeight := s.cfg.Chain.BestSnapshot().Height
|
|
if !s.cfg.AllowUnsyncedMining && bestHeight != 0 && !s.cfg.Chain.IsCurrent() {
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCClientInInitialDownload,
|
|
Message: "Decred is downloading blocks...",
|
|
}
|
|
}
|
|
|
|
c := cmd.(*types.GetWorkCmd)
|
|
|
|
// Protect concurrent access from multiple RPC invocations for work
|
|
// requests and submission.
|
|
s.workState.Lock()
|
|
defer s.workState.Unlock()
|
|
|
|
// 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 != nil && *c.Data != "" {
|
|
return handleGetWorkSubmission(ctx, s, *c.Data)
|
|
}
|
|
|
|
// No data was provided, so the caller is requesting work.
|
|
return handleGetWorkRequest(s)
|
|
}
|
|
|
|
// handleHelp implements the help command.
|
|
func handleHelp(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.HelpCmd)
|
|
|
|
// Provide a usage overview of all commands when no specific command
|
|
// was specified.
|
|
var method types.Method
|
|
if c.Command != nil {
|
|
method = types.Method(*c.Command)
|
|
}
|
|
if method == "" {
|
|
usage, err := s.helpCacher.RPCUsage(false)
|
|
if err != nil {
|
|
context := "Failed to generate RPC usage"
|
|
return nil, rpcInternalError(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[method]; !ok {
|
|
return nil, rpcInvalidError("Unknown method: %v", method)
|
|
}
|
|
|
|
// Get the help for the command.
|
|
help, err := s.helpCacher.RPCMethodHelp(method)
|
|
if err != nil {
|
|
context := "Failed to generate help"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
return help, nil
|
|
}
|
|
|
|
// handleInvalidateBlock implements the invalidateblock command.
|
|
func handleInvalidateBlock(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.InvalidateBlockCmd)
|
|
hash, err := chainhash.NewHashFromStr(c.BlockHash)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(c.BlockHash)
|
|
}
|
|
|
|
chain := s.cfg.Chain
|
|
err = chain.InvalidateBlock(hash)
|
|
if err != nil {
|
|
if errors.Is(err, blockchain.ErrUnknownBlock) {
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCBlockNotFound,
|
|
Message: fmt.Sprintf("Block not found: %v", hash),
|
|
}
|
|
}
|
|
|
|
if errors.Is(err, blockchain.ErrInvalidateGenesisBlock) {
|
|
return nil, rpcInvalidError("%v", err)
|
|
}
|
|
|
|
context := fmt.Sprintf("Failed to invalidate block %s", hash)
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// handleLiveTickets implements the livetickets command.
|
|
func handleLiveTickets(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
lt, err := s.cfg.Chain.LiveTickets()
|
|
if err != nil {
|
|
return nil, rpcInternalError("Could not get live tickets "+
|
|
err.Error(), "")
|
|
}
|
|
|
|
ltString := make([]string, len(lt))
|
|
for i := range lt {
|
|
ltString[i] = lt[i].String()
|
|
}
|
|
|
|
return types.LiveTicketsResult{Tickets: ltString}, nil
|
|
}
|
|
|
|
// handleMissedTickets implements the missedtickets command.
|
|
func handleMissedTickets(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
mt, err := s.cfg.Chain.MissedTickets()
|
|
if err != nil {
|
|
return nil, rpcInternalError("Could not get missed tickets "+
|
|
err.Error(), "")
|
|
}
|
|
|
|
mtString := make([]string, len(mt))
|
|
for i, hash := range mt {
|
|
mtString[i] = hash.String()
|
|
}
|
|
|
|
return types.MissedTicketsResult{Tickets: mtString}, nil
|
|
}
|
|
|
|
// handlePing implements the ping command.
|
|
func handlePing(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
// Ask server to ping \o_
|
|
nonce, err := wire.RandomUint64()
|
|
if err != nil {
|
|
return nil, rpcInternalError("Not sending ping - failed to "+
|
|
"generate nonce: "+err.Error(), "")
|
|
}
|
|
s.cfg.ConnMgr.BroadcastMessage(wire.NewMsgPing(nonce))
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// handleReconsiderBlock implements the reconsiderblock command.
|
|
func handleReconsiderBlock(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.ReconsiderBlockCmd)
|
|
hash, err := chainhash.NewHashFromStr(c.BlockHash)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(c.BlockHash)
|
|
}
|
|
|
|
chain := s.cfg.Chain
|
|
err = chain.ReconsiderBlock(hash)
|
|
if err != nil {
|
|
if errors.Is(err, blockchain.ErrUnknownBlock) {
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCBlockNotFound,
|
|
Message: fmt.Sprintf("Block not found: %v", hash),
|
|
}
|
|
}
|
|
|
|
// Use separate error code for failed validation.
|
|
allRuleErrs := func(err error) bool {
|
|
var rErr blockchain.RuleError
|
|
if !errors.As(err, &rErr) {
|
|
return false
|
|
}
|
|
|
|
var mErr blockchain.MultiError
|
|
if errors.As(err, &mErr) {
|
|
for _, e := range mErr {
|
|
if !errors.As(e, &rErr) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
if allRuleErrs(err) {
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCReconsiderFailure,
|
|
Message: fmt.Sprintf("Reconsidering block %s led to one or "+
|
|
"more validation failures: %v", hash, err),
|
|
}
|
|
}
|
|
|
|
// Fall back to an internal error.
|
|
context := fmt.Sprintf("Error while reconsidering block %s", hash)
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// handleRegenTemplate implements the regentemplate command.
|
|
func handleRegenTemplate(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
bt := s.cfg.BlockTemplater
|
|
if bt == nil {
|
|
return nil, rpcInternalError("Node is not configured for mining", "")
|
|
}
|
|
bt.ForceRegen()
|
|
return nil, nil
|
|
}
|
|
|
|
// retrievedTx represents a transaction that was either loaded from the
|
|
// transaction memory pool or from the database. When a transaction is loaded
|
|
// from the database, it is loaded with the raw serialized bytes while the
|
|
// mempool has the fully deserialized structure. This structure therefore will
|
|
// have one of the two fields set depending on where is was retrieved from.
|
|
// This is mainly done for efficiency to avoid extra serialization steps when
|
|
// possible.
|
|
type retrievedTx struct {
|
|
txBytes []byte
|
|
blkHash *chainhash.Hash // Only set when transaction is in a block.
|
|
blkIndex uint32 // Only set when transaction is in a block.
|
|
tx *dcrutil.Tx
|
|
}
|
|
|
|
// fetchInputTxos fetches the outpoints from all transactions referenced by the
|
|
// inputs to the passed transaction by checking the transaction mempool first
|
|
// then the transaction index for those already mined into blocks.
|
|
func fetchInputTxos(s *Server, tx *wire.MsgTx, isTreasuryEnabled bool) (map[wire.OutPoint]wire.TxOut, error) {
|
|
originOutputs := make(map[wire.OutPoint]wire.TxOut)
|
|
voteTx := stake.IsSSGen(tx, isTreasuryEnabled)
|
|
for txInIndex, txIn := range tx.TxIn {
|
|
// vote tx have null input for vin[0],
|
|
// skip since it resolves to an invalid transaction
|
|
if voteTx && txInIndex == 0 {
|
|
continue
|
|
}
|
|
// Attempt to fetch and use the referenced transaction from the
|
|
// memory pool.
|
|
origin := &txIn.PreviousOutPoint
|
|
originTx, err := s.cfg.TxMempooler.FetchTransaction(&origin.Hash)
|
|
if err == nil {
|
|
txOuts := originTx.MsgTx().TxOut
|
|
if origin.Index >= uint32(len(txOuts)) {
|
|
errStr := fmt.Sprintf("unable to find output "+
|
|
"%v referenced from transaction %s:%d",
|
|
origin, tx.TxHash(), txInIndex)
|
|
return nil, rpcInternalError(errStr, "")
|
|
}
|
|
|
|
originOutputs[*origin] = *txOuts[origin.Index]
|
|
continue
|
|
}
|
|
|
|
// Ensure the tx index is synced.
|
|
txIndex := s.cfg.TxIndexer
|
|
tHeight, tHash, err := txIndex.Tip()
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Tip")
|
|
}
|
|
|
|
chain := s.cfg.Chain
|
|
|
|
// Return an out-of-sync error if index is lagging a
|
|
// maximum reorg depth (6) blocks or more from the chain tip.
|
|
if chain.BestSnapshot().Height > (tHeight + 5) {
|
|
msg := fmt.Sprintf("%s: index not synced", txIndex.Name())
|
|
return nil, rpcInternalError(msg, "Sync")
|
|
}
|
|
|
|
sync:
|
|
for !chain.BestSnapshot().Hash.IsEqual(tHash) {
|
|
select {
|
|
case <-time.After(syncWait):
|
|
msg := fmt.Sprintf("%s: index not synced", txIndex.Name())
|
|
return nil, rpcInternalError(msg, "Sync")
|
|
case <-txIndex.WaitForSync():
|
|
break sync
|
|
}
|
|
}
|
|
|
|
// Look up the location of the transaction.
|
|
idxEntry, err := txIndex.Entry(&origin.Hash)
|
|
if err != nil {
|
|
context := "Failed to retrieve transaction location"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
if idxEntry == nil {
|
|
return nil, rpcNoTxInfoError(&origin.Hash)
|
|
}
|
|
blockRegion := &idxEntry.BlockRegion
|
|
|
|
// Load the raw transaction bytes from the database.
|
|
var txBytes []byte
|
|
err = s.cfg.DB.View(func(dbTx database.Tx) error {
|
|
var err error
|
|
txBytes, err = dbTx.FetchBlockRegion(blockRegion)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, rpcNoTxInfoError(&origin.Hash)
|
|
}
|
|
|
|
// Deserialize the transaction
|
|
var msgTx wire.MsgTx
|
|
err = msgTx.Deserialize(bytes.NewReader(txBytes))
|
|
if err != nil {
|
|
context := "Failed to deserialize transaction"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
|
|
// Add the referenced output to the map.
|
|
if origin.Index >= uint32(len(msgTx.TxOut)) {
|
|
errStr := fmt.Sprintf("unable to find output %v "+
|
|
"referenced from transaction %s:%d", origin,
|
|
tx.TxHash(), txInIndex)
|
|
return nil, rpcInternalError(errStr, "")
|
|
}
|
|
originOutputs[*origin] = *msgTx.TxOut[origin.Index]
|
|
}
|
|
|
|
return originOutputs, nil
|
|
}
|
|
|
|
// createVinListPrevOut returns a slice of JSON objects for the inputs of the
|
|
// passed transaction.
|
|
func createVinListPrevOut(s *Server, mtx *wire.MsgTx,
|
|
chainParams *chaincfg.Params,
|
|
vinExtra bool, filterAddrMap map[string]struct{},
|
|
isTreasuryEnabled bool) ([]types.VinPrevOut, error) {
|
|
|
|
// Treasurybase transactions only have a single txin by definition.
|
|
//
|
|
// NOTE: This check MUST come before the coinbase check because a
|
|
// treasurybase will be identified as a coinbase as well.
|
|
if isTreasuryEnabled && standalone.IsTreasuryBase(mtx) {
|
|
// Only include the transaction if the filter map is empty because a
|
|
// treasurybase input has no addresses and so would never match a
|
|
// non-empty filter.
|
|
if len(filterAddrMap) != 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
txIn := mtx.TxIn[0]
|
|
vinList := make([]types.VinPrevOut, 1)
|
|
vinEntry := &vinList[0]
|
|
vinEntry.Treasurybase = true
|
|
vinEntry.Sequence = txIn.Sequence
|
|
vinEntry.AmountIn = dcrjson.Float64(dcrutil.Amount(txIn.ValueIn).ToCoin())
|
|
return vinList, nil
|
|
}
|
|
|
|
// Coinbase transactions only have a single txin by definition.
|
|
if standalone.IsCoinBaseTx(mtx, isTreasuryEnabled) {
|
|
// Only include the transaction if the filter map is empty
|
|
// because a coinbase input has no addresses and so would never
|
|
// match a non-empty filter.
|
|
if len(filterAddrMap) != 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
txIn := mtx.TxIn[0]
|
|
vinList := make([]types.VinPrevOut, 1)
|
|
vinEntry := &vinList[0]
|
|
vinEntry.Coinbase = hex.EncodeToString(txIn.SignatureScript)
|
|
vinEntry.Sequence = txIn.Sequence
|
|
vinEntry.AmountIn = dcrjson.Float64(dcrutil.Amount(txIn.ValueIn).ToCoin())
|
|
return vinList, nil
|
|
}
|
|
|
|
// Treasury spend transactions only have a single txin by definition.
|
|
if isTreasuryEnabled && stake.IsTSpend(mtx) {
|
|
// Only include the transaction if the filter map is empty because a
|
|
// treasury spend input has no addresses and so would never match a
|
|
// non-empty filter.
|
|
if len(filterAddrMap) != 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
txIn := mtx.TxIn[0]
|
|
vinList := make([]types.VinPrevOut, 1)
|
|
vinEntry := &vinList[0]
|
|
vinEntry.TreasurySpend = hex.EncodeToString(txIn.SignatureScript)
|
|
vinEntry.Sequence = txIn.Sequence
|
|
vinEntry.AmountIn = dcrjson.Float64(dcrutil.Amount(txIn.ValueIn).ToCoin())
|
|
return vinList, nil
|
|
}
|
|
|
|
// Use a dynamically sized list to accommodate the address filter.
|
|
vinList := make([]types.VinPrevOut, 0, len(mtx.TxIn))
|
|
|
|
// Lookup all of the referenced transaction outputs needed to populate
|
|
// the previous output information if requested.
|
|
var originOutputs map[wire.OutPoint]wire.TxOut
|
|
if vinExtra || len(filterAddrMap) > 0 {
|
|
var err error
|
|
originOutputs, err = fetchInputTxos(s, mtx, isTreasuryEnabled)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Stakebase transactions (votes) have two inputs: a null stake base
|
|
// followed by an input consuming a ticket's stakesubmission.
|
|
isSSGen := stake.IsSSGen(mtx, isTreasuryEnabled)
|
|
|
|
for i, txIn := range mtx.TxIn {
|
|
// Handle only the null input of a stakebase differently.
|
|
if isSSGen && i == 0 {
|
|
amountIn := dcrutil.Amount(txIn.ValueIn).ToCoin()
|
|
vinEntry := types.VinPrevOut{
|
|
Stakebase: hex.EncodeToString(txIn.SignatureScript),
|
|
Sequence: txIn.Sequence,
|
|
AmountIn: &amountIn,
|
|
}
|
|
vinList = append(vinList, vinEntry)
|
|
// No previous outpoints to check against the address filter.
|
|
continue
|
|
}
|
|
|
|
// The disassembled string will contain [error] inline if the
|
|
// script doesn't fully parse, so ignore the error here.
|
|
disbuf, _ := txscript.DisasmString(txIn.SignatureScript)
|
|
|
|
// Create the basic input entry without the additional optional
|
|
// previous output details which will be added later if
|
|
// requested and available.
|
|
prevOut := &txIn.PreviousOutPoint
|
|
amountIn := dcrutil.Amount(txIn.ValueIn).ToCoin()
|
|
vinEntry := types.VinPrevOut{
|
|
Txid: prevOut.Hash.String(),
|
|
Vout: prevOut.Index,
|
|
Tree: prevOut.Tree,
|
|
Sequence: txIn.Sequence,
|
|
AmountIn: &amountIn,
|
|
BlockHeight: &txIn.BlockHeight,
|
|
BlockIndex: &txIn.BlockIndex,
|
|
ScriptSig: &types.ScriptSig{
|
|
Asm: disbuf,
|
|
Hex: hex.EncodeToString(txIn.SignatureScript),
|
|
},
|
|
}
|
|
|
|
// Add the entry to the list now if it already passed the
|
|
// filter since the previous output might not be available.
|
|
passesFilter := len(filterAddrMap) == 0
|
|
if passesFilter {
|
|
vinList = append(vinList, vinEntry)
|
|
}
|
|
|
|
// Only populate previous output information if requested and
|
|
// available.
|
|
if len(originOutputs) == 0 {
|
|
continue
|
|
}
|
|
originTxOut, ok := originOutputs[*prevOut]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Attempt to extract known addresses associated with the script.
|
|
_, addrs := stdscript.ExtractAddrs(originTxOut.Version,
|
|
originTxOut.PkScript, chainParams)
|
|
|
|
// Encode the addresses while checking if the address passes
|
|
// the filter when needed.
|
|
encodedAddrs := make([]string, len(addrs))
|
|
for j, addr := range addrs {
|
|
encodedAddr := addr.String()
|
|
encodedAddrs[j] = encodedAddr
|
|
|
|
// No need to check the map again if the filter already
|
|
// passes.
|
|
if passesFilter {
|
|
continue
|
|
}
|
|
if _, exists := filterAddrMap[encodedAddr]; exists {
|
|
passesFilter = true
|
|
}
|
|
}
|
|
|
|
// Ignore the entry if it doesn't pass the filter.
|
|
if !passesFilter {
|
|
continue
|
|
}
|
|
|
|
// Add entry to the list if it wasn't already done above.
|
|
if len(filterAddrMap) != 0 {
|
|
vinList = append(vinList, vinEntry)
|
|
}
|
|
|
|
// Update the entry with previous output information if
|
|
// requested.
|
|
if vinExtra {
|
|
vinListEntry := &vinList[len(vinList)-1]
|
|
vinListEntry.PrevOut = &types.PrevOut{
|
|
Addresses: encodedAddrs,
|
|
Value: dcrutil.Amount(originTxOut.Value).ToCoin(),
|
|
}
|
|
}
|
|
}
|
|
|
|
return vinList, nil
|
|
}
|
|
|
|
// fetchMempoolTxnsForAddress queries the address index for all unconfirmed
|
|
// transactions that involve the provided address. The results will be limited
|
|
// by the number to skip and the number requested.
|
|
func fetchMempoolTxnsForAddress(s *Server, addr stdaddr.Address, numToSkip, numRequested uint32) ([]*dcrutil.Tx, uint32) {
|
|
// There are no entries to return when there are less available than
|
|
// the number being skipped.
|
|
mpTxns := s.cfg.AddrIndexer.UnconfirmedTxnsForAddress(addr)
|
|
numAvailable := uint32(len(mpTxns))
|
|
if numToSkip > numAvailable {
|
|
return nil, numAvailable
|
|
}
|
|
|
|
// Filter the available entries based on the number to skip and number
|
|
// requested.
|
|
rangeEnd := numToSkip + numRequested
|
|
if rangeEnd > numAvailable {
|
|
rangeEnd = numAvailable
|
|
}
|
|
return mpTxns[numToSkip:rangeEnd], numToSkip
|
|
}
|
|
|
|
// handleSearchRawTransactions implements the searchrawtransactions command.
|
|
func handleSearchRawTransactions(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
// Respond with an error if the address index is not enabled.
|
|
if s.cfg.AddrIndexer == nil {
|
|
return nil, rpcInternalError("Address index must be "+
|
|
"enabled (--addrindex)", "Configuration")
|
|
}
|
|
|
|
// Override the flag for including extra previous output information in
|
|
// each input if needed.
|
|
c := cmd.(*types.SearchRawTransactionsCmd)
|
|
vinExtra := false
|
|
if c.VinExtra != nil {
|
|
vinExtra = *c.VinExtra != 0
|
|
}
|
|
|
|
// Including the extra previous output information requires the
|
|
// transaction index. Currently the address index relies on the
|
|
// transaction index, so this check is redundant, but it's better to be
|
|
// safe in case the address index is ever changed to not rely on it.
|
|
if vinExtra && s.cfg.TxIndexer == nil {
|
|
return nil, rpcInternalError("Transaction index must be "+
|
|
"enabled (--txindex)", "Configuration")
|
|
}
|
|
|
|
// Attempt to decode the supplied address. This also ensures the network
|
|
// encoded with the address matches the network the server is currently on.
|
|
addr, err := stdaddr.DecodeAddress(c.Address, s.cfg.ChainParams)
|
|
if err != nil {
|
|
return nil, rpcAddressKeyError("Could not decode address: %v",
|
|
err)
|
|
}
|
|
|
|
// Override the default number of requested entries if needed. Also,
|
|
// just return now if the number of requested entries is zero to avoid
|
|
// extra work.
|
|
numRequested := 100
|
|
if c.Count != nil {
|
|
numRequested = *c.Count
|
|
const maxCount = 10000
|
|
if numRequested < 0 {
|
|
numRequested = 1
|
|
} else if numRequested > maxCount {
|
|
numRequested = maxCount
|
|
}
|
|
}
|
|
if numRequested == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Override the default number of entries to skip if needed.
|
|
var numToSkip int
|
|
if c.Skip != nil {
|
|
numToSkip = *c.Skip
|
|
if numToSkip < 0 {
|
|
numToSkip = 0
|
|
}
|
|
}
|
|
|
|
// Override the reverse flag if needed.
|
|
var reverse bool
|
|
if c.Reverse != nil {
|
|
reverse = *c.Reverse
|
|
}
|
|
|
|
// Add transactions from mempool first if client asked for reverse
|
|
// order. Otherwise, they will be added last (as needed depending on
|
|
// the requested counts).
|
|
//
|
|
// NOTE: This code doesn't sort by dependency. This might be something
|
|
// to do in the future for the client's convenience, or leave it to the
|
|
// client.
|
|
numSkipped := uint32(0)
|
|
addressTxns := make([]retrievedTx, 0, numRequested)
|
|
if reverse {
|
|
// Transactions in the mempool are not in a block yet, so the block and
|
|
// block index fields in the retrieved transaction struct are left
|
|
// unset.
|
|
mpTxns, mpSkipped := fetchMempoolTxnsForAddress(s, addr,
|
|
uint32(numToSkip), uint32(numRequested))
|
|
numSkipped += mpSkipped
|
|
for _, tx := range mpTxns {
|
|
addressTxns = append(addressTxns, retrievedTx{tx: tx})
|
|
}
|
|
}
|
|
|
|
// Fetch transactions from the database in the desired order if more
|
|
// are needed.
|
|
if len(addressTxns) < numRequested {
|
|
// Ensure the adddr index is synced.
|
|
addrIndex := s.cfg.AddrIndexer
|
|
tHeight, tHash, err := addrIndex.Tip()
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Tip")
|
|
}
|
|
|
|
chain := s.cfg.Chain
|
|
|
|
// Return an out-of-sync error if index is lagging a
|
|
// maximum reorg depth (6) blocks or more from the chain tip.
|
|
if chain.BestSnapshot().Height > (tHeight + 5) {
|
|
msg := fmt.Sprintf("%s: index not synced", addrIndex.Name())
|
|
return nil, rpcInternalError(msg, "Sync")
|
|
}
|
|
|
|
sync:
|
|
for !chain.BestSnapshot().Hash.IsEqual(tHash) {
|
|
select {
|
|
case <-time.After(syncWait):
|
|
msg := fmt.Sprintf("%s: index not synced", addrIndex.Name())
|
|
return nil, rpcInternalError(msg, "Sync")
|
|
case <-addrIndex.WaitForSync():
|
|
break sync
|
|
}
|
|
}
|
|
|
|
err = s.cfg.DB.View(func(dbTx database.Tx) error {
|
|
idxEntries, dbSkipped, err := addrIndex.EntriesForAddress(
|
|
dbTx, addr, uint32(numToSkip)-numSkipped,
|
|
uint32(numRequested-len(addressTxns)), reverse)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
regions := make([]database.BlockRegion, 0, len(idxEntries))
|
|
for i := 0; i < len(idxEntries); i++ {
|
|
entry := &idxEntries[i]
|
|
regions = append(regions, entry.BlockRegion)
|
|
}
|
|
|
|
// Load the raw transaction bytes from the database.
|
|
serializedTxns, err := dbTx.FetchBlockRegions(regions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add the transaction and the hash of the block it is
|
|
// contained in to the list. Note that the transaction
|
|
// is left serialized here since the caller might have
|
|
// requested non-verbose output and hence there would
|
|
// be no point in deserializing it just to reserialize
|
|
// it later.
|
|
for i, serializedTx := range serializedTxns {
|
|
addressTxns = append(addressTxns, retrievedTx{
|
|
txBytes: serializedTx,
|
|
blkHash: regions[i].Hash,
|
|
blkIndex: idxEntries[i].BlockIndex,
|
|
})
|
|
}
|
|
numSkipped += dbSkipped
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
context := "Failed to load address index entries"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
}
|
|
|
|
// Add transactions from mempool last if client did not request reverse
|
|
// order and the number of results is still under the number requested.
|
|
if !reverse && len(addressTxns) < numRequested {
|
|
// Transactions in the mempool are not in a block yet, so the block
|
|
// field in the retrieved transaction struct is left nil.
|
|
mpTxns, mpSkipped := fetchMempoolTxnsForAddress(s, addr,
|
|
uint32(numToSkip)-numSkipped, uint32(numRequested-
|
|
len(addressTxns)))
|
|
numSkipped += mpSkipped
|
|
for _, tx := range mpTxns {
|
|
addressTxns = append(addressTxns, retrievedTx{tx: tx})
|
|
}
|
|
}
|
|
|
|
// Address has never been used if neither source yielded any results.
|
|
if len(addressTxns) == 0 {
|
|
return nil, rpcInternalError("No Txns available", "")
|
|
}
|
|
|
|
// Serialize all of the transactions to hex.
|
|
hexTxns := make([]string, len(addressTxns))
|
|
for i := range addressTxns {
|
|
// Simply encode the raw bytes to hex when the retrieved
|
|
// transaction is already in serialized form.
|
|
rtx := &addressTxns[i]
|
|
if rtx.txBytes != nil {
|
|
hexTxns[i] = hex.EncodeToString(rtx.txBytes)
|
|
continue
|
|
}
|
|
|
|
// Serialize the transaction first and convert to hex when the
|
|
// retrieved transaction is the deserialized structure.
|
|
hexTxns[i], err = s.messageToHex(rtx.tx.MsgTx())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// When not in verbose mode, simply return a list of serialized txns.
|
|
if c.Verbose != nil && *c.Verbose == 0 {
|
|
return hexTxns, nil
|
|
}
|
|
|
|
// Normalize the provided filter addresses (if any) to ensure there are
|
|
// no duplicates.
|
|
filterAddrMap := make(map[string]struct{})
|
|
if c.FilterAddrs != nil && len(*c.FilterAddrs) > 0 {
|
|
for _, addr := range *c.FilterAddrs {
|
|
filterAddrMap[addr] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// The verbose flag is set, so generate the JSON object and return it.
|
|
|
|
// Determine if the treasury rules are active as of the current best tip for
|
|
// transactions in the mempool.
|
|
chain := s.cfg.Chain
|
|
best := chain.BestSnapshot()
|
|
isTreasuryEnabledMempool, err := s.isTreasuryAgendaActive(&best.PrevHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Determine if the automatic ticket revocations agenda is active as of the
|
|
// current best tip for transactions in the mempool.
|
|
isAutoRevocationsEnabledMempool, err :=
|
|
s.isAutoRevocationsAgendaActive(&best.PrevHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chainParams := s.cfg.ChainParams
|
|
srtList := make([]types.SearchRawTransactionsResult, len(addressTxns))
|
|
for i := range addressTxns {
|
|
// The deserialized transaction is needed, so deserialize the
|
|
// retrieved transaction if it's in serialized form (which will
|
|
// be the case when it was lookup up from the database).
|
|
// Otherwise, use the existing deserialized transaction.
|
|
rtx := &addressTxns[i]
|
|
var mtx *wire.MsgTx
|
|
if rtx.tx == nil {
|
|
// Deserialize the transaction.
|
|
mtx = new(wire.MsgTx)
|
|
err := mtx.Deserialize(bytes.NewReader(rtx.txBytes))
|
|
if err != nil {
|
|
context := "Failed to deserialize transaction"
|
|
return nil, rpcInternalError(err.Error(),
|
|
context)
|
|
}
|
|
} else {
|
|
mtx = rtx.tx.MsgTx()
|
|
}
|
|
|
|
// Transactions grabbed from the mempool aren't yet in a block,
|
|
// so conditionally fetch block details here. This will be
|
|
// reflected in the final JSON output (mempool won't have
|
|
// confirmations or block information).
|
|
var blkHeader *wire.BlockHeader
|
|
var blkHashStr string
|
|
var blkHeight int64
|
|
var blkIndex uint32
|
|
isTreasuryEnabled := isTreasuryEnabledMempool
|
|
isAutoRevocationsEnabled := isAutoRevocationsEnabledMempool
|
|
if blkHash := rtx.blkHash; blkHash != nil {
|
|
// Fetch the header from chain.
|
|
header, err := s.cfg.Chain.HeaderByHash(blkHash)
|
|
if err != nil {
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCBlockNotFound,
|
|
Message: "Block not found",
|
|
}
|
|
}
|
|
|
|
// Determine if the treasury rules are active for the block that
|
|
// contains the transaction.
|
|
prevBlkHash := header.PrevBlock
|
|
isTreasuryEnabled, err = s.isTreasuryAgendaActive(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Determine if the automatic ticket revocations agenda is active for the
|
|
// block that contains the transaction.
|
|
isAutoRevocationsEnabled, err =
|
|
s.isAutoRevocationsAgendaActive(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
blkHeader = &header
|
|
blkHashStr = blkHash.String()
|
|
blkHeight = int64(header.Height)
|
|
blkIndex = rtx.blkIndex
|
|
}
|
|
|
|
result := &srtList[i]
|
|
result.Hex = hexTxns[i]
|
|
result.Txid = mtx.TxHash().String()
|
|
result.Vin, err = createVinListPrevOut(s, mtx, s.cfg.ChainParams,
|
|
vinExtra, filterAddrMap, isTreasuryEnabled)
|
|
if err != nil {
|
|
context := "Could not create vin list"
|
|
return nil, rpcInternalError(err.Error(), context)
|
|
}
|
|
result.Vout = createVoutList(mtx, chainParams, filterAddrMap,
|
|
isTreasuryEnabled, isAutoRevocationsEnabled)
|
|
result.Version = int32(mtx.Version)
|
|
result.LockTime = mtx.LockTime
|
|
result.Expiry = mtx.Expiry
|
|
|
|
// Add the block information to the result if there is any.
|
|
if blkHeader != nil {
|
|
// This is not a typo, they are identical in Bitcoin
|
|
// Core as well.
|
|
result.Time = blkHeader.Timestamp.Unix()
|
|
result.Blocktime = blkHeader.Timestamp.Unix()
|
|
result.BlockHash = blkHashStr
|
|
result.BlockHeight = blkHeight
|
|
result.BlockIndex = blkIndex
|
|
result.Confirmations = uint64(1 + best.Height - blkHeight)
|
|
}
|
|
}
|
|
|
|
return srtList, nil
|
|
}
|
|
|
|
// handleSendRawTransaction implements the sendrawtransaction command.
|
|
func handleSendRawTransaction(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.SendRawTransactionCmd)
|
|
// Deserialize and send off to tx relay
|
|
|
|
allowHighFees := *c.AllowHighFees
|
|
hexStr := c.HexTx
|
|
if len(hexStr)%2 != 0 {
|
|
hexStr = "0" + hexStr
|
|
}
|
|
serializedTx, err := hex.DecodeString(hexStr)
|
|
if err != nil {
|
|
return nil, rpcDecodeHexError(hexStr)
|
|
}
|
|
msgtx := wire.NewMsgTx()
|
|
err = msgtx.Deserialize(bytes.NewReader(serializedTx))
|
|
if err != nil {
|
|
return nil, rpcDeserializationError("Could not decode Tx: %v",
|
|
err)
|
|
}
|
|
|
|
// Use 0 for the tag to represent local node.
|
|
tx := dcrutil.NewTx(msgtx)
|
|
acceptedTxs, err := s.cfg.SyncMgr.ProcessTransaction(tx, false,
|
|
false, allowHighFees, 0)
|
|
if err != nil {
|
|
// When the error is a rule error, it means the transaction was
|
|
// simply rejected as opposed to something actually going
|
|
// wrong, so log it as such. Otherwise, something really did
|
|
// go wrong, so log it as an actual error. In both cases, a
|
|
// JSON-RPC error is returned to the client with the
|
|
// deserialization error code (to match bitcoind behavior).
|
|
var rErr mempool.RuleError
|
|
if errors.As(err, &rErr) {
|
|
hash := tx.Hash()
|
|
err = fmt.Errorf("rejected transaction %v: %w", hash,
|
|
err)
|
|
log.Debugf("%v", err)
|
|
|
|
// Use the duplicate tx error code when the transaction
|
|
// is known to already be submitted to the mempool, as
|
|
// well as whenever there is a high certainty that the
|
|
// transaction has been confirmed in a recent block.
|
|
switch {
|
|
case errors.Is(rErr, mempool.ErrDuplicate):
|
|
fallthrough
|
|
case errors.Is(rErr, mempool.ErrAlreadyExists):
|
|
fallthrough
|
|
case s.cfg.SyncMgr.RecentlyConfirmedTxn(hash):
|
|
return nil, rpcDuplicateTxError("%v", err)
|
|
}
|
|
|
|
// return a generic rule error
|
|
return nil, rpcRuleError("%v", err)
|
|
}
|
|
|
|
err = fmt.Errorf("failed to process transaction %v: %w",
|
|
tx.Hash(), err)
|
|
log.Errorf("%v", err)
|
|
return nil, rpcDeserializationError("rejected: %v", err)
|
|
}
|
|
|
|
// Generate and relay inventory vectors for all newly accepted
|
|
// transactions.
|
|
s.cfg.ConnMgr.RelayTransactions(acceptedTxs)
|
|
|
|
// Notify websocket clients of all newly accepted transactions.
|
|
s.NotifyNewTransactions(acceptedTxs)
|
|
|
|
// Determine if the treasury rules are active as of the current best tip.
|
|
prevBlkHash := s.cfg.Chain.BestSnapshot().Hash
|
|
isTreasuryEnabled, err := s.isTreasuryAgendaActive(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Determine if the automatic ticket revocations agenda is active as of the
|
|
// current best tip.
|
|
isAutoRevocationsEnabled, err := s.isAutoRevocationsAgendaActive(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Keep track of all the regular sendrawtransaction request txns so that
|
|
// they can be rebroadcast if they don't make their way into a block.
|
|
//
|
|
// Note that votes are only valid for a specific block and are time
|
|
// sensitive, so they should not be added to the rebroadcast logic.
|
|
txType := stake.DetermineTxType(msgtx, isTreasuryEnabled,
|
|
isAutoRevocationsEnabled)
|
|
if txType != stake.TxTypeSSGen {
|
|
iv := wire.NewInvVect(wire.InvTypeTx, tx.Hash())
|
|
s.cfg.ConnMgr.AddRebroadcastInventory(iv, tx)
|
|
}
|
|
|
|
return tx.Hash().String(), nil
|
|
}
|
|
|
|
// handleSetGenerate implements the setgenerate command.
|
|
func handleSetGenerate(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.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
|
|
genProcLimit := -1
|
|
if c.GenProcLimit != nil {
|
|
genProcLimit = *c.GenProcLimit
|
|
}
|
|
if genProcLimit == 0 {
|
|
generate = false
|
|
}
|
|
|
|
if !generate {
|
|
// Stop CPU mining by setting the number of workers to zero, if needed.
|
|
s.cfg.CPUMiner.SetNumWorkers(0)
|
|
} else {
|
|
// Respond with an error if there are no addresses to pay the
|
|
// created blocks to.
|
|
if len(s.cfg.MiningAddrs) == 0 {
|
|
return nil, rpcInternalError("No payment addresses "+
|
|
"specified via --miningaddr", "Configuration")
|
|
}
|
|
|
|
s.cfg.CPUMiner.SetNumWorkers(int32(genProcLimit))
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// handleStop implements the stop command.
|
|
func handleStop(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
select {
|
|
case s.requestProcessShutdown <- struct{}{}:
|
|
default:
|
|
}
|
|
return "dcrd stopping.", nil
|
|
}
|
|
|
|
// handleSubmitBlock implements the submitblock command.
|
|
func handleSubmitBlock(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.SubmitBlockCmd)
|
|
|
|
// Deserialize the submitted block.
|
|
hexStr := c.HexBlock
|
|
if len(hexStr)%2 != 0 {
|
|
hexStr = "0" + c.HexBlock
|
|
}
|
|
serializedBlock, err := hex.DecodeString(hexStr)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Block decode")
|
|
}
|
|
|
|
block, err := dcrutil.NewBlockFromBytes(serializedBlock)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "Block decode")
|
|
}
|
|
|
|
err = s.cfg.SyncMgr.SubmitBlock(block)
|
|
if err != nil {
|
|
return fmt.Sprintf("rejected: %v", err), nil
|
|
}
|
|
|
|
log.Infof("Accepted block %s via submitblock", block.Hash())
|
|
return nil, nil
|
|
}
|
|
|
|
// min gets the minimum amount from a slice of amounts.
|
|
func min(s []dcrutil.Amount) dcrutil.Amount {
|
|
if len(s) == 0 {
|
|
return 0
|
|
}
|
|
|
|
min := s[0]
|
|
for i := range s {
|
|
if s[i] < min {
|
|
min = s[i]
|
|
}
|
|
}
|
|
|
|
return min
|
|
}
|
|
|
|
// max gets the maximum amount from a slice of amounts.
|
|
func max(s []dcrutil.Amount) dcrutil.Amount {
|
|
max := dcrutil.Amount(0)
|
|
for i := range s {
|
|
if s[i] > max {
|
|
max = s[i]
|
|
}
|
|
}
|
|
|
|
return max
|
|
}
|
|
|
|
// mean gets the mean amount from a slice of amounts.
|
|
func mean(s []dcrutil.Amount) dcrutil.Amount {
|
|
sum := dcrutil.Amount(0)
|
|
for i := range s {
|
|
sum += s[i]
|
|
}
|
|
|
|
if len(s) == 0 {
|
|
return 0
|
|
}
|
|
|
|
return sum / dcrutil.Amount(len(s))
|
|
}
|
|
|
|
// median gets the median amount from a slice of amounts.
|
|
func median(s []dcrutil.Amount) dcrutil.Amount {
|
|
if len(s) == 0 {
|
|
return 0
|
|
}
|
|
|
|
sort.Sort(dcrutil.AmountSorter(s))
|
|
|
|
middle := len(s) / 2
|
|
|
|
if len(s) == 0 {
|
|
return 0
|
|
} else if (len(s) % 2) != 0 {
|
|
return s[middle]
|
|
}
|
|
return (s[middle] + s[middle-1]) / 2
|
|
}
|
|
|
|
// stdDev gets the standard deviation amount from a slice of amounts.
|
|
func stdDev(s []dcrutil.Amount) dcrutil.Amount {
|
|
var total float64
|
|
mean := mean(s)
|
|
|
|
for i := range s {
|
|
total += math.Pow(s[i].ToCoin()-mean.ToCoin(), 2)
|
|
}
|
|
if len(s)-1 == 0 {
|
|
return 0
|
|
}
|
|
v := total / float64(len(s)-1)
|
|
|
|
// Not concerned with an error here, it'll return
|
|
// zero if the amount is too small.
|
|
amt, _ := dcrutil.NewAmount(math.Sqrt(v))
|
|
|
|
return amt
|
|
}
|
|
|
|
// feeInfoForMempool returns the fee information for the passed tx type in the
|
|
// memory pool.
|
|
func feeInfoForMempool(s *Server, txType stake.TxType) *types.FeeInfoMempool {
|
|
txDs := s.cfg.TxMempooler.TxDescs()
|
|
ticketFees := make([]dcrutil.Amount, 0, len(txDs))
|
|
for _, txD := range txDs {
|
|
if txD.Type == txType {
|
|
feePerKb := (dcrutil.Amount(txD.Fee)) * 1000 /
|
|
dcrutil.Amount(txD.Tx.MsgTx().SerializeSize())
|
|
ticketFees = append(ticketFees, feePerKb)
|
|
}
|
|
}
|
|
|
|
return &types.FeeInfoMempool{
|
|
Number: uint32(len(ticketFees)),
|
|
Min: min(ticketFees).ToCoin(),
|
|
Max: max(ticketFees).ToCoin(),
|
|
Mean: mean(ticketFees).ToCoin(),
|
|
Median: median(ticketFees).ToCoin(),
|
|
StdDev: stdDev(ticketFees).ToCoin(),
|
|
}
|
|
}
|
|
|
|
// calcFee calculates the fee of a transaction that has its fraud proofs
|
|
// properly set.
|
|
func calcFeePerKb(tx *dcrutil.Tx) dcrutil.Amount {
|
|
var in dcrutil.Amount
|
|
for _, txIn := range tx.MsgTx().TxIn {
|
|
in += dcrutil.Amount(txIn.ValueIn)
|
|
}
|
|
var out dcrutil.Amount
|
|
for _, txOut := range tx.MsgTx().TxOut {
|
|
out += dcrutil.Amount(txOut.Value)
|
|
}
|
|
|
|
return ((in - out) * 1000) / dcrutil.Amount(tx.MsgTx().SerializeSize())
|
|
}
|
|
|
|
// ticketFeeInfoForBlock fetches the ticket fee information for a given tx type
|
|
// in a block.
|
|
func ticketFeeInfoForBlock(s *Server, height int64, txType stake.TxType) (*types.FeeInfoBlock, error) {
|
|
bl, err := s.cfg.Chain.BlockByHeight(height)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Determine if the treasury rules are active for the block.
|
|
prevBlkHash := bl.MsgBlock().Header.PrevBlock
|
|
isTreasuryEnabled, err := s.isTreasuryAgendaActive(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Determine if the automatic ticket revocations agenda is active for the
|
|
// block.
|
|
isAutoRevocationsEnabled, err := s.isAutoRevocationsAgendaActive(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
txNum := 0
|
|
switch txType {
|
|
case stake.TxTypeRegular:
|
|
txNum = len(bl.MsgBlock().Transactions) - 1
|
|
case stake.TxTypeSStx:
|
|
txNum = int(bl.MsgBlock().Header.FreshStake)
|
|
case stake.TxTypeSSGen:
|
|
txNum = int(bl.MsgBlock().Header.Voters)
|
|
case stake.TxTypeSSRtx:
|
|
txNum = int(bl.MsgBlock().Header.Revocations)
|
|
}
|
|
|
|
txFees := make([]dcrutil.Amount, txNum)
|
|
itr := 0
|
|
if txType == stake.TxTypeRegular {
|
|
for i, tx := range bl.Transactions() {
|
|
// Skip the coin base.
|
|
if i == 0 {
|
|
continue
|
|
}
|
|
|
|
txFees[itr] = calcFeePerKb(tx)
|
|
itr++
|
|
}
|
|
} else {
|
|
for _, stx := range bl.STransactions() {
|
|
thisTxType := stake.DetermineTxType(stx.MsgTx(),
|
|
isTreasuryEnabled, isAutoRevocationsEnabled)
|
|
if thisTxType == txType {
|
|
txFees[itr] = calcFeePerKb(stx)
|
|
itr++
|
|
}
|
|
}
|
|
}
|
|
|
|
return &types.FeeInfoBlock{
|
|
Height: uint32(height),
|
|
Number: uint32(txNum),
|
|
Min: min(txFees).ToCoin(),
|
|
Max: max(txFees).ToCoin(),
|
|
Mean: mean(txFees).ToCoin(),
|
|
Median: median(txFees).ToCoin(),
|
|
StdDev: stdDev(txFees).ToCoin(),
|
|
}, nil
|
|
}
|
|
|
|
// ticketFeeInfoForRange fetches the ticket fee information for a given range
|
|
// from [start, end).
|
|
func ticketFeeInfoForRange(s *Server, start int64, end int64, txType stake.TxType) (*types.FeeInfoWindow, error) {
|
|
chain := s.cfg.Chain
|
|
hashes, err := chain.HeightRange(start, end)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var txFees []dcrutil.Amount
|
|
for i := range hashes {
|
|
bl, err := chain.BlockByHash(&hashes[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if txType == stake.TxTypeRegular {
|
|
for i, tx := range bl.Transactions() {
|
|
// Skip the coin base.
|
|
if i == 0 {
|
|
continue
|
|
}
|
|
|
|
txFees = append(txFees, calcFeePerKb(tx))
|
|
}
|
|
} else {
|
|
// Determine if the treasury rules are active for the block.
|
|
prevBlkHash := bl.MsgBlock().Header.PrevBlock
|
|
isTreasuryEnabled, err := s.isTreasuryAgendaActive(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Determine if the automatic ticket revocations agenda is active for the
|
|
// block.
|
|
isAutoRevocationsEnabled, err :=
|
|
s.isAutoRevocationsAgendaActive(&prevBlkHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, stx := range bl.STransactions() {
|
|
thisTxType := stake.DetermineTxType(stx.MsgTx(),
|
|
isTreasuryEnabled, isAutoRevocationsEnabled)
|
|
if thisTxType == txType {
|
|
txFees = append(txFees, calcFeePerKb(stx))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return &types.FeeInfoWindow{
|
|
StartHeight: uint32(start),
|
|
EndHeight: uint32(end),
|
|
Number: uint32(len(txFees)),
|
|
Min: min(txFees).ToCoin(),
|
|
Max: max(txFees).ToCoin(),
|
|
Mean: mean(txFees).ToCoin(),
|
|
Median: median(txFees).ToCoin(),
|
|
StdDev: stdDev(txFees).ToCoin(),
|
|
}, nil
|
|
}
|
|
|
|
// handleTicketFeeInfo implements the ticketfeeinfo command.
|
|
func handleTicketFeeInfo(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.TicketFeeInfoCmd)
|
|
|
|
bestHeight := s.cfg.Chain.BestSnapshot().Height
|
|
|
|
// Memory pool first.
|
|
feeInfoMempool := feeInfoForMempool(s, stake.TxTypeSStx)
|
|
|
|
// Blocks requested, descending from the chain tip.
|
|
var feeInfoBlocks []types.FeeInfoBlock
|
|
blocks := uint32(0)
|
|
if c.Blocks != nil {
|
|
blocks = *c.Blocks
|
|
}
|
|
if blocks > 0 {
|
|
start := bestHeight
|
|
end := bestHeight - int64(blocks)
|
|
|
|
for i := start; i > end; i-- {
|
|
feeInfo, err := ticketFeeInfoForBlock(s, i, stake.TxTypeSStx)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"Could not obtain ticket fee info")
|
|
}
|
|
feeInfoBlocks = append(feeInfoBlocks, *feeInfo)
|
|
}
|
|
}
|
|
|
|
var feeInfoWindows []types.FeeInfoWindow
|
|
windows := uint32(0)
|
|
if c.Windows != nil {
|
|
windows = *c.Windows
|
|
}
|
|
if windows > 0 {
|
|
// The first window is special because it may not be finished.
|
|
// Perform this first and return if it's the only window the
|
|
// user wants. Otherwise, append and continue.
|
|
winLen := s.cfg.ChainParams.StakeDiffWindowSize
|
|
lastChange := (bestHeight / winLen) * winLen
|
|
|
|
feeInfo, err := ticketFeeInfoForRange(s, lastChange, bestHeight+1,
|
|
stake.TxTypeSStx)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"Could not obtain ticket fee info")
|
|
}
|
|
feeInfoWindows = append(feeInfoWindows, *feeInfo)
|
|
|
|
// We need data on windows from before this. Start from
|
|
// the last adjustment and move backwards through window
|
|
// lengths, calculating the fees data and appending it
|
|
// each time.
|
|
if windows > 1 {
|
|
// Go down to the last height requested, except
|
|
// in the case that the user has specified to
|
|
// many windows. In that case, just proceed to the
|
|
// first block.
|
|
end := int64(-1)
|
|
if lastChange-int64(windows)*winLen > end {
|
|
end = lastChange - int64(windows)*winLen
|
|
}
|
|
for i := lastChange; i > end+winLen; i -= winLen {
|
|
feeInfo, err := ticketFeeInfoForRange(s, i-winLen, i,
|
|
stake.TxTypeSStx)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"Could not obtain ticket fee info")
|
|
}
|
|
feeInfoWindows = append(feeInfoWindows, *feeInfo)
|
|
}
|
|
}
|
|
}
|
|
|
|
return &types.TicketFeeInfoResult{
|
|
FeeInfoMempool: *feeInfoMempool,
|
|
FeeInfoBlocks: feeInfoBlocks,
|
|
FeeInfoWindows: feeInfoWindows,
|
|
}, nil
|
|
}
|
|
|
|
// handleTicketsForAddress implements the ticketsforaddress command.
|
|
func handleTicketsForAddress(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.TicketsForAddressCmd)
|
|
|
|
// Decode the provided address. This also ensures the network encoded
|
|
// with the address matches the network the server is currently on.
|
|
addr, err := stdaddr.DecodeAddress(c.Address, s.cfg.ChainParams)
|
|
if err != nil {
|
|
return nil, rpcInvalidError("Invalid address: %v", err)
|
|
}
|
|
|
|
stakeAddr, ok := addr.(stdaddr.StakeAddress)
|
|
if !ok {
|
|
return nil, rpcInvalidError("Address is not valid for use in the " +
|
|
"staking system")
|
|
}
|
|
|
|
chain := s.cfg.Chain
|
|
tickets, err := chain.TicketsWithAddress(stakeAddr)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(), "could not obtain tickets")
|
|
}
|
|
|
|
ticketStrings := make([]string, len(tickets))
|
|
itr := 0
|
|
for _, ticket := range tickets {
|
|
ticketStrings[itr] = ticket.String()
|
|
itr++
|
|
}
|
|
|
|
reply := &types.TicketsForAddressResult{
|
|
Tickets: ticketStrings,
|
|
}
|
|
return reply, nil
|
|
}
|
|
|
|
// handleTicketVWAP implements the ticketvwap command.
|
|
func handleTicketVWAP(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.TicketVWAPCmd)
|
|
|
|
// The default VWAP is for the past WorkDiffWindows * WorkDiffWindowSize
|
|
// many blocks.
|
|
bestHeight := s.cfg.Chain.BestSnapshot().Height
|
|
var start uint32
|
|
if c.Start == nil {
|
|
params := s.cfg.ChainParams
|
|
toEval := params.WorkDiffWindows * params.WorkDiffWindowSize
|
|
startI64 := bestHeight - toEval
|
|
|
|
// Use 1 as the first block if there aren't enough blocks.
|
|
if startI64 <= 0 {
|
|
start = 1
|
|
} else {
|
|
start = uint32(startI64)
|
|
}
|
|
} else {
|
|
start = *c.Start
|
|
}
|
|
|
|
end := uint32(bestHeight)
|
|
if c.End != nil {
|
|
end = *c.End
|
|
}
|
|
if start > end {
|
|
return nil, rpcInvalidError("Start height %v is beyond end "+
|
|
"height %v", start, end)
|
|
}
|
|
if end > uint32(bestHeight) {
|
|
return nil, rpcInvalidError("End height %v is beyond "+
|
|
"blockchain tip height %v", end, bestHeight)
|
|
}
|
|
|
|
// Calculate the volume weighted average price of a ticket for the
|
|
// given range.
|
|
ticketNum := int64(0)
|
|
totalValue := int64(0)
|
|
for i := start; i <= end; i++ {
|
|
blockHeader, err := s.cfg.Chain.HeaderByHeight(int64(i))
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"Could not obtain header")
|
|
}
|
|
|
|
ticketNum += int64(blockHeader.FreshStake)
|
|
totalValue += blockHeader.SBits * int64(blockHeader.FreshStake)
|
|
}
|
|
vwap := int64(0)
|
|
if ticketNum > 0 {
|
|
vwap = totalValue / ticketNum
|
|
}
|
|
|
|
return dcrutil.Amount(vwap).ToCoin(), nil
|
|
}
|
|
|
|
// handleTxFeeInfo implements the txfeeinfo command.
|
|
func handleTxFeeInfo(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.TxFeeInfoCmd)
|
|
|
|
bestHeight := s.cfg.Chain.BestSnapshot().Height
|
|
|
|
// Memory pool first.
|
|
feeInfoMempool := feeInfoForMempool(s, stake.TxTypeRegular)
|
|
|
|
// Blocks requested, descending from the chain tip.
|
|
var feeInfoBlocks []types.FeeInfoBlock
|
|
blocks := uint32(0)
|
|
if c.Blocks != nil {
|
|
blocks = *c.Blocks
|
|
}
|
|
if blocks > 0 {
|
|
start := bestHeight
|
|
end := bestHeight - int64(blocks)
|
|
|
|
for i := start; i > end; i-- {
|
|
feeInfo, err := ticketFeeInfoForBlock(s, i,
|
|
stake.TxTypeRegular)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"Could not obtain ticket fee info")
|
|
}
|
|
feeInfoBlocks = append(feeInfoBlocks, *feeInfo)
|
|
}
|
|
}
|
|
|
|
// Get the fee info for the range requested, unless none is given. The
|
|
// default range is for the past WorkDiffWindowSize many blocks.
|
|
var feeInfoRange types.FeeInfoRange
|
|
|
|
var start uint32
|
|
if c.RangeStart == nil {
|
|
toEval := s.cfg.ChainParams.WorkDiffWindowSize
|
|
startI64 := bestHeight - toEval
|
|
|
|
// Use 1 as the first block if there aren't enough blocks.
|
|
if startI64 <= 0 {
|
|
start = 1
|
|
} else {
|
|
start = uint32(startI64)
|
|
}
|
|
} else {
|
|
start = *c.RangeStart
|
|
}
|
|
|
|
end := uint32(bestHeight)
|
|
if c.RangeEnd != nil {
|
|
end = *c.RangeEnd
|
|
}
|
|
if start > end {
|
|
return nil, rpcInvalidError("Start height %v is beyond end "+
|
|
"height %v", start, end)
|
|
}
|
|
if end > uint32(bestHeight) {
|
|
return nil, rpcInvalidError("End height %v is beyond "+
|
|
"blockchain tip height %v", end, bestHeight)
|
|
}
|
|
|
|
feeInfo, err := ticketFeeInfoForRange(s, int64(start), int64(end+1),
|
|
stake.TxTypeRegular)
|
|
if err != nil {
|
|
return nil, rpcInternalError(err.Error(),
|
|
"Could not obtain ticket fee info")
|
|
}
|
|
|
|
feeInfoRange = types.FeeInfoRange{
|
|
Number: feeInfo.Number,
|
|
Min: feeInfo.Min,
|
|
Max: feeInfo.Max,
|
|
Mean: feeInfo.Mean,
|
|
Median: feeInfo.Median,
|
|
StdDev: feeInfo.StdDev,
|
|
}
|
|
|
|
return &types.TxFeeInfoResult{
|
|
FeeInfoMempool: *feeInfoMempool,
|
|
FeeInfoBlocks: feeInfoBlocks,
|
|
FeeInfoRange: feeInfoRange,
|
|
}, nil
|
|
}
|
|
|
|
// handleValidateAddress implements the validateaddress command.
|
|
func handleValidateAddress(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.ValidateAddressCmd)
|
|
result := types.ValidateAddressChainResult{}
|
|
addr, err := stdaddr.DecodeAddress(c.Address, s.cfg.ChainParams)
|
|
if err != nil {
|
|
// Return the default value (false) for IsValid.
|
|
return result, nil
|
|
}
|
|
|
|
result.Address = addr.String()
|
|
result.IsValid = true
|
|
return result, nil
|
|
}
|
|
|
|
func verifyChain(_ context.Context, s *Server, level, depth int64) error {
|
|
best := s.cfg.Chain.BestSnapshot()
|
|
finishHeight := best.Height - depth
|
|
if finishHeight < 0 {
|
|
finishHeight = 0
|
|
}
|
|
log.Infof("Verifying chain for %d blocks at level %d",
|
|
best.Height-finishHeight, level)
|
|
|
|
for height := best.Height; height > finishHeight; height-- {
|
|
// Level 0 just looks up the block.
|
|
block, err := s.cfg.Chain.BlockByHeight(height)
|
|
if err != nil {
|
|
log.Errorf("Verify is unable to fetch block at "+
|
|
"height %d: %v", height, err)
|
|
return err
|
|
}
|
|
|
|
// Level 1 does basic chain sanity checks.
|
|
if level > 0 {
|
|
err := s.cfg.SanityChecker.CheckBlockSanity(block)
|
|
if err != nil {
|
|
log.Errorf("Verify is unable to validate "+
|
|
"block at hash %v height %d: %v",
|
|
block.Hash(), height, err)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
log.Infof("Chain verify completed successfully")
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleVerifyChain implements the verifychain command.
|
|
func handleVerifyChain(ctx context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.VerifyChainCmd)
|
|
|
|
var checkLevel, checkDepth int64
|
|
if c.CheckLevel != nil {
|
|
checkLevel = *c.CheckLevel
|
|
}
|
|
if c.CheckDepth != nil {
|
|
checkDepth = *c.CheckDepth
|
|
}
|
|
|
|
err := verifyChain(ctx, s, checkLevel, checkDepth)
|
|
return err == nil, nil
|
|
}
|
|
|
|
// handleVerifyMessage implements the verifymessage command.
|
|
func handleVerifyMessage(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
c := cmd.(*types.VerifyMessageCmd)
|
|
|
|
// Decode the provided address. This also ensures the network encoded with
|
|
// the address matches the network the server is currently on.
|
|
params := s.cfg.ChainParams
|
|
addr, err := stdaddr.DecodeAddress(c.Address, params)
|
|
if err != nil {
|
|
return nil, rpcAddressKeyError("Could not decode address: %v",
|
|
err)
|
|
}
|
|
|
|
// Only version 0 P2PKH addresses are valid for signing.
|
|
if _, ok := addr.(*stdaddr.AddressPubKeyHashEcdsaSecp256k1V0); !ok {
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCType,
|
|
Message: "Address is not a pay-to-pubkey-hash address",
|
|
}
|
|
}
|
|
|
|
// Decode base64 signature.
|
|
sig, err := base64.StdEncoding.DecodeString(c.Signature)
|
|
if err != nil {
|
|
return nil, &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCParse.Code,
|
|
Message: "Malformed base64 encoding: " + err.Error(),
|
|
}
|
|
}
|
|
|
|
// Validate the signature - this just shows that it was valid at all.
|
|
// we will compare it with the key next.
|
|
var buf bytes.Buffer
|
|
wire.WriteVarString(&buf, 0, "Decred Signed Message:\n")
|
|
wire.WriteVarString(&buf, 0, c.Message)
|
|
expectedMessageHash := chainhash.HashB(buf.Bytes())
|
|
pk, wasCompressed, err := ecdsa.RecoverCompact(sig, expectedMessageHash)
|
|
if err != nil {
|
|
// Treat errors in RecoverCompact as an invalid signature.
|
|
return false, nil
|
|
}
|
|
|
|
// Reconstruct the pubkey hash.
|
|
var pkHash []byte
|
|
if wasCompressed {
|
|
pkHash = stdaddr.Hash160(pk.SerializeCompressed())
|
|
} else {
|
|
pkHash = stdaddr.Hash160(pk.SerializeUncompressed())
|
|
}
|
|
address, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(pkHash, params)
|
|
if err != nil {
|
|
// Treat error in reconstruction as an invalid signature.
|
|
return false, nil
|
|
}
|
|
|
|
// Return boolean if addresses match.
|
|
return address.String() == c.Address, nil
|
|
}
|
|
|
|
// handleVersion implements the version command.
|
|
func handleVersion(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
|
|
runtimeVer := strings.Replace(runtime.Version(), ".", "-", -1)
|
|
buildMeta := version.NormalizeString(runtimeVer)
|
|
build := version.NormalizeString(version.BuildMetadata)
|
|
if build != "" {
|
|
buildMeta = fmt.Sprintf("%s.%s", build, buildMeta)
|
|
}
|
|
result := map[string]types.VersionResult{
|
|
"dcrdjsonrpcapi": {
|
|
VersionString: jsonrpcSemverString,
|
|
Major: jsonrpcSemverMajor,
|
|
Minor: jsonrpcSemverMinor,
|
|
Patch: jsonrpcSemverPatch,
|
|
},
|
|
"dcrd": {
|
|
VersionString: version.String(),
|
|
Major: version.Major,
|
|
Minor: version.Minor,
|
|
Patch: version.Patch,
|
|
Prerelease: version.NormalizeString(version.PreRelease),
|
|
BuildMetadata: buildMeta,
|
|
},
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Server provides a concurrent safe RPC server to a chain server.
|
|
type Server struct {
|
|
// atomic
|
|
numClients int32
|
|
|
|
cfg Config
|
|
hmac hash.Hash
|
|
hmacMu sync.Mutex
|
|
authsha [sha256.Size]byte
|
|
limitauthsha [sha256.Size]byte
|
|
ntfnMgr NtfnManager
|
|
statusLines map[int]string
|
|
statusLock sync.RWMutex
|
|
wg sync.WaitGroup
|
|
workState *workState
|
|
helpCacher RPCHelpCacher
|
|
requestProcessShutdown chan struct{}
|
|
}
|
|
|
|
// isTreasuryAgendaActive returns if the treasury agenda is active or not for
|
|
// the block AFTER the provided block hash.
|
|
func (s *Server) isTreasuryAgendaActive(prevBlkHash *chainhash.Hash) (bool, error) {
|
|
chain := s.cfg.Chain
|
|
isTreasuryEnabled, err := chain.IsTreasuryAgendaActive(prevBlkHash)
|
|
if err != nil {
|
|
context := fmt.Sprintf("Could not obtain treasury agenda status for "+
|
|
"block %s", prevBlkHash)
|
|
return false, rpcInternalError(err.Error(), context)
|
|
}
|
|
return isTreasuryEnabled, nil
|
|
}
|
|
|
|
// isAutoRevocationsAgendaActive returns if the automatic ticket revocations
|
|
// agenda is active or not for the block AFTER the provided block hash.
|
|
func (s *Server) isAutoRevocationsAgendaActive(prevBlkHash *chainhash.Hash) (bool, error) {
|
|
chain := s.cfg.Chain
|
|
isAutoRevocationsEnabled, err := chain.IsAutoRevocationsAgendaActive(prevBlkHash)
|
|
if err != nil {
|
|
context := fmt.Sprintf("Could not obtain automatic ticket revocations "+
|
|
"agenda status for block %s", prevBlkHash)
|
|
return false, rpcInternalError(err.Error(), context)
|
|
}
|
|
return isAutoRevocationsEnabled, nil
|
|
}
|
|
|
|
// isSubsidySplitAgendaActive returns if the modified subsidy split agenda is
|
|
// active or not for the block AFTER the provided block hash.
|
|
func (s *Server) isSubsidySplitAgendaActive(prevBlkHash *chainhash.Hash) (bool, error) {
|
|
chain := s.cfg.Chain
|
|
isSubsidySplitEnabled, err := chain.IsSubsidySplitAgendaActive(prevBlkHash)
|
|
if err != nil {
|
|
context := fmt.Sprintf("Could not obtain modified subsidy split "+
|
|
"agenda status for block %s", prevBlkHash)
|
|
return false, rpcInternalError(err.Error(), context)
|
|
}
|
|
return isSubsidySplitEnabled, nil
|
|
}
|
|
|
|
// httpStatusLine returns a response Status-Line (RFC 2616 Section 6.1) for the
|
|
// given request and response status code. This function was lifted and
|
|
// adapted from the standard library HTTP server code since it's not exported.
|
|
func (s *Server) httpStatusLine(req *http.Request, code int) string {
|
|
// Fast path:
|
|
key := code
|
|
proto11 := req.ProtoAtLeast(1, 1)
|
|
if !proto11 {
|
|
key = -key
|
|
}
|
|
s.statusLock.RLock()
|
|
line, ok := s.statusLines[key]
|
|
s.statusLock.RUnlock()
|
|
if ok {
|
|
return line
|
|
}
|
|
|
|
// Slow path:
|
|
proto := "HTTP/1.0"
|
|
if proto11 {
|
|
proto = "HTTP/1.1"
|
|
}
|
|
codeStr := strconv.Itoa(code)
|
|
text := http.StatusText(code)
|
|
if text != "" {
|
|
line = proto + " " + codeStr + " " + text + "\r\n"
|
|
s.statusLock.Lock()
|
|
s.statusLines[key] = line
|
|
s.statusLock.Unlock()
|
|
} else {
|
|
text = "status code " + codeStr
|
|
line = proto + " " + codeStr + " " + text + "\r\n"
|
|
}
|
|
|
|
return line
|
|
}
|
|
|
|
// writeHTTPResponseHeaders writes the necessary response headers prior to
|
|
// writing an HTTP body given a request to use for protocol negotiation,
|
|
// headers to write, a status code, and a writer.
|
|
func (s *Server) writeHTTPResponseHeaders(req *http.Request, headers http.Header, code int, w io.Writer) error {
|
|
_, err := io.WriteString(w, s.httpStatusLine(req, code))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = headers.Write(w)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = io.WriteString(w, "\r\n")
|
|
return err
|
|
}
|
|
|
|
// shutdown terminates the processes of the rpc server.
|
|
func (s *Server) shutdown() error {
|
|
log.Warnf("RPC server shutting down")
|
|
for _, listener := range s.cfg.Listeners {
|
|
err := listener.Close()
|
|
if err != nil {
|
|
log.Errorf("Problem shutting down rpc: %v", err)
|
|
return err
|
|
}
|
|
}
|
|
s.wg.Wait()
|
|
log.Infof("RPC server shutdown complete")
|
|
return nil
|
|
}
|
|
|
|
// RequestedProcessShutdown returns a channel that is sent to when an
|
|
// authorized RPC client requests the process to shutdown. If the request can
|
|
// not be read immediately, it is dropped.
|
|
func (s *Server) RequestedProcessShutdown() <-chan struct{} {
|
|
return s.requestProcessShutdown
|
|
}
|
|
|
|
// NotifyNewTransactions notifies both websocket and getblocktemplate long
|
|
// poll clients of the passed transactions. This function should be called
|
|
// whenever new transactions are added to the mempool.
|
|
func (s *Server) NotifyNewTransactions(txns []*dcrutil.Tx) {
|
|
for _, tx := range txns {
|
|
// Notify websocket clients about mempool transactions.
|
|
s.ntfnMgr.NotifyMempoolTx(tx, true)
|
|
}
|
|
}
|
|
|
|
// NotifyTSpend notifies websocket clients that have registered to receive new
|
|
// tspends in the mempool.
|
|
func (s *Server) NotifyTSpend(tx *dcrutil.Tx) {
|
|
s.ntfnMgr.NotifyTSpend(tx)
|
|
}
|
|
|
|
// NotifyNewTickets notifies websocket clients that have registered for maturing
|
|
// ticket updates.
|
|
func (s *Server) NotifyNewTickets(tnd *blockchain.TicketNotificationsData) {
|
|
s.ntfnMgr.NotifyNewTickets(tnd)
|
|
}
|
|
|
|
// NotifyBlockConnected notifies websocket clients that have registered for
|
|
// block updates when a block is connected to the main chain.
|
|
func (s *Server) NotifyBlockConnected(block *dcrutil.Block) {
|
|
s.ntfnMgr.NotifyBlockConnected(block)
|
|
}
|
|
|
|
// NotifySpentAndMissedTickets notifies websocket clients that have registered
|
|
// for spent and missed ticket updates.
|
|
func (s *Server) NotifySpentAndMissedTickets(tnd *blockchain.TicketNotificationsData) {
|
|
s.ntfnMgr.NotifySpentAndMissedTickets(tnd)
|
|
}
|
|
|
|
// NotifyBlockDisconnected notifies websocket clients that have registered for
|
|
// block updates when a block is disconnected from the main chain.
|
|
func (s *Server) NotifyBlockDisconnected(block *dcrutil.Block) {
|
|
s.ntfnMgr.NotifyBlockDisconnected(block)
|
|
}
|
|
|
|
// NotifyReorganization notifies websocket clients that have registered for
|
|
// block updates when the blockchain is beginning a reorganization.
|
|
func (s *Server) NotifyReorganization(rd *blockchain.ReorganizationNtfnsData) {
|
|
s.ntfnMgr.NotifyReorganization(rd)
|
|
}
|
|
|
|
// NotifyWinningTickets notifies websocket clients that have registered for
|
|
// winning ticket updates.
|
|
func (s *Server) NotifyWinningTickets(wtnd *WinningTicketsNtfnData) {
|
|
s.ntfnMgr.NotifyWinningTickets(wtnd)
|
|
}
|
|
|
|
// limitConnections responds with a 503 service unavailable and returns true if
|
|
// adding another client would exceed the maximum allow RPC clients.
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func (s *Server) limitConnections(w http.ResponseWriter, remoteAddr string) bool {
|
|
if int(atomic.LoadInt32(&s.numClients)+1) > s.cfg.RPCMaxClients {
|
|
log.Infof("Max RPC clients exceeded [%d] - "+
|
|
"disconnecting client %s", s.cfg.RPCMaxClients,
|
|
remoteAddr)
|
|
http.Error(w, "503 Too busy. Try again later.",
|
|
http.StatusServiceUnavailable)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// incrementClients adds one to the number of connected RPC clients. Note this
|
|
// only applies to standard clients. Websocket clients have their own limits
|
|
// and are tracked separately.
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func (s *Server) incrementClients() {
|
|
atomic.AddInt32(&s.numClients, 1)
|
|
}
|
|
|
|
// decrementClients subtracts one from the number of connected RPC clients.
|
|
// Note this only applies to standard clients. Websocket clients have their
|
|
// own limits and are tracked separately.
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func (s *Server) decrementClients() {
|
|
atomic.AddInt32(&s.numClients, -1)
|
|
}
|
|
|
|
// authMAC calculates the MAC (currently HMAC-SHA256) of an Authorization
|
|
// header, keyed with a random key created during server creation. The MAC is
|
|
// appended to dst, and the appended slice is returned.
|
|
func (s *Server) authMAC(dst, auth []byte) []byte {
|
|
s.hmacMu.Lock()
|
|
s.hmac.Reset()
|
|
s.hmac.Write(auth)
|
|
dst = s.hmac.Sum(dst)
|
|
s.hmacMu.Unlock()
|
|
return dst
|
|
}
|
|
|
|
// checkAuth checks the HTTP Basic authentication supplied by a wallet or RPC
|
|
// client in the HTTP request r. If the supplied authentication does not match
|
|
// the username and password expected, a non-nil error is returned.
|
|
//
|
|
// This check is time-constant.
|
|
//
|
|
// The first bool return value signifies auth success (true if successful) and
|
|
// the second bool return value specifies whether the user can change the state
|
|
// of the server (true) or whether the user is limited (false). The second is
|
|
// always false if the first is.
|
|
func (s *Server) checkAuth(r *http.Request, require bool) (bool, bool, error) {
|
|
// If admin-level RPC user and pass options are not set, this always
|
|
// succeeds. This will be the case when TLS client certificates are
|
|
// being used for authentication.
|
|
if s.authsha == ([32]byte{}) {
|
|
return true, true, nil
|
|
}
|
|
|
|
authhdr := r.Header["Authorization"]
|
|
if len(authhdr) == 0 {
|
|
if require {
|
|
log.Warnf("RPC authentication failure from %s",
|
|
r.RemoteAddr)
|
|
return false, false, errors.New("auth failure")
|
|
}
|
|
|
|
return false, false, nil
|
|
}
|
|
|
|
mac := make([]byte, 0, sha256.Size)
|
|
mac = s.authMAC(mac, []byte(authhdr[0]))
|
|
|
|
cmp := subtle.ConstantTimeCompare(mac, s.authsha[:])
|
|
limitcmp := subtle.ConstantTimeCompare(mac, s.limitauthsha[:])
|
|
if cmp|limitcmp == 0 {
|
|
// Request's auth doesn't match either user
|
|
log.Warnf("RPC authentication failure from %s", r.RemoteAddr)
|
|
return false, false, errors.New("auth failure")
|
|
}
|
|
|
|
isAdmin := cmp == 1
|
|
return true, isAdmin, nil
|
|
}
|
|
|
|
// 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 {
|
|
jsonrpc string
|
|
id interface{}
|
|
method types.Method
|
|
params interface{}
|
|
err *dcrjson.RPCError
|
|
}
|
|
|
|
// standardCmdResult checks that a parsed command is a standard 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 *Server) standardCmdResult(ctx context.Context, cmd *parsedRPCCmd) (interface{}, error) {
|
|
handler, ok := rpcHandlers[cmd.method]
|
|
if !ok {
|
|
return nil, dcrjson.ErrRPCMethodNotFound
|
|
}
|
|
|
|
return handler(ctx, s, cmd.params)
|
|
}
|
|
|
|
// 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 *dcrjson.Request) *parsedRPCCmd {
|
|
parsedCmd := parsedRPCCmd{
|
|
jsonrpc: request.Jsonrpc,
|
|
id: request.ID,
|
|
method: types.Method(request.Method),
|
|
}
|
|
|
|
params, err := dcrjson.ParseParams(types.Method(request.Method), request.Params)
|
|
if err != nil {
|
|
// Produce a relevant error when the requested method is not registered
|
|
// depending on whether or not it is recognized as being a wallet
|
|
// command, recognized as unimplemented, or completely unrecognized.
|
|
if errors.Is(err, dcrjson.ErrUnregisteredMethod) {
|
|
parsedCmd.err = dcrjson.ErrRPCMethodNotFound
|
|
if _, ok := rpcAskWallet[request.Method]; ok {
|
|
parsedCmd.err = ErrRPCNoWallet
|
|
} else if _, ok := rpcUnimplemented[request.Method]; ok {
|
|
parsedCmd.err = ErrRPCUnimplemented
|
|
}
|
|
|
|
return &parsedCmd
|
|
}
|
|
|
|
// Otherwise, some type of invalid parameters is the cause, so
|
|
// produce the equivalent RPC error.
|
|
parsedCmd.err = rpcInvalidError("Failed to parse request: %v", err)
|
|
return &parsedCmd
|
|
}
|
|
|
|
parsedCmd.params = params
|
|
return &parsedCmd
|
|
}
|
|
|
|
// createMarshalledReply returns a new marshalled JSON-RPC response given the
|
|
// passed parameters. It will automatically convert errors that are not of the
|
|
// type *dcrjson.RPCError to the appropriate type as needed.
|
|
func createMarshalledReply(rpcVersion string, id interface{}, result interface{}, replyErr error) ([]byte, error) {
|
|
var jsonErr *dcrjson.RPCError
|
|
if replyErr != nil && !errors.As(replyErr, &jsonErr) {
|
|
jsonErr = rpcInternalError(replyErr.Error(), "")
|
|
}
|
|
|
|
return dcrjson.MarshalResponse(rpcVersion, id, result, jsonErr)
|
|
}
|
|
|
|
// processRequest determines the incoming request type (single or batched),
|
|
// parses it and returns a marshalled response.
|
|
func (s *Server) processRequest(ctx context.Context, request *dcrjson.Request, isAdmin bool) []byte {
|
|
var result interface{}
|
|
var jsonErr error
|
|
|
|
if !isAdmin {
|
|
if _, ok := rpcLimited[request.Method]; !ok {
|
|
jsonErr = rpcInvalidError("limited user not " +
|
|
"authorized for this method")
|
|
}
|
|
}
|
|
|
|
if jsonErr == nil {
|
|
if request.Method == "" {
|
|
jsonErr = &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCInvalidRequest.Code,
|
|
Message: "Invalid request: malformed",
|
|
}
|
|
msg, err := createMarshalledReply(request.Jsonrpc, request.ID, result, jsonErr)
|
|
if err != nil {
|
|
log.Errorf("Failed to marshal reply: %v", err)
|
|
return nil
|
|
}
|
|
return msg
|
|
}
|
|
|
|
// Valid requests with no ID (notifications) must not have a response
|
|
// per the JSON-RPC spec.
|
|
if request.ID == nil {
|
|
return nil
|
|
}
|
|
|
|
// Attempt to parse the JSON-RPC request into a known
|
|
// concrete command.
|
|
parsedCmd := parseCmd(request)
|
|
if parsedCmd.err != nil {
|
|
jsonErr = parsedCmd.err
|
|
} else {
|
|
result, jsonErr = s.standardCmdResult(ctx, parsedCmd)
|
|
}
|
|
}
|
|
|
|
// Marshal the response.
|
|
msg, err := createMarshalledReply(request.Jsonrpc, request.ID, result, jsonErr)
|
|
if err != nil {
|
|
log.Errorf("Failed to marshal reply: %v", err)
|
|
return nil
|
|
}
|
|
return msg
|
|
}
|
|
|
|
// jsonRPCRead handles reading and responding to RPC messages.
|
|
func (s *Server) jsonRPCRead(sCtx context.Context, w http.ResponseWriter, r *http.Request, isAdmin bool) {
|
|
select {
|
|
case <-sCtx.Done():
|
|
return
|
|
default:
|
|
}
|
|
|
|
// Read and close the JSON-RPC request body from the caller.
|
|
bodyReader := io.LimitReader(r.Body, rpcReadLimitAuthenticated)
|
|
body, err := io.ReadAll(bodyReader)
|
|
r.Body.Close()
|
|
if err != nil {
|
|
errMsg := fmt.Sprintf("error reading JSON message: %v", err)
|
|
errCode := http.StatusBadRequest
|
|
http.Error(w, strconv.Itoa(errCode)+" "+errMsg,
|
|
errCode)
|
|
return
|
|
}
|
|
|
|
// Unfortunately, the http server doesn't provide the ability to change
|
|
// the read deadline for the new connection and having one breaks long
|
|
// polling. However, not having a read deadline on the initial
|
|
// connection would mean clients can connect and idle forever. Thus,
|
|
// hijack the connection from the HTTP server, clear the read deadline,
|
|
// and handle writing the response manually.
|
|
hj, ok := w.(http.Hijacker)
|
|
if !ok {
|
|
errMsg := "webserver doesn't support hijacking"
|
|
log.Warnf(errMsg)
|
|
errCode := http.StatusInternalServerError
|
|
http.Error(w, strconv.Itoa(errCode)+" "+errMsg,
|
|
errCode)
|
|
return
|
|
}
|
|
|
|
conn, buf, err := hj.Hijack()
|
|
if err != nil {
|
|
log.Warnf("Failed to hijack HTTP connection: %v", err)
|
|
errCode := http.StatusInternalServerError
|
|
http.Error(w, strconv.Itoa(errCode)+" "+
|
|
err.Error(), errCode)
|
|
return
|
|
}
|
|
|
|
defer conn.Close()
|
|
defer buf.Flush()
|
|
conn.SetReadDeadline(timeZeroVal)
|
|
// Setup a close notifier. Since the connection is hijacked,
|
|
// the CloseNotifier on the ResponseWriter is not available.
|
|
ctx, cancel := context.WithCancel(sCtx)
|
|
defer cancel()
|
|
|
|
go func() {
|
|
_, err := conn.Read(make([]byte, 1))
|
|
if err != nil {
|
|
cancel()
|
|
}
|
|
}()
|
|
|
|
var results []json.RawMessage
|
|
var batchSize int
|
|
var batchedRequest bool
|
|
|
|
// Determine request type
|
|
if bytes.HasPrefix(body, batchedRequestPrefix) {
|
|
batchedRequest = true
|
|
}
|
|
|
|
// Process a single request
|
|
if !batchedRequest {
|
|
var req dcrjson.Request
|
|
var resp json.RawMessage
|
|
err = json.Unmarshal(body, &req)
|
|
if err != nil {
|
|
jsonErr := &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCParse.Code,
|
|
Message: fmt.Sprintf("Failed to parse request: %v", err),
|
|
}
|
|
resp, err = dcrjson.MarshalResponse("1.0", nil, nil, jsonErr)
|
|
if err != nil {
|
|
log.Errorf("Failed to create reply: %v", err)
|
|
}
|
|
} else {
|
|
resp = s.processRequest(ctx, &req, isAdmin)
|
|
}
|
|
|
|
if resp != nil {
|
|
results = append(results, resp)
|
|
}
|
|
}
|
|
|
|
// Process a batched request
|
|
if batchedRequest {
|
|
var batchedRequests []json.RawMessage
|
|
var resp json.RawMessage
|
|
err = json.Unmarshal(body, &batchedRequests)
|
|
if err != nil {
|
|
jsonErr := &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCParse.Code,
|
|
Message: fmt.Sprintf("Failed to parse request: %v", err),
|
|
}
|
|
resp, err = dcrjson.MarshalResponse("2.0", nil, nil, jsonErr)
|
|
if err != nil {
|
|
log.Errorf("Failed to create reply: %v", err)
|
|
}
|
|
|
|
if resp != nil {
|
|
results = append(results, resp)
|
|
}
|
|
}
|
|
|
|
if err == nil {
|
|
// Response with an empty batch error if the batch size is zero
|
|
if len(batchedRequests) == 0 {
|
|
jsonErr := &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCInvalidRequest.Code,
|
|
Message: "Invalid request: empty batch",
|
|
}
|
|
resp, err = dcrjson.MarshalResponse("2.0", nil, nil, jsonErr)
|
|
if err != nil {
|
|
log.Errorf("Failed to marshal reply: %v", err)
|
|
}
|
|
|
|
if resp != nil {
|
|
results = append(results, resp)
|
|
}
|
|
}
|
|
|
|
// Process each batch entry individually
|
|
if len(batchedRequests) > 0 {
|
|
batchSize = len(batchedRequests)
|
|
|
|
for _, entry := range batchedRequests {
|
|
var req dcrjson.Request
|
|
err := json.Unmarshal(entry, &req)
|
|
if err != nil {
|
|
jsonErr := &dcrjson.RPCError{
|
|
Code: dcrjson.ErrRPCInvalidRequest.Code,
|
|
Message: fmt.Sprintf("Invalid request: %v",
|
|
err),
|
|
}
|
|
resp, err = dcrjson.MarshalResponse("", nil, nil, jsonErr)
|
|
if err != nil {
|
|
log.Errorf("Failed to create reply: %v", err)
|
|
}
|
|
|
|
if resp != nil {
|
|
results = append(results, resp)
|
|
}
|
|
continue
|
|
}
|
|
|
|
resp = s.processRequest(ctx, &req, isAdmin)
|
|
if resp != nil {
|
|
results = append(results, resp)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var msg = []byte{}
|
|
if batchedRequest && batchSize > 0 {
|
|
if len(results) > 0 {
|
|
// Form the batched response json
|
|
var buffer bytes.Buffer
|
|
buffer.WriteByte('[')
|
|
for idx, reply := range results {
|
|
if idx == len(results)-1 {
|
|
buffer.Write(reply)
|
|
buffer.WriteByte(']')
|
|
break
|
|
}
|
|
buffer.Write(reply)
|
|
buffer.WriteByte(',')
|
|
}
|
|
msg = buffer.Bytes()
|
|
}
|
|
}
|
|
|
|
if !batchedRequest || batchSize == 0 {
|
|
// Respond with the first results entry for single requests
|
|
if len(results) > 0 {
|
|
msg = results[0]
|
|
}
|
|
}
|
|
|
|
// Write the response.
|
|
err = s.writeHTTPResponseHeaders(r, w.Header(), http.StatusOK, buf)
|
|
if err != nil {
|
|
log.Error(err)
|
|
return
|
|
}
|
|
if _, err := buf.Write(msg); err != nil {
|
|
log.Errorf("Failed to write marshalled reply: %v", err)
|
|
}
|
|
|
|
// Terminate with newline to maintain compatibility with Bitcoin Core.
|
|
if err := buf.WriteByte('\n'); err != nil {
|
|
log.Errorf("Failed to append terminating newline to reply: %v", err)
|
|
}
|
|
}
|
|
|
|
// jsonAuthFail sends a message back to the client if the http auth is rejected.
|
|
func jsonAuthFail(w http.ResponseWriter) {
|
|
w.Header().Add("WWW-Authenticate", `Basic realm="dcrd RPC"`)
|
|
http.Error(w, "401 Unauthorized.", http.StatusUnauthorized)
|
|
}
|
|
|
|
// logForwarder provides logic to forward log messages writing to an io.Writer
|
|
// to the rpcserver logger.
|
|
type logForwarder struct{}
|
|
|
|
// Write implements the io.Writer interface and forwards the message to the
|
|
// active rpcserver logger.
|
|
func (logForwarder) Write(p []byte) (int, error) {
|
|
log.Error(strings.TrimRight(string(p), "\r\n"))
|
|
return len(p), nil
|
|
}
|
|
|
|
// equalASCIIFold returns true if s is equal to t with ASCII case folding as
|
|
// defined in RFC 4790. This function was lifted and from the gorilla websocket
|
|
// code since it's not exported.
|
|
func equalASCIIFold(s, t string) bool {
|
|
for s != "" && t != "" {
|
|
sr, size := utf8.DecodeRuneInString(s)
|
|
s = s[size:]
|
|
tr, size := utf8.DecodeRuneInString(t)
|
|
t = t[size:]
|
|
if sr == tr {
|
|
continue
|
|
}
|
|
if 'A' <= sr && sr <= 'Z' {
|
|
sr = sr + 'a' - 'A'
|
|
}
|
|
if 'A' <= tr && tr <= 'Z' {
|
|
tr = tr + 'a' - 'A'
|
|
}
|
|
if sr != tr {
|
|
return false
|
|
}
|
|
}
|
|
return s == t
|
|
}
|
|
|
|
// route sets up the endpoints of the rpc server.
|
|
func (s *Server) route(ctx context.Context) *http.Server {
|
|
rpcServeMux := http.NewServeMux()
|
|
httpServer := &http.Server{
|
|
Handler: rpcServeMux,
|
|
|
|
// Timeout connections which don't complete the initial
|
|
// handshake within the allowed timeframe.
|
|
ReadTimeout: time.Second * rpcAuthTimeoutSeconds,
|
|
|
|
// Reroute http server error logging through the rpcserver
|
|
// logger.
|
|
ErrorLog: stdlog.New(logForwarder{}, "", 0),
|
|
}
|
|
rpcServeMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Connection", "close")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
r.Close = true
|
|
|
|
// Limit the number of connections to max allowed.
|
|
if s.limitConnections(w, r.RemoteAddr) {
|
|
return
|
|
}
|
|
|
|
// Keep track of the number of connected clients.
|
|
s.incrementClients()
|
|
defer s.decrementClients()
|
|
_, isAdmin, err := s.checkAuth(r, true)
|
|
if err != nil {
|
|
jsonAuthFail(w)
|
|
return
|
|
}
|
|
|
|
// Read and respond to the request.
|
|
s.jsonRPCRead(ctx, w, r, isAdmin)
|
|
})
|
|
|
|
// Websocket endpoint.
|
|
rpcServeMux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
|
authenticated, isAdmin, err := s.checkAuth(r, false)
|
|
if err != nil {
|
|
jsonAuthFail(w)
|
|
return
|
|
}
|
|
|
|
// Attempt to upgrade the connection to a websocket connection using the
|
|
// default size for read/write buffers and impose a read limit that
|
|
// depends on whether or not the connection is authenticated yet.
|
|
upgrader := websocket.Upgrader{
|
|
CheckOrigin: func(r *http.Request) bool {
|
|
// Allow requests with no origin header set.
|
|
origin := r.Header["Origin"]
|
|
if len(origin) == 0 {
|
|
return true
|
|
}
|
|
|
|
// Reject requests with origin headers that are not valid URLs.
|
|
originURL, err := url.Parse(origin[0])
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Allow local resources on browsers that set the origin header
|
|
// for them. In particular:
|
|
// - Firefox which sets it to "null"
|
|
// - Chrome which sets it to "file://"
|
|
// - Edge which sets it to "file://"
|
|
if originURL.Scheme == "file" || originURL.Path == "null" {
|
|
return true
|
|
}
|
|
|
|
// Strip the port from both the origin and request hosts.
|
|
originHost := originURL.Host
|
|
requestHost := r.Host
|
|
if host, _, err := net.SplitHostPort(originHost); err != nil {
|
|
originHost = host
|
|
}
|
|
if host, _, err := net.SplitHostPort(requestHost); err != nil {
|
|
requestHost = host
|
|
}
|
|
|
|
// Reject mismatched hosts.
|
|
return equalASCIIFold(originHost, requestHost)
|
|
},
|
|
}
|
|
ws, err := upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
var herr websocket.HandshakeError
|
|
if !errors.As(err, &herr) {
|
|
log.Errorf("Unexpected websocket error: %v", err)
|
|
}
|
|
return
|
|
}
|
|
ws.SetPingHandler(func(payload string) error {
|
|
log.Debugf("ping received: len %d", len(payload))
|
|
log.Tracef("ping payload: %q", payload)
|
|
var netErr net.Error
|
|
err := ws.WriteControl(websocket.PongMessage, []byte(payload),
|
|
time.Now().Add(websocketPongTimeout))
|
|
if err != nil && !errors.Is(err, websocket.ErrCloseSent) &&
|
|
!(errors.As(err, &netErr) && netErr.Timeout()) {
|
|
|
|
log.Errorf("Failed to send pong: %v", err)
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
ws.SetPongHandler(func(payload string) error {
|
|
log.Debugf("pong received: len %d", len(payload))
|
|
log.Tracef("pong payload: %q", payload)
|
|
return nil
|
|
})
|
|
if !authenticated {
|
|
ws.SetReadLimit(websocketReadLimitUnauthenticated)
|
|
} else {
|
|
ws.SetReadLimit(websocketReadLimitAuthenticated)
|
|
}
|
|
s.WebsocketHandler(ws, r.RemoteAddr, authenticated, isAdmin)
|
|
})
|
|
return httpServer
|
|
}
|
|
|
|
// Run starts the rpc server and its listeners. It blocks until the
|
|
// provided context is cancelled.
|
|
func (s *Server) Run(ctx context.Context) {
|
|
log.Trace("Starting RPC server")
|
|
server := s.route(ctx)
|
|
for _, listener := range s.cfg.Listeners {
|
|
s.wg.Add(1)
|
|
go func(listener net.Listener) {
|
|
log.Infof("RPC server listening on %s", listener.Addr())
|
|
server.Serve(listener)
|
|
log.Tracef("RPC listener done for %s", listener.Addr())
|
|
s.wg.Done()
|
|
}(listener)
|
|
}
|
|
|
|
// Subscribe for async work notifications when background template
|
|
// generation is enabled.
|
|
if len(s.cfg.MiningAddrs) > 0 && s.cfg.BlockTemplater != nil {
|
|
s.wg.Add(1)
|
|
go func(s *Server, ctx context.Context) {
|
|
templateSub := s.cfg.BlockTemplater.Subscribe()
|
|
defer templateSub.Stop()
|
|
|
|
for {
|
|
select {
|
|
case templateNtfn := <-templateSub.C():
|
|
s.ntfnMgr.NotifyWork(templateNtfn)
|
|
|
|
case <-ctx.Done():
|
|
s.wg.Done()
|
|
return
|
|
}
|
|
}
|
|
}(s, ctx)
|
|
}
|
|
|
|
s.ntfnMgr.Run(ctx)
|
|
err := s.shutdown()
|
|
if err != nil {
|
|
log.Error(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Config is a descriptor containing the RPC server configuration.
|
|
type Config struct {
|
|
// Listeners defines a slice of listeners for which the RPC server will
|
|
// take ownership of and accept connections. Since the RPC server takes
|
|
// ownership of these listeners, they will be closed when the RPC server
|
|
// is stopped.
|
|
Listeners []net.Listener
|
|
|
|
// StartupTime is the unix timestamp for when the server that is hosting
|
|
// the RPC server started.
|
|
StartupTime int64
|
|
|
|
// ConnMgr defines the connection manager for the RPC server to use. It
|
|
// provides the RPC server with a means to do things such as add,
|
|
// remove, connect, disconnect, and query peers as well as other
|
|
// connection-related data and tasks.
|
|
ConnMgr ConnManager
|
|
|
|
// SyncMgr defines the sync manager for the RPC server to use.
|
|
SyncMgr SyncManager
|
|
|
|
// ExistsAddresser defines the exist addresser for the RPC server to
|
|
// use.
|
|
ExistsAddresser ExistsAddresser
|
|
|
|
// These fields allow the RPC server to interface with the local block
|
|
// chain data and state.
|
|
TimeSource blockchain.MedianTimeSource
|
|
Chain Chain
|
|
SanityChecker SanityChecker
|
|
ChainParams *chaincfg.Params
|
|
DB database.DB
|
|
FeeEstimator FeeEstimator
|
|
Services wire.ServiceFlag
|
|
|
|
// SubsidyCache defines a cache for efficient access to consensus-critical
|
|
// subsidy calculations.
|
|
SubsidyCache *standalone.SubsidyCache
|
|
|
|
// AddrManager defines a concurrency safe address manager for caching
|
|
// potential peers on the network.
|
|
AddrManager AddrManager
|
|
|
|
// Clock defines the clock for the RPC server to use.
|
|
Clock Clock
|
|
|
|
// TxMempooler defines the transaction memory pool to interact with.
|
|
TxMempooler TxMempooler
|
|
|
|
// These fields allow the RPC server to interface with mining.
|
|
//
|
|
// BlockTemplater generates block templates, CPUMiner solves
|
|
// templates using the CPU. CPU mining is typically only useful
|
|
// for test purposes when doing regression or simulation testing.
|
|
BlockTemplater BlockTemplater
|
|
CPUMiner CPUMiner
|
|
|
|
// TxIndexer defines the optional transaction indexer for the RPC server to
|
|
// use.
|
|
TxIndexer TxIndexer
|
|
|
|
// AddrIndexer defines the optional address indexer for the RPC server to use.
|
|
AddrIndexer AddrIndexer
|
|
|
|
// NetInfo defines a slice of the available networks.
|
|
NetInfo []types.NetworksResult
|
|
|
|
// MinRelayTxFee defines the minimum transaction fee in Atoms/1000 bytes to be
|
|
// considered a non-zero fee.
|
|
MinRelayTxFee dcrutil.Amount
|
|
|
|
// Proxy defines the proxy that is being used for connections.
|
|
Proxy string
|
|
|
|
// These fields define the username and password for RPC connections and
|
|
// limited RPC connections.
|
|
RPCUser string
|
|
RPCPass string
|
|
RPCLimitUser string
|
|
RPCLimitPass string
|
|
|
|
// RPCMaxClients defines the max number of RPC clients for standard
|
|
// connections.
|
|
RPCMaxClients int
|
|
|
|
// RPCMaxConcurrentReqs defines the max number of RPC requests that may be
|
|
// processed concurrently.
|
|
RPCMaxConcurrentReqs int
|
|
|
|
// RPCMaxWebsockets defines the max number of RPC websocket connections.
|
|
RPCMaxWebsockets int
|
|
|
|
// TestNet represents whether or not the server is using testnet.
|
|
TestNet bool
|
|
|
|
// MiningAddrs is a list of payment addresses to use for the generated blocks.
|
|
MiningAddrs []stdaddr.Address
|
|
|
|
// AllowUnsyncedMining indicates whether block templates should be created even
|
|
// when the chain is not fully synced.
|
|
AllowUnsyncedMining bool
|
|
|
|
// MaxProtocolVersion is the max protocol version that the server supports.
|
|
MaxProtocolVersion uint32
|
|
|
|
// UserAgentVersion is the user agent version and is used to help identify
|
|
// ourselves to other peers.
|
|
UserAgentVersion string
|
|
|
|
// LogManager defines the log manager for the RPC server to use.
|
|
LogManager LogManager
|
|
|
|
// FiltererV2 defines the V2 filterer for the RPC server to use.
|
|
FiltererV2 FiltererV2
|
|
}
|
|
|
|
// New returns a new instance of the Server struct.
|
|
func New(config *Config) (*Server, error) {
|
|
rpc := Server{
|
|
cfg: *config,
|
|
statusLines: make(map[int]string),
|
|
workState: newWorkState(),
|
|
helpCacher: newHelpCacher(),
|
|
requestProcessShutdown: make(chan struct{}),
|
|
}
|
|
key := make([]byte, 32)
|
|
_, err := io.ReadFull(rand.Reader, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rpc.hmac = hmac.New(sha256.New, key)
|
|
if config.RPCUser != "" && config.RPCPass != "" {
|
|
login := config.RPCUser + ":" + config.RPCPass
|
|
auth := "Basic " +
|
|
base64.StdEncoding.EncodeToString([]byte(login))
|
|
rpc.authMAC(rpc.authsha[:0], []byte(auth))
|
|
}
|
|
if config.RPCLimitUser != "" && config.RPCLimitPass != "" {
|
|
login := config.RPCLimitUser + ":" + config.RPCLimitPass
|
|
auth := "Basic " +
|
|
base64.StdEncoding.EncodeToString([]byte(login))
|
|
rpc.authMAC(rpc.limitauthsha[:0], []byte(auth))
|
|
}
|
|
rpc.ntfnMgr = newWsNotificationManager(&rpc)
|
|
|
|
return &rpc, nil
|
|
}
|
|
|
|
func init() {
|
|
rpcHandlers = rpcHandlersBeforeInit
|
|
|
|
// blake256Pad is the extra blake256 internal padding needed for the
|
|
// data of the getwork RPC. The internal blake256 padding consists of
|
|
// a single 1 bit followed by zeros and a final 1 bit in order to pad
|
|
// the message out to 56 bytes followed by length of the message in
|
|
// bits encoded as a big-endian uint64 (8 bytes). Thus, the resulting
|
|
// length is a multiple of the blake256 block size (64 bytes). Since
|
|
// the block header is a fixed size, it only needs to be calculated
|
|
// once.
|
|
blake256Pad = make([]byte, getworkDataLen-wire.MaxBlockHeaderPayload)
|
|
blake256Pad[0] = 0x80
|
|
blake256Pad[len(blake256Pad)-9] |= 0x01
|
|
binary.BigEndian.PutUint64(blake256Pad[len(blake256Pad)-8:],
|
|
wire.MaxBlockHeaderPayload*8)
|
|
}
|