This converts the internal/rpcserver package to use the stdscript package instead of txscript. This is part of a series of commits to convert all packages in the repository to make use of the new stdscript package.
6577 lines
203 KiB
Go
6577 lines
203 KiB
Go
// Copyright (c) 2013-2016 The btcsuite developers
|
|
// Copyright (c) 2015-2021 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
|
|
}
|
|
|
|
dev := s.cfg.SubsidyCache.CalcTreasurySubsidy(height, voters,
|
|
isTreasuryEnabled)
|
|
pos := s.cfg.SubsidyCache.CalcStakeVoteSubsidy(height-1) * int64(voters)
|
|
pow := s.cfg.SubsidyCache.CalcWorkSubsidy(height, voters)
|
|
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
|
|
}
|
|
|
|
// 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 {
|
|
err := ws.WriteControl(websocket.PongMessage, []byte(payload),
|
|
time.Now().Add(time.Second))
|
|
log.Debugf("ping received: len %d", len(payload))
|
|
log.Tracef("ping payload: %s", payload)
|
|
if err != nil {
|
|
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: %s", 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)
|
|
}
|