diff --git a/database/database.go b/database/database.go index 281db74..194a644 100644 --- a/database/database.go +++ b/database/database.go @@ -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 } diff --git a/docs/api.md b/docs/api.md index ce21ef4..bfaa2cb 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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 diff --git a/go.mod b/go.mod index 4ed1c59..58e0b45 100644 --- a/go.mod +++ b/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 diff --git a/rpc/dcrd.go b/rpc/dcrd.go index 2d2a63b..ec84eb9 100644 --- a/rpc/dcrd.go +++ b/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 +} diff --git a/webapi/getfeeaddress.go b/webapi/getfeeaddress.go index d67db5f..ff569a4 100644 --- a/webapi/getfeeaddress.go +++ b/webapi/getfeeaddress.go @@ -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(), diff --git a/webapi/payfee.go b/webapi/payfee.go index 4ec261b..9e83010 100644 --- a/webapi/payfee.go +++ b/webapi/payfee.go @@ -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()) diff --git a/webapi/webapi.go b/webapi/webapi.go index 0457771..49afa16 100644 --- a/webapi/webapi.go +++ b/webapi/webapi.go @@ -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 )