vspd/webapi/payfee.go
jholdstock 81a6bf1ea8 Reject unvotable tickets.
/payfee and /getaddress will now only accept tickets which are immature or live.
2020-06-03 12:49:14 +00:00

195 lines
5.9 KiB
Go

package webapi
import (
"encoding/hex"
"net/http"
"time"
"github.com/decred/dcrd/dcrec"
"github.com/decred/dcrd/dcrutil/v3"
"github.com/decred/dcrd/txscript/v3"
"github.com/decred/dcrd/wire"
"github.com/decred/vspd/database"
"github.com/decred/vspd/rpc"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
// payFee is the handler for "POST /payfee".
func payFee(c *gin.Context) {
// Get values which have been added to context by middleware.
rawRequest := c.MustGet("RawRequest").([]byte)
ticket := c.MustGet("Ticket").(database.Ticket)
knownTicket := c.MustGet("KnownTicket").(bool)
dcrdClient := c.MustGet("DcrdClient").(*rpc.DcrdRPC)
if !knownTicket {
log.Warnf("Invalid ticket from %s", c.ClientIP())
sendErrorResponse("invalid ticket", http.StatusBadRequest, c)
return
}
var payFeeRequest PayFeeRequest
if err := binding.JSON.BindBody(rawRequest, &payFeeRequest); err != nil {
log.Warnf("Bad payfee request from %s: %v", c.ClientIP(), err)
sendErrorResponse(err.Error(), http.StatusBadRequest, c)
return
}
// Respond early if we already have the fee tx for this ticket.
if ticket.FeeTxHex != "" {
log.Warnf("Fee tx already received from %s: ticketHash=%s", c.ClientIP(), ticket.Hash)
sendErrorResponse("fee tx already received", http.StatusBadRequest, c)
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())
sendErrorResponse("fee has expired", http.StatusBadRequest, c)
return
}
// Validate VotingKey.
votingKey := payFeeRequest.VotingKey
votingWIF, err := dcrutil.DecodeWIF(votingKey, cfg.NetParams.PrivateKeyID)
if err != nil {
log.Warnf("Failed to decode WIF: %v", err)
sendErrorResponse("error decoding WIF", http.StatusBadRequest, c)
return
}
// Validate VoteChoices.
voteChoices := payFeeRequest.VoteChoices
err = isValidVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), voteChoices)
if err != nil {
log.Warnf("Invalid votechoices from %s: %v", c.ClientIP(), err)
sendErrorResponse(err.Error(), http.StatusBadRequest, c)
return
}
// Validate FeeTx.
feeTxBytes, err := hex.DecodeString(payFeeRequest.FeeTx)
if err != nil {
log.Warnf("Failed to decode tx: %v", err)
sendErrorResponse("failed to decode transaction", http.StatusBadRequest, c)
return
}
feeTx := wire.NewMsgTx()
err = feeTx.FromBytes(feeTxBytes)
if err != nil {
log.Warnf("Failed to deserialize tx: %v", err)
sendErrorResponse("unable to deserialize transaction", http.StatusBadRequest, c)
return
}
// Loop through transaction outputs until we find one which pays to the
// expected fee address. Record how much is being paid to the fee address.
var feePaid dcrutil.Amount
const scriptVersion = 0
findAddress:
for _, txOut := range feeTx.TxOut {
_, addresses, _, err := txscript.ExtractPkScriptAddrs(scriptVersion,
txOut.PkScript, cfg.NetParams)
if err != nil {
log.Errorf("Extract PK error: %v", err)
sendErrorResponse("extract PK error", http.StatusInternalServerError, c)
return
}
for _, addr := range addresses {
if addr.Address() == ticket.FeeAddress {
feePaid = dcrutil.Amount(txOut.Value)
break findAddress
}
}
}
if feePaid == 0 {
log.Warnf("FeeTx for ticket %s did not include any payments for address %s", ticket.Hash, ticket.FeeAddress)
sendErrorResponse("feetx did not include any payments for fee address", http.StatusBadRequest, c)
return
}
_, err = dcrutil.NewAddressPubKeyHash(dcrutil.Hash160(votingWIF.PubKey()), cfg.NetParams,
dcrec.STEcdsaSecp256k1)
if err != nil {
log.Errorf("NewAddressPubKeyHash: %v", err)
sendErrorResponse("failed to deserialize voting wif", http.StatusInternalServerError, c)
return
}
// TODO: DB - validate votingkey against ticket submission address
minFee, err := dcrutil.NewAmount(ticket.FeeAmount)
if err != nil {
log.Errorf("dcrutil.NewAmount: %v", err)
sendErrorResponse("fee error", http.StatusInternalServerError, c)
return
}
if feePaid < minFee {
log.Warnf("Fee too small from %s: was %v, expected %v", c.ClientIP(), feePaid, minFee)
sendErrorResponse("fee too small", http.StatusInternalServerError, c)
return
}
// At this point we are satisfied that the request is valid and the FeeTx
// pays sufficient fees to the expected address. Proceed to update the
// database, and if the ticket is confirmed broadcast the transaction.
ticket.VotingWIF = votingWIF.String()
ticket.FeeTxHex = payFeeRequest.FeeTx
ticket.VoteChoices = voteChoices
err = db.UpdateTicket(ticket)
if err != nil {
log.Errorf("InsertTicket failed: %v", err)
sendErrorResponse("database error", http.StatusInternalServerError, c)
return
}
log.Debugf("Fee tx received for ticket: minExpectedFee=%v, feePaid=%v, "+
"ticketHash=%s", minFee, feePaid, ticket.Hash)
if ticket.Confirmed {
feeTxHash, err := dcrdClient.SendRawTransaction(payFeeRequest.FeeTx)
if err != nil {
// TODO: SendRawTransaction can return a "transcation already
// exists" error, which isnt necessarily a problem here.
log.Errorf("SendRawTransaction failed: %v", err)
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
return
}
ticket.FeeTxHash = feeTxHash
err = db.UpdateTicket(ticket)
if err != nil {
log.Errorf("InsertTicket failed: %v", err)
sendErrorResponse("database error", http.StatusInternalServerError, c)
return
}
log.Debugf("Fee tx broadcast for ticket: ticketHash=%s, feeHash=%s", ticket.Hash, feeTxHash)
}
sendJSONResponse(payFeeResponse{
Timestamp: time.Now().Unix(),
Request: payFeeRequest,
}, c)
}