diff --git a/cmd/vspd/params.go b/cmd/vspd/params.go index 861be11a..baf17ab7 100644 --- a/cmd/vspd/params.go +++ b/cmd/vspd/params.go @@ -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 } diff --git a/cmd/vspd/spentticket.go b/cmd/vspd/spentticket.go new file mode 100644 index 00000000..5146c1bf --- /dev/null +++ b/cmd/vspd/spentticket.go @@ -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) +} diff --git a/cmd/vspd/vspd.go b/cmd/vspd/vspd.go index 903b2ac5..bebd0db4 100644 --- a/cmd/vspd/vspd.go +++ b/cmd/vspd/vspd.go @@ -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 - } + 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 { - break - } + // If the database has no votable tickets, there is nothing more to do. + if len(votableTickets) == 0 { + return + } - // Find the oldest block height from confirmed tickets. - oldestHeight := votableTickets.EarliestPurchaseHeight() + 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 + } - 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 - } + spent, endHeight, err := v.findSpentTickets(votableTickets, startHeight) + if err != nil { + v.log.Errorf("%s: findSpentTickets error: %v", funcName, err) + return + } - 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 - } + v.lastScannedBlock = endHeight - switch tInfo.Status { - case "missed", "expired", "revoked": - dbTicket.Outcome = database.Revoked - case "voted": - dbTicket.Outcome = database.Voted - default: - // Skip to next ticket. - continue - } + for _, spentTicket := range spent { + dbTicket := spentTicket.dbTicket - 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 - } + if spentTicket.voted() { + dbTicket.Outcome = database.Voted + } else { + dbTicket.Outcome = database.Revoked + } - v.log.Infof("%s: Ticket no longer votable: outcome=%s, ticketHash=%s", funcName, - dbTicket.Outcome, dbTicket.Hash) + 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 diff --git a/go.mod b/go.mod index ab1a11ff..a448dbbc 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 117edf1c..e37278fe 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/rpc/dcrd.go b/rpc/dcrd.go index da0609d5..0eaffdd5 100644 --- a/rpc/dcrd.go +++ b/rpc/dcrd.go @@ -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 +}