// Copyright (c) 2021-2023 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package rpc import ( "bytes" "context" "encoding/hex" "errors" "fmt" "strings" "github.com/decred/dcrd/blockchain/standalone/v2" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/gcs/v4" "github.com/decred/dcrd/gcs/v4/blockcf2" dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v4" "github.com/decred/dcrd/wire" "github.com/decred/slog" "github.com/jrick/bitset" "github.com/jrick/wsrpc/v2" ) var ( requiredDcrdVersion = semver{Major: 8, Minor: 0, Patch: 0} ) const ( // These numerical error codes are defined in dcrd/dcrjson. Copied here so // we dont need to import the whole package. ErrRPCDuplicateTx = -40 ErrNoTxInfo = -5 // This error string is defined in dcrd/internal/mempool. Copied here // because it is not exported. ErrUnknownOutputs = "references outputs of unknown or fully-spent transaction" ) // DcrdRPC provides methods for calling dcrd JSON-RPCs without exposing the details // of JSON encoding. type DcrdRPC struct { Caller } type DcrdConnect struct { client *client params *chaincfg.Params log slog.Logger } func SetupDcrd(user, pass, addr string, cert []byte, params *chaincfg.Params, log slog.Logger, blockConnectedChan chan *wire.BlockHeader) DcrdConnect { client := setup(user, pass, addr, cert, log) client.notifier = &blockConnectedHandler{ blockConnected: blockConnectedChan, log: log, } return DcrdConnect{ client: client, params: params, log: log, } } func (d *DcrdConnect) Close() { d.client.Close() d.log.Debug("dcrd client closed") } // Client creates a new DcrdRPC client instance. Returns an error if dialing // dcrd fails or if dcrd is misconfigured. func (d *DcrdConnect) Client() (*DcrdRPC, string, error) { ctx := context.TODO() c, newConnection, err := d.client.dial(ctx) if err != nil { return nil, d.client.addr, fmt.Errorf("dcrd dial error: %w", err) } // If this is a reused connection, we don't need to validate the dcrd config // again. if !newConnection { return &DcrdRPC{c}, d.client.addr, nil } // Verify dcrd is at the required api version. var verMap map[string]dcrdtypes.VersionResult err = c.Call(ctx, "version", &verMap) if err != nil { d.client.Close() return nil, d.client.addr, fmt.Errorf("dcrd version check failed: %w", err) } ver, exists := verMap["dcrdjsonrpcapi"] if !exists { d.client.Close() return nil, d.client.addr, fmt.Errorf("dcrd version response missing 'dcrdjsonrpcapi'") } sVer := semver{ver.Major, ver.Minor, ver.Patch} if !semverCompatible(requiredDcrdVersion, sVer) { d.client.Close() return nil, d.client.addr, fmt.Errorf("dcrd has incompatible JSON-RPC version: got %s, expected %s", sVer, requiredDcrdVersion) } // Verify dcrd is on the correct network. var netID wire.CurrencyNet err = c.Call(ctx, "getcurrentnet", &netID) if err != nil { d.client.Close() return nil, d.client.addr, fmt.Errorf("dcrd getcurrentnet check failed: %w", err) } if netID != d.params.Net { d.client.Close() return nil, d.client.addr, fmt.Errorf("dcrd running on %s, expected %s", netID, d.params.Net) } // Verify dcrd has tx index enabled (required for getrawtransaction). var info dcrdtypes.InfoChainResult err = c.Call(ctx, "getinfo", &info) if err != nil { d.client.Close() return nil, d.client.addr, fmt.Errorf("dcrd getinfo check failed: %w", err) } if !info.TxIndex { d.client.Close() return nil, d.client.addr, errors.New("dcrd does not have transaction index enabled (--txindex)") } // Request blockconnected notifications. if d.client.notifier != nil { err = c.Call(ctx, "notifyblocks", nil) if err != nil { return nil, d.client.addr, fmt.Errorf("notifyblocks failed: %w", err) } } d.log.Debugf("Connected to dcrd") return &DcrdRPC{c}, d.client.addr, nil } // GetRawTransaction uses getrawtransaction RPC to retrieve details about the // transaction with the provided hash. func (c *DcrdRPC) GetRawTransaction(txHash string) (*dcrdtypes.TxRawResult, error) { verbose := 1 var resp dcrdtypes.TxRawResult err := c.Call(context.TODO(), "getrawtransaction", &resp, txHash, verbose) if err != nil { return nil, err } return &resp, nil } // DecodeRawTransaction uses decoderawtransaction RPC to decode raw transaction bytes. func (c *DcrdRPC) DecodeRawTransaction(txHex string) (*dcrdtypes.TxRawDecodeResult, error) { var resp dcrdtypes.TxRawDecodeResult err := c.Call(context.TODO(), "decoderawtransaction", &resp, txHex) if err != nil { return nil, err } return &resp, nil } // SendRawTransaction uses sendrawtransaction RPC to broadcast a transaction to // the network. It ignores errors caused by duplicate transactions. func (c *DcrdRPC) SendRawTransaction(txHex string) error { const allowHighFees = false err := c.Call(context.TODO(), "sendrawtransaction", nil, txHex, allowHighFees) if err != nil { // Ignore errors caused by the transaction already existing in the // mempool or in a mined block. // Error code -40 (ErrRPCDuplicateTx) is completely ignorable because it // indicates that dcrd definitely already has this transaction. var e *wsrpc.Error if errors.As(err, &e) && e.Code == ErrRPCDuplicateTx { return nil } // Errors about orphan/spent outputs indicate that dcrd *might* already // have this transaction. Use getrawtransaction to confirm. if strings.Contains(err.Error(), ErrUnknownOutputs) { _, getErr := c.GetRawTransaction(txHex) if getErr == nil { return nil } } return err } return nil } // IsDCP0010Active uses getblockchaininfo RPC to determine if the DCP-0010 // agenda has activated on the current network. func (c *DcrdRPC) IsDCP0010Active() (bool, error) { var info dcrdtypes.GetBlockChainInfoResult err := c.Call(context.TODO(), "getblockchaininfo", &info) if err != nil { return false, err } agenda, ok := info.Deployments[chaincfg.VoteIDChangeSubsidySplit] if !ok { return false, fmt.Errorf("getblockchaininfo did not return agenda %q", chaincfg.VoteIDChangeSubsidySplit) } return agenda.Status == dcrdtypes.AgendaInfoStatusActive, nil } // NotifyBlocks uses notifyblocks RPC to request new block notifications from dcrd. func (c *DcrdRPC) NotifyBlocks() error { return c.Call(context.TODO(), "notifyblocks", nil) } // GetBestBlockHeader uses getbestblockhash RPC, followed by getblockheader RPC, // to retrieve the header of the best block known to the dcrd instance. func (c *DcrdRPC) GetBestBlockHeader() (*wire.BlockHeader, error) { var bestBlockHash string err := c.Call(context.TODO(), "getbestblockhash", &bestBlockHash) if err != nil { return nil, err } blockHeader, err := c.GetBlockHeader(bestBlockHash) if err != nil { return nil, err } return blockHeader, nil } // GetBlockHeader uses getblockheader RPC with verbose=false to retrieve // the header of the requested block. func (c *DcrdRPC) GetBlockHeader(blockHash string) (*wire.BlockHeader, error) { const verbose = false var resp string err := c.Call(context.TODO(), "getblockheader", &resp, blockHash, verbose) if err != nil { return nil, err } // Decode the serialized block header hex to raw bytes. headerBytes, err := hex.DecodeString(resp) if err != nil { return nil, err } // Deserialize the block header and return it. var blockHeader wire.BlockHeader err = blockHeader.Deserialize(bytes.NewReader(headerBytes)) if err != nil { return nil, err } return &blockHeader, nil } // ExistsLiveTicket uses existslivetickets RPC to check if the provided ticket // hash is a live ticket known to the dcrd instance. func (c *DcrdRPC) ExistsLiveTicket(ticketHash string) (bool, error) { var exists string err := c.Call(context.TODO(), "existslivetickets", &exists, []string{ticketHash}) if err != nil { return false, err } existsBytes := make([]byte, hex.DecodedLen(len(exists))) _, err = hex.Decode(existsBytes, []byte(exists)) if err != nil { return false, err } return bitset.Bytes(existsBytes).Get(0), nil } func (c *DcrdRPC) GetBlock(hash string) (*wire.MsgBlock, error) { var resp string const verbose = false const verboseTx = false err := c.Call(context.TODO(), "getblock", &resp, hash, verbose, verboseTx) if err != nil { return nil, err } // Decode the serialized block hex to raw bytes. blockBytes, err := hex.DecodeString(resp) if err != nil { return nil, err } // Deserialize the block and return it. var msgBlock wire.MsgBlock err = msgBlock.Deserialize(bytes.NewReader(blockBytes)) if err != nil { return nil, err } return &msgBlock, nil } func (c *DcrdRPC) GetBlockCount() (int64, error) { var count int64 err := c.Call(context.TODO(), "getblockcount", &count) if err != nil { return 0, err } return count, nil } func (c *DcrdRPC) GetBlockHash(height int64) (string, error) { var resp string err := c.Call(context.TODO(), "getblockhash", &resp, height) if err != nil { return "", err } return resp, nil } // GetCFilterV2 retrieves the GCS filter for the provided block header, // optionally verifies the inclusion proof, then returns the filter along with // its key. func (c *DcrdRPC) GetCFilterV2(header *wire.BlockHeader, verifyProof bool) ([gcs.KeySize]byte, *gcs.FilterV2, error) { var key [gcs.KeySize]byte var resp dcrdtypes.GetCFilterV2Result err := c.Call(context.TODO(), "getcfilterv2", &resp, header.BlockHash().String()) if err != nil { return key, nil, fmt.Errorf("getcfilterv2 error: %w", err) } filterB, err := hex.DecodeString(resp.Data) if err != nil { return key, nil, fmt.Errorf("error decoding block filter: %w", err) } filter, err := gcs.FromBytesV2(blockcf2.B, blockcf2.M, filterB) if err != nil { return key, nil, fmt.Errorf("error decoding block filter: %w", err) } if verifyProof { filterHash := filter.Hash() proofHashes := make([]chainhash.Hash, len(resp.ProofHashes)) for i, proofHash := range resp.ProofHashes { h, err := chainhash.NewHashFromStr(proofHash) if err != nil { return key, nil, err } proofHashes[i] = *h } if !standalone.VerifyInclusionProof(&header.StakeRoot, &filterHash, resp.ProofIndex, proofHashes) { return key, nil, fmt.Errorf("failed to verify inclusion proof: %w", err) } } return blockcf2.Key(&header.MerkleRoot), filter, nil }