Reject unvotable tickets.

/payfee and /getaddress will now only accept tickets which are immature or live.
This commit is contained in:
jholdstock 2020-06-01 12:44:19 +01:00 committed by David Hill
parent 1270f77fd6
commit 81a6bf1ea8
7 changed files with 80 additions and 34 deletions

View File

@ -98,17 +98,17 @@ func Open(ctx context.Context, shutdownWg *sync.WaitGroup, dbFile string, backup
// Start a ticker to update the backup file at the specified interval. // Start a ticker to update the backup file at the specified interval.
shutdownWg.Add(1) shutdownWg.Add(1)
backupTicker := time.NewTicker(backupInterval)
go func() { go func() {
ticker := time.NewTicker(backupInterval)
for { for {
select { select {
case <-backupTicker.C: case <-ticker.C:
err := writeBackup(db, dbFile) err := writeBackup(db, dbFile)
if err != nil { if err != nil {
log.Errorf("Failed to write database backup: %v", err) log.Errorf("Failed to write database backup: %v", err)
} }
case <-ctx.Done(): case <-ctx.Done():
backupTicker.Stop() ticker.Stop()
shutdownWg.Done() shutdownWg.Done()
return return
} }

View File

@ -48,7 +48,7 @@ its voting wallets unless both of these calls have succeeded.**
Request fee amount and address for a ticket. The fee amount is only valid until Request fee amount and address for a ticket. The fee amount is only valid until
the expiration time has passed. The fee amount is an absolute value measured in the expiration time has passed. The fee amount is an absolute value measured in
DCR. DCR. Returns an error if the specified ticket is not currently immature or live.
This call will return an error if a fee transaction has already been provided This call will return an error if a fee transaction has already been provided
for the specified ticket. for the specified ticket.
@ -82,7 +82,8 @@ for the specified ticket.
Provide the voting key for the ticket, voting preference, and a signed Provide the voting key for the ticket, voting preference, and a signed
transaction which pays the fee to the specified address. If the fee has expired, transaction which pays the fee to the specified address. If the fee has expired,
this call will return an error and the client will need to request a new fee by this call will return an error and the client will need to request a new fee by
calling `/feeaddress` again. calling `/feeaddress` again. Returns an error if the specified ticket is not
currently immature or live.
The VSP will not broadcast the fee transaction until the ticket purchase has 6 The VSP will not broadcast the fee transaction until the ticket purchase has 6
confirmations. For this reason, it is important that the client ensures the confirmations. For this reason, it is important that the client ensures the

1
go.mod
View File

@ -15,6 +15,7 @@ require (
github.com/decred/slog v1.0.0 github.com/decred/slog v1.0.0
github.com/gin-gonic/gin v1.6.3 github.com/gin-gonic/gin v1.6.3
github.com/jessevdk/go-flags v1.4.0 github.com/jessevdk/go-flags v1.4.0
github.com/jrick/bitset v1.0.0
github.com/jrick/logrotate v1.0.0 github.com/jrick/logrotate v1.0.0
github.com/jrick/wsrpc/v2 v2.3.3 github.com/jrick/wsrpc/v2 v2.3.3
go.etcd.io/bbolt v1.3.4 go.etcd.io/bbolt v1.3.4

View File

@ -10,6 +10,7 @@ import (
"github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/chaincfg/v3"
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2" dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2"
"github.com/decred/dcrd/wire" "github.com/decred/dcrd/wire"
"github.com/jrick/bitset"
) )
const ( const (
@ -135,3 +136,47 @@ func (c *DcrdRPC) GetBestBlockHeader() (*dcrdtypes.GetBlockHeaderVerboseResult,
} }
return &blockHeader, nil return &blockHeader, nil
} }
func (c *DcrdRPC) ExistsLiveTicket(ticketHash string) (bool, error) {
var exists string
err := c.Call(c.ctx, "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
}
// CanTicketVote checks determines whether a ticket is able to vote at some
// point in the future by checking that it is currently either immature or live.
func (c *DcrdRPC) CanTicketVote(ticketHash string, netParams *chaincfg.Params) (bool, error) {
// Get ticket details.
rawTx, err := c.GetRawTransaction(ticketHash)
if err != nil {
return false, err
}
// Tickets which older than (TicketMaturity+TicketExpiry) are too old to vote.
if rawTx.Confirmations > int64(uint32(netParams.TicketMaturity)+netParams.TicketExpiry) {
return false, nil
}
// If ticket is currently immature, it will be able to vote in future.
if rawTx.Confirmations <= int64(netParams.TicketMaturity) {
return true, nil
}
// If ticket is currently live, it will be able to vote in future.
live, err := c.ExistsLiveTicket(ticketHash)
if err != nil {
return false, err
}
return live, nil
}

View File

@ -81,6 +81,20 @@ func feeAddress(c *gin.Context) {
return return
} }
ticketHash := feeAddressRequest.TicketHash
canVote, err := dcrdClient.CanTicketVote(ticketHash, cfg.NetParams)
if err != nil {
log.Errorf("canTicketVote error: %v", err)
sendErrorResponse("error validating ticket", http.StatusInternalServerError, c)
return
}
if !canVote {
log.Warnf("Unvotable ticket %s from %s", ticketHash, c.ClientIP())
sendErrorResponse("ticket not eligible to vote", http.StatusBadRequest, c)
return
}
// VSP already knows this ticket and has already issued it a fee address. // VSP already knows this ticket and has already issued it a fee address.
if knownTicket { if knownTicket {
@ -126,29 +140,6 @@ func feeAddress(c *gin.Context) {
// Beyond this point we are processing a new ticket which the VSP has not // Beyond this point we are processing a new ticket which the VSP has not
// seen before. // seen before.
ticketHash := feeAddressRequest.TicketHash
// Get transaction details.
rawTx, err := dcrdClient.GetRawTransaction(ticketHash)
if err != nil {
log.Warnf("Could not retrieve tx %s for %s: %v", ticketHash, c.ClientIP(), err)
sendErrorResponse("unknown transaction", http.StatusBadRequest, c)
return
}
// Don't accept tickets which are too old.
if rawTx.Confirmations > int64(uint32(cfg.NetParams.TicketMaturity)+cfg.NetParams.TicketExpiry) {
log.Warnf("Too old tx from %s", c.ClientIP())
sendErrorResponse("transaction too old", http.StatusBadRequest, c)
return
}
// Check if ticket is fully confirmed.
var confirmed bool
if rawTx.Confirmations >= requiredConfs {
confirmed = true
}
fee, err := getCurrentFee(dcrdClient) fee, err := getCurrentFee(dcrdClient)
if err != nil { if err != nil {
log.Errorf("getCurrentFee error: %v", err) log.Errorf("getCurrentFee error: %v", err)
@ -169,7 +160,6 @@ func feeAddress(c *gin.Context) {
CommitmentAddress: commitmentAddress, CommitmentAddress: commitmentAddress,
FeeAddressIndex: newAddressIdx, FeeAddressIndex: newAddressIdx,
FeeAddress: newAddress, FeeAddress: newAddress,
Confirmed: confirmed,
FeeAmount: fee, FeeAmount: fee,
FeeExpiration: expire, FeeExpiration: expire,
// VotingKey and VoteChoices: set during payfee // VotingKey and VoteChoices: set during payfee
@ -182,8 +172,8 @@ func feeAddress(c *gin.Context) {
return return
} }
log.Debugf("Fee address created for new ticket: tktConfirmed=%t, feeAddrIdx=%d, "+ log.Debugf("Fee address created for new ticket: feeAddrIdx=%d, feeAddr=%s, "+
"feeAddr=%s, feeAmt=%f, ticketHash=%s", confirmed, newAddressIdx, newAddress, fee, ticketHash) "feeAmt=%f, ticketHash=%s", newAddressIdx, newAddress, fee, ticketHash)
sendJSONResponse(feeAddressResponse{ sendJSONResponse(feeAddressResponse{
Timestamp: now.Unix(), Timestamp: now.Unix(),

View File

@ -44,6 +44,18 @@ func payFee(c *gin.Context) {
return return
} }
canVote, err := dcrdClient.CanTicketVote(ticket.Hash, cfg.NetParams)
if err != nil {
log.Errorf("canTicketVote error: %v", err)
sendErrorResponse("error validating ticket", http.StatusInternalServerError, c)
return
}
if !canVote {
log.Warnf("Unvotable ticket %s from %s", ticket.Hash, c.ClientIP())
sendErrorResponse("ticket not eligible to vote", http.StatusBadRequest, c)
return
}
// Respond early if the fee for this ticket is expired. // Respond early if the fee for this ticket is expired.
if ticket.FeeExpired() { if ticket.FeeExpired() {
log.Warnf("Expired payfee request from %s", c.ClientIP()) log.Warnf("Expired payfee request from %s", c.ClientIP())

View File

@ -28,9 +28,6 @@ type Config struct {
} }
const ( const (
// requiredConfs is the number of confirmations required to consider a
// ticket purchase or a fee transaction to be final.
requiredConfs = 6
// TODO: Make this configurable or get it from RPC. // TODO: Make this configurable or get it from RPC.
relayFee = 0.0001 relayFee = 0.0001
) )