multi: Find voted/revoked tickets with GCS filters

Use dcrd and GCS filters to find voted/revoked tickets rather than using
the dcrwallet TicketInfo RPC.

Using TicketInfo was a bit flakey because wallets do not always
correctly detect votes/revokes, and as a result VSP admins may notice
that with this change vspd detects some historic voted/revoked tickets
which TicketInfo never detected.
This commit is contained in:
jholdstock 2023-08-24 15:10:48 +01:00 committed by Jamie Holdstock
parent 618cfc7cf1
commit 9be203c923
6 changed files with 372 additions and 57 deletions

View File

@ -17,6 +17,9 @@ type netParams struct {
// deployment on this network. vspd will log an error and refuse to start if
// fewer wallets are configured.
minWallets int
// dcp0005Height is the activation height of DCP-0005 block header
// commitments agenda on this network.
dcp0005Height int64
}
var mainNetParams = netParams{
@ -25,6 +28,9 @@ var mainNetParams = netParams{
walletRPCServerPort: "9110",
blockExplorerURL: "https://dcrdata.decred.org",
minWallets: 3,
// dcp0005Height on mainnet is block
// 000000000000000010815bed2c4dc431c34a859f4fc70774223dde788e95a01e.
dcp0005Height: 431488,
}
var testNet3Params = netParams{
@ -33,4 +39,13 @@ var testNet3Params = netParams{
walletRPCServerPort: "19110",
blockExplorerURL: "https://testnet.dcrdata.org",
minWallets: 1,
// dcp0005Height on testnet3 is block
// 0000003e54421d585f4a609393a8694509af98f62b8449f245b09fe1389f8f77.
dcp0005Height: 323328,
}
// dcp5Active returns true if the DCP-0005 block header commitments agenda is
// active on this network at the provided height, otherwise false.
func (n *netParams) dcp5Active(height int64) bool {
return height >= n.dcp0005Height
}

174
cmd/vspd/spentticket.go Normal file
View File

@ -0,0 +1,174 @@
// Copyright (c) 2023 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.package main
package main
import (
"fmt"
"github.com/decred/dcrd/blockchain/stake/v5"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/txscript/v4/stdaddr"
"github.com/decred/dcrd/wire"
"github.com/decred/vspd/database"
)
type spentTicket struct {
dbTicket database.Ticket
expiryHeight int64
heightSpent int64
spendingTx *wire.MsgTx
}
func (s *spentTicket) voted() bool {
return stake.IsSSGen(s.spendingTx)
}
// findSpentTickets attempts to find transactions that vote/revoke the provided
// tickets by matching the payment script of the ticket's commitment address
// against the block filters of the mainchain blocks between the provided start
// block and the current best block. Returns any found spent tickets and the
// height of the most recent scanned block.
func (v *vspd) findSpentTickets(toCheck database.TicketList, startHeight int64) ([]spentTicket, int64, error) {
params := v.cfg.netParams
dcrdClient, _, err := v.dcrd.Client()
if err != nil {
return nil, 0, err
}
endHeight, err := dcrdClient.GetBlockCount()
if err != nil {
return nil, 0, fmt.Errorf("dcrd.GetBlockCount error: %w", err)
}
if startHeight > endHeight {
return nil, 0, fmt.Errorf("start height %d greater than best block height %d",
startHeight, endHeight)
}
numBlocks := 1 + endHeight - startHeight
// Only log if checking a larger number of blocks to avoid spam.
if numBlocks > 5 {
v.log.Debugf("Scanning %d blocks for %s",
numBlocks, pluralize(len(toCheck), "spent ticket"))
}
// Get commitment address payment script for each ticket.
type ticketTuple struct {
dbTicket database.Ticket
pkScript []byte
}
tickets := make(map[chainhash.Hash]ticketTuple)
for _, ticket := range toCheck {
parsedAddr, err := stdaddr.DecodeAddress(ticket.CommitmentAddress, params)
if err != nil {
return nil, 0, err
}
_, script := parsedAddr.PaymentScript()
hash, err := chainhash.NewHashFromStr(ticket.Hash)
if err != nil {
return nil, 0, err
}
tickets[*hash] = ticketTuple{
dbTicket: ticket,
pkScript: script,
}
}
spent := make([]spentTicket, 0)
for iHeight := startHeight; iHeight <= endHeight; iHeight++ {
iHash, err := dcrdClient.GetBlockHash(iHeight)
if err != nil {
return nil, 0, err
}
iHeader, err := dcrdClient.GetBlockHeader(iHash)
if err != nil {
return nil, 0, err
}
verifyProof := v.cfg.netParams.dcp5Active(iHeight)
key, filter, err := dcrdClient.GetCFilterV2(iHeader, verifyProof)
if err != nil {
return nil, 0, err
}
var iBlock *wire.MsgBlock
outer:
for ticketHash, ticket := range tickets {
if filter.Match(key, ticket.pkScript) {
// Filter match means the ticket is likely spent in block. Get
// the full block to confirm.
if iBlock == nil {
iBlock, err = dcrdClient.GetBlock(iHash)
if err != nil {
return nil, 0, err
}
}
// The regular transaction tree does not need to be checked
// because tickets can only be spent by vote or revoke
// transactions which are always in the stake tree.
for _, blkTx := range iBlock.STransactions {
if !txSpendsTicket(blkTx, ticketHash) {
continue
}
// Confirmed - ticket is spent in block.
spent = append(spent, spentTicket{
dbTicket: ticket.dbTicket,
expiryHeight: ticket.dbTicket.PurchaseHeight + int64(params.TicketMaturity) + int64(params.TicketExpiry),
heightSpent: iHeight,
spendingTx: blkTx,
})
// Remove this ticket and continue with the next one.
delete(tickets, ticketHash)
continue outer
}
// Ticket is not spent in block.
}
}
if len(tickets) == 0 {
// Found spenders for all tickets, stop searching.
break
}
}
return spent, endHeight, nil
}
// txSpendsTicket returns true if the passed tx has an input that spends the
// specified output.
func txSpendsTicket(tx *wire.MsgTx, outputHash chainhash.Hash) bool {
for _, txIn := range tx.TxIn {
prevOut := &txIn.PreviousOutPoint
if prevOut.Index == 0 && prevOut.Hash == outputHash {
return true // Found spender.
}
}
return false
}
// pluralize suffixes the provided noun with "s" if n is not 1, then
// concatenates n and noun with a space between them. For example:
//
// (0, "biscuit") will return "0 biscuits"
// (1, "biscuit") will return "1 biscuit"
// (3, "biscuit") will return "3 biscuits"
func pluralize(n int, noun string) string {
if n != 1 {
noun += "s"
}
return fmt.Sprintf("%d %s", n, noun)
}

View File

@ -44,6 +44,10 @@ type vspd struct {
wallets rpc.WalletConnect
blockNotifChan chan *wire.BlockHeader
// lastScannedBlock is the height of the most recent block which has been
// scanned for spent tickets.
lastScannedBlock int64
}
// newVspd creates the essential resources required by vspd - a database, logger
@ -466,63 +470,54 @@ func (v *vspd) blockConnected() {
// Step 4/4: Set ticket outcome in database if any tickets are voted/revoked.
// Ticket status needs to be checked on every wallet. This is because only
// one of the voting wallets will actually succeed in voting/revoking
// tickets (the others will get errors like "tx already exists"). Only the
// successful wallet will have the most up-to-date ticket status, the others
// will be outdated.
for _, walletClient := range walletClients {
votableTickets, err := v.db.GetVotableTickets()
if err != nil {
v.log.Errorf("%s: db.GetVotableTickets failed: %v", funcName, err)
continue
}
// If the database has no votable tickets, there is nothing more to do
if len(votableTickets) == 0 {
break
}
// Find the oldest block height from confirmed tickets.
oldestHeight := votableTickets.EarliestPurchaseHeight()
ticketInfo, err := walletClient.TicketInfo(oldestHeight)
if err != nil {
v.log.Errorf("%s: dcrwallet.TicketInfo failed (startHeight=%d, wallet=%s): %v",
funcName, oldestHeight, walletClient.String(), err)
continue
}
for _, dbTicket := range votableTickets {
tInfo, ok := ticketInfo[dbTicket.Hash]
if !ok {
v.log.Warnf("%s: TicketInfo response did not include expected ticket (wallet=%s, ticketHash=%s)",
funcName, walletClient.String(), dbTicket.Hash)
continue
}
switch tInfo.Status {
case "missed", "expired", "revoked":
dbTicket.Outcome = database.Revoked
case "voted":
dbTicket.Outcome = database.Voted
default:
// Skip to next ticket.
continue
}
err = v.db.UpdateTicket(dbTicket)
if err != nil {
v.log.Errorf("%s: db.UpdateTicket error, failed to set ticket outcome (ticketHash=%s): %v",
funcName, dbTicket.Hash, err)
continue
}
v.log.Infof("%s: Ticket no longer votable: outcome=%s, ticketHash=%s", funcName,
dbTicket.Outcome, dbTicket.Hash)
}
votableTickets, err := v.db.GetVotableTickets()
if err != nil {
v.log.Errorf("%s: db.GetVotableTickets failed: %v", funcName, err)
return
}
// If the database has no votable tickets, there is nothing more to do.
if len(votableTickets) == 0 {
return
}
var startHeight int64
if v.lastScannedBlock == 0 {
// Use the earliest height at which a votable ticket matured if vspd has
// not performed a scan for spent tickets since it started. This will
// catch any tickets which were spent whilst vspd was offline.
startHeight = votableTickets.EarliestPurchaseHeight() + int64(v.cfg.netParams.TicketMaturity)
} else {
startHeight = v.lastScannedBlock
}
spent, endHeight, err := v.findSpentTickets(votableTickets, startHeight)
if err != nil {
v.log.Errorf("%s: findSpentTickets error: %v", funcName, err)
return
}
v.lastScannedBlock = endHeight
for _, spentTicket := range spent {
dbTicket := spentTicket.dbTicket
if spentTicket.voted() {
dbTicket.Outcome = database.Voted
} else {
dbTicket.Outcome = database.Revoked
}
err = v.db.UpdateTicket(dbTicket)
if err != nil {
v.log.Errorf("%s: db.UpdateTicket error, failed to set ticket outcome (ticketHash=%s): %v",
funcName, dbTicket.Hash, err)
continue
}
v.log.Infof("%s: Ticket %s %s at height %d", funcName,
dbTicket.Hash, dbTicket.Outcome, spentTicket.heightSpent)
}
}
// checkWalletConsistency will retrieve all votable tickets from the database

5
go.mod
View File

@ -10,6 +10,7 @@ require (
github.com/decred/dcrd/chaincfg/v3 v3.2.0
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
github.com/decred/dcrd/dcrutil/v4 v4.0.1
github.com/decred/dcrd/gcs/v4 v4.0.0
github.com/decred/dcrd/hdkeychain/v3 v3.1.1
github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0
github.com/decred/dcrd/txscript/v4 v4.1.0
@ -49,10 +50,12 @@ require (
github.com/gorilla/websocket v1.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
@ -62,7 +65,9 @@ require (
golang.org/x/sys v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
golang.org/x/time v0.3.0
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
)

15
go.sum
View File

@ -8,6 +8,7 @@ github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZX
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -39,6 +40,8 @@ github.com/decred/dcrd/dcrjson/v4 v4.0.1 h1:vyQuB1miwGqbCVNm8P6br3V65WQ6wyrh0Lyc
github.com/decred/dcrd/dcrjson/v4 v4.0.1/go.mod h1:2qVikafVF9/X3PngQVmqkbUbyAl32uik0k/kydgtqMc=
github.com/decred/dcrd/dcrutil/v4 v4.0.1 h1:E+d2TNbpOj0f1L9RqkZkEm1QolFjajvkzxWC5WOPf1s=
github.com/decred/dcrd/dcrutil/v4 v4.0.1/go.mod h1:7EXyHYj8FEqY+WzMuRkF0nh32ueLqhutZDoW4eQ+KRc=
github.com/decred/dcrd/gcs/v4 v4.0.0 h1:bet+Ax1ZFUqn2M0g1uotm0b8F6BZ9MmblViyJ088E8k=
github.com/decred/dcrd/gcs/v4 v4.0.0/go.mod h1:9z+EBagzpEdAumwS09vf/hiGaR8XhNmsBgaVq6u7/NI=
github.com/decred/dcrd/hdkeychain/v3 v3.1.1 h1:4WhyHNBy7ec6qBUC7Fq7JFVGSd7bpuR5H+AJRID8Lyk=
github.com/decred/dcrd/hdkeychain/v3 v3.1.1/go.mod h1:HaabrLc27lnny5/Ph9+6I3szp0op5MCb7smEwlzfD60=
github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0 h1:4YUKsWKrKlkhVMYGRB6G0XI6QfwUnwEH18eoEbM1/+M=
@ -94,6 +97,10 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
@ -103,6 +110,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -141,13 +150,15 @@ golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -5,13 +5,18 @@
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"
@ -232,6 +237,32 @@ func (c *DcrdRPC) GetBestBlockHeader() (*dcrdtypes.GetBlockHeaderVerboseResult,
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
}
// GetBlockHeaderVerbose uses getblockheader RPC with verbose=true to retrieve
// the header of the requested block.
func (c *DcrdRPC) GetBlockHeaderVerbose(blockHash string) (*dcrdtypes.GetBlockHeaderVerboseResult, error) {
@ -261,3 +292,87 @@ func (c *DcrdRPC) ExistsLiveTicket(ticketHash string) (bool, error) {
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
}