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:
parent
618cfc7cf1
commit
9be203c923
@ -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
174
cmd/vspd/spentticket.go
Normal 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)
|
||||
}
|
||||
105
cmd/vspd/vspd.go
105
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
|
||||
}
|
||||
|
||||
// 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
5
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
|
||||
)
|
||||
|
||||
15
go.sum
15
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=
|
||||
|
||||
115
rpc/dcrd.go
115
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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user