Reject unvotable tickets.
/payfee and /getaddress will now only accept tickets which are immature or live.
This commit is contained in:
parent
1270f77fd6
commit
81a6bf1ea8
@ -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.
|
||||
shutdownWg.Add(1)
|
||||
backupTicker := time.NewTicker(backupInterval)
|
||||
go func() {
|
||||
ticker := time.NewTicker(backupInterval)
|
||||
for {
|
||||
select {
|
||||
case <-backupTicker.C:
|
||||
case <-ticker.C:
|
||||
err := writeBackup(db, dbFile)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to write database backup: %v", err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
backupTicker.Stop()
|
||||
ticker.Stop()
|
||||
shutdownWg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
@ -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
|
||||
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
|
||||
for the specified ticket.
|
||||
@ -82,7 +82,8 @@ for the specified ticket.
|
||||
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,
|
||||
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
|
||||
confirmations. For this reason, it is important that the client ensures the
|
||||
|
||||
1
go.mod
1
go.mod
@ -15,6 +15,7 @@ require (
|
||||
github.com/decred/slog v1.0.0
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
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/wsrpc/v2 v2.3.3
|
||||
go.etcd.io/bbolt v1.3.4
|
||||
|
||||
45
rpc/dcrd.go
45
rpc/dcrd.go
@ -10,6 +10,7 @@ import (
|
||||
"github.com/decred/dcrd/chaincfg/v3"
|
||||
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2"
|
||||
"github.com/decred/dcrd/wire"
|
||||
"github.com/jrick/bitset"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -135,3 +136,47 @@ func (c *DcrdRPC) GetBestBlockHeader() (*dcrdtypes.GetBlockHeaderVerboseResult,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -81,6 +81,20 @@ func feeAddress(c *gin.Context) {
|
||||
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.
|
||||
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
|
||||
// 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)
|
||||
if err != nil {
|
||||
log.Errorf("getCurrentFee error: %v", err)
|
||||
@ -169,7 +160,6 @@ func feeAddress(c *gin.Context) {
|
||||
CommitmentAddress: commitmentAddress,
|
||||
FeeAddressIndex: newAddressIdx,
|
||||
FeeAddress: newAddress,
|
||||
Confirmed: confirmed,
|
||||
FeeAmount: fee,
|
||||
FeeExpiration: expire,
|
||||
// VotingKey and VoteChoices: set during payfee
|
||||
@ -182,8 +172,8 @@ func feeAddress(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("Fee address created for new ticket: tktConfirmed=%t, feeAddrIdx=%d, "+
|
||||
"feeAddr=%s, feeAmt=%f, ticketHash=%s", confirmed, newAddressIdx, newAddress, fee, ticketHash)
|
||||
log.Debugf("Fee address created for new ticket: feeAddrIdx=%d, feeAddr=%s, "+
|
||||
"feeAmt=%f, ticketHash=%s", newAddressIdx, newAddress, fee, ticketHash)
|
||||
|
||||
sendJSONResponse(feeAddressResponse{
|
||||
Timestamp: now.Unix(),
|
||||
|
||||
@ -44,6 +44,18 @@ func payFee(c *gin.Context) {
|
||||
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.
|
||||
if ticket.FeeExpired() {
|
||||
log.Warnf("Expired payfee request from %s", c.ClientIP())
|
||||
|
||||
@ -28,9 +28,6 @@ type Config struct {
|
||||
}
|
||||
|
||||
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.
|
||||
relayFee = 0.0001
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user