Remove global vars from webapi package.

This commit is contained in:
jholdstock 2022-03-15 15:50:29 +00:00 committed by Jamie Holdstock
parent 4d4f9c8ca0
commit 37d51df546
13 changed files with 420 additions and 411 deletions

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 The Decred developers
// Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -109,7 +109,7 @@ func walletStatus(c *gin.Context) map[string]WalletStatus {
// statusJSON is the handler for "GET /admin/status". It returns a JSON object
// describing the current status of voting wallets.
func statusJSON(c *gin.Context) {
func (s *Server) statusJSON(c *gin.Context) {
httpStatus := http.StatusOK
wallets := walletStatus(c)
@ -141,10 +141,10 @@ func statusJSON(c *gin.Context) {
}
// adminPage is the handler for "GET /admin".
func adminPage(c *gin.Context) {
func (s *Server) adminPage(c *gin.Context) {
c.HTML(http.StatusOK, "admin.html", gin.H{
"WebApiCache": getCache(),
"WebApiCfg": cfg,
"WebApiCfg": s.cfg,
"WalletStatus": walletStatus(c),
"DcrdStatus": dcrdStatus(c),
})
@ -152,24 +152,24 @@ func adminPage(c *gin.Context) {
// ticketSearch is the handler for "POST /admin/ticket". The hash param will be
// used to retrieve a ticket from the database.
func ticketSearch(c *gin.Context) {
func (s *Server) ticketSearch(c *gin.Context) {
hash := c.PostForm("hash")
ticket, found, err := db.GetTicketByHash(hash)
ticket, found, err := s.db.GetTicketByHash(hash)
if err != nil {
log.Errorf("db.GetTicketByHash error (ticketHash=%s): %v", hash, err)
c.String(http.StatusInternalServerError, "Error getting ticket from db")
return
}
voteChanges, err := db.GetVoteChanges(hash)
voteChanges, err := s.db.GetVoteChanges(hash)
if err != nil {
log.Errorf("db.GetVoteChanges error (ticketHash=%s): %v", hash, err)
c.String(http.StatusInternalServerError, "Error getting vote changes from db")
return
}
altSignAddrData, err := db.AltSignAddrData(hash)
altSignAddrData, err := s.db.AltSignAddrData(hash)
if err != nil {
log.Errorf("db.AltSignAddrData error (ticketHash=%s): %v", hash, err)
c.String(http.StatusInternalServerError, "Error getting alt sig from db")
@ -183,10 +183,10 @@ func ticketSearch(c *gin.Context) {
Ticket: ticket,
AltSignAddrData: altSignAddrData,
VoteChanges: voteChanges,
MaxVoteChanges: cfg.MaxVoteChangeRecords,
MaxVoteChanges: s.cfg.MaxVoteChangeRecords,
},
"WebApiCache": getCache(),
"WebApiCfg": cfg,
"WebApiCfg": s.cfg,
"WalletStatus": walletStatus(c),
"DcrdStatus": dcrdStatus(c),
})
@ -194,14 +194,14 @@ func ticketSearch(c *gin.Context) {
// adminLogin is the handler for "POST /admin". If a valid password is provided,
// the current session will be authenticated as an admin.
func adminLogin(c *gin.Context) {
func (s *Server) adminLogin(c *gin.Context) {
password := c.PostForm("password")
if password != cfg.AdminPass {
if password != s.cfg.AdminPass {
log.Warnf("Failed login attempt from %s", c.ClientIP())
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
"WebApiCache": getCache(),
"WebApiCfg": cfg,
"WebApiCfg": s.cfg,
"IncorrectPassword": true,
})
return
@ -212,14 +212,14 @@ func adminLogin(c *gin.Context) {
// adminLogout is the handler for "POST /admin/logout". The current session will
// have its admin authentication removed.
func adminLogout(c *gin.Context) {
func (s *Server) adminLogout(c *gin.Context) {
setAdminStatus(nil, c)
}
// downloadDatabaseBackup is the handler for "GET /backup". A binary
// representation of the whole database is generated and returned to the client.
func downloadDatabaseBackup(c *gin.Context) {
err := db.BackupDB(c.Writer)
func (s *Server) downloadDatabaseBackup(c *gin.Context) {
err := s.db.BackupDB(c.Writer)
if err != nil {
log.Errorf("Error backing up database: %v", err)
// Don't write any http body here because Content-Length has already

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 The Decred developers
// Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -6,7 +6,6 @@ package webapi
import (
"context"
"encoding/base64"
"errors"
"sync"
"time"
@ -45,12 +44,12 @@ func getCache() apiCache {
// initCache creates the struct which holds the cached VSP stats, and
// initializes it with static values.
func initCache() {
func initCache(signPubKey string) {
cacheMtx.Lock()
defer cacheMtx.Unlock()
cache = apiCache{
PubKey: base64.StdEncoding.EncodeToString(signPubKey),
PubKey: signPubKey,
}
}
@ -85,7 +84,7 @@ func updateCache(ctx context.Context, db *database.VspDatabase,
return errors.New("dcr node reports a network ticket pool size of zero")
}
clients, failedConnections := wallets.Clients(ctx, cfg.NetParams)
clients, failedConnections := wallets.Clients(ctx, netParams)
if len(clients) == 0 {
log.Error("Could not connect to any wallets")
} else if len(failedConnections) > 0 {

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 The Decred developers
// Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -23,16 +23,16 @@ var addrMtx sync.Mutex
// the last used address index in the database. In order to maintain consistency
// between the internal counter of address generator and the database, this func
// uses a mutex to ensure it is not run concurrently.
func getNewFeeAddress(db *database.VspDatabase, addrGen *addressGenerator) (string, uint32, error) {
func (s *Server) getNewFeeAddress() (string, uint32, error) {
addrMtx.Lock()
defer addrMtx.Unlock()
addr, idx, err := addrGen.NextAddress()
addr, idx, err := s.addrGen.NextAddress()
if err != nil {
return "", 0, err
}
err = db.SetLastAddressIndex(idx)
err = s.db.SetLastAddressIndex(idx)
if err != nil {
return "", 0, err
}
@ -42,7 +42,7 @@ func getNewFeeAddress(db *database.VspDatabase, addrGen *addressGenerator) (stri
// getCurrentFee returns the minimum fee amount a client should pay in order to
// register a ticket with the VSP at the current block height.
func getCurrentFee(dcrdClient *rpc.DcrdRPC) (dcrutil.Amount, error) {
func (s *Server) getCurrentFee(dcrdClient *rpc.DcrdRPC) (dcrutil.Amount, error) {
bestBlock, err := dcrdClient.GetBestBlockHeader()
if err != nil {
return 0, err
@ -64,7 +64,7 @@ func getCurrentFee(dcrdClient *rpc.DcrdRPC) (dcrutil.Amount, error) {
}
fee := txrules.StakePoolTicketFee(sDiff, defaultMinRelayTxFee,
int32(bestBlock.Height), cfg.VSPFee, cfg.NetParams, isDCP0010Active)
int32(bestBlock.Height), s.cfg.VSPFee, s.cfg.NetParams, isDCP0010Active)
if err != nil {
return 0, err
}
@ -72,7 +72,7 @@ func getCurrentFee(dcrdClient *rpc.DcrdRPC) (dcrutil.Amount, error) {
}
// feeAddress is the handler for "POST /api/v3/feeaddress".
func feeAddress(c *gin.Context) {
func (s *Server) feeAddress(c *gin.Context) {
const funcName = "feeAddress"
@ -84,20 +84,20 @@ func feeAddress(c *gin.Context) {
dcrdErr := c.MustGet(dcrdErrorKey)
if dcrdErr != nil {
log.Errorf("%s: could not get dcrd client: %v", funcName, dcrdErr.(error))
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
reqBytes := c.MustGet(requestBytesKey).([]byte)
if cfg.VspClosed {
sendError(errVspClosed, c)
if s.cfg.VspClosed {
s.sendError(errVspClosed, c)
return
}
var request feeAddressRequest
if err := binding.JSON.BindBody(reqBytes, &request); err != nil {
log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
sendErrorWithMsg(err.Error(), errBadRequest, c)
s.sendErrorWithMsg(err.Error(), errBadRequest, c)
return
}
@ -110,7 +110,7 @@ func feeAddress(c *gin.Context) {
ticket.FeeTxStatus == database.FeeConfirmed) {
log.Warnf("%s: Fee tx already received (clientIP=%s, ticketHash=%s)",
funcName, c.ClientIP(), ticket.Hash)
sendError(errFeeAlreadyReceived, c)
s.sendError(errFeeAlreadyReceived, c)
return
}
@ -118,21 +118,21 @@ func feeAddress(c *gin.Context) {
rawTicket, err := dcrdClient.GetRawTransaction(ticketHash)
if err != nil {
log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v", funcName, ticketHash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
// Ensure this ticket is eligible to vote at some point in the future.
canVote, err := dcrdClient.CanTicketVote(rawTicket, ticketHash, cfg.NetParams)
canVote, err := dcrdClient.CanTicketVote(rawTicket, ticketHash, s.cfg.NetParams)
if err != nil {
log.Errorf("%s: dcrd.CanTicketVote error (ticketHash=%s): %v", funcName, ticketHash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
if !canVote {
log.Warnf("%s: Unvotable ticket (clientIP=%s, ticketHash=%s)",
funcName, c.ClientIP(), ticketHash)
sendError(errTicketCannotVote, c)
s.sendError(errTicketCannotVote, c)
return
}
@ -142,26 +142,26 @@ func feeAddress(c *gin.Context) {
// If the expiry period has passed we need to issue a new fee.
now := time.Now()
if ticket.FeeExpired() {
newFee, err := getCurrentFee(dcrdClient)
newFee, err := s.getCurrentFee(dcrdClient)
if err != nil {
log.Errorf("%s: getCurrentFee error (ticketHash=%s): %v", funcName, ticket.Hash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
ticket.FeeExpiration = now.Add(feeAddressExpiration).Unix()
ticket.FeeAmount = int64(newFee)
err = db.UpdateTicket(ticket)
err = s.db.UpdateTicket(ticket)
if err != nil {
log.Errorf("%s: db.UpdateTicket error, failed to update fee expiry (ticketHash=%s): %v",
funcName, ticket.Hash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
log.Debugf("%s: Expired fee updated (newFeeAmt=%s, ticketHash=%s)",
funcName, newFee, ticket.Hash)
}
sendJSONResponse(feeAddressResponse{
s.sendJSONResponse(feeAddressResponse{
Timestamp: now.Unix(),
Request: reqBytes,
FeeAddress: ticket.FeeAddress,
@ -175,17 +175,17 @@ func feeAddress(c *gin.Context) {
// Beyond this point we are processing a new ticket which the VSP has not
// seen before.
fee, err := getCurrentFee(dcrdClient)
fee, err := s.getCurrentFee(dcrdClient)
if err != nil {
log.Errorf("%s: getCurrentFee error (ticketHash=%s): %v", funcName, ticketHash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
newAddress, newAddressIdx, err := getNewFeeAddress(db, addrGen)
newAddress, newAddressIdx, err := s.getNewFeeAddress()
if err != nil {
log.Errorf("%s: getNewFeeAddress error (ticketHash=%s): %v", funcName, ticketHash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
@ -213,10 +213,10 @@ func feeAddress(c *gin.Context) {
FeeTxStatus: database.NoFee,
}
err = db.InsertNewTicket(dbTicket)
err = s.db.InsertNewTicket(dbTicket)
if err != nil {
log.Errorf("%s: db.InsertNewTicket failed (ticketHash=%s): %v", funcName, ticketHash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
@ -224,7 +224,7 @@ func feeAddress(c *gin.Context) {
"feeAddr=%s, feeAmt=%s, ticketHash=%s)",
funcName, confirmed, newAddressIdx, newAddress, fee, ticketHash)
sendJSONResponse(feeAddressResponse{
s.sendJSONResponse(feeAddressResponse{
Timestamp: now.Unix(),
Request: reqBytes,
FeeAddress: newAddress,

View File

@ -15,6 +15,7 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/dcrutil/v4"
"github.com/decred/dcrd/wire"
"github.com/decred/vspd/database"
"github.com/decred/vspd/rpc"
)
@ -105,8 +106,10 @@ func validPolicyOption(policy string) error {
}
}
func validateSignature(hash, commitmentAddress, signature, message string) error {
firstErr := dcrutil.VerifyMessage(commitmentAddress, signature, message, cfg.NetParams)
func validateSignature(hash, commitmentAddress, signature, message string,
db *database.VspDatabase, params *chaincfg.Params) error {
firstErr := dcrutil.VerifyMessage(commitmentAddress, signature, message, params)
if firstErr != nil {
// Don't return an error straight away if sig validation fails -
// first check if we have an alternate sign address for this ticket.
@ -117,7 +120,8 @@ func validateSignature(hash, commitmentAddress, signature, message string) error
// If we have no alternate sign address, or if validating with the
// alt sign addr fails, return an error to the client.
if altSigData == nil || dcrutil.VerifyMessage(altSigData.AltSignAddr, signature, message, cfg.NetParams) != nil {
if altSigData == nil ||
dcrutil.VerifyMessage(altSigData.AltSignAddr, signature, message, params) != nil {
return fmt.Errorf("bad signature")
}
@ -167,7 +171,7 @@ func validateTicketHash(hash string) error {
// getCommitmentAddress gets the commitment address of the provided ticket hash
// from the chain.
func getCommitmentAddress(hash string, dcrdClient *rpc.DcrdRPC) (string, error) {
func getCommitmentAddress(hash string, dcrdClient *rpc.DcrdRPC, params *chaincfg.Params) (string, error) {
var commitmentAddress string
resp, err := dcrdClient.GetRawTransaction(hash)
if err != nil {
@ -187,7 +191,7 @@ func getCommitmentAddress(hash string, dcrdClient *rpc.DcrdRPC) (string, error)
}
addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, cfg.NetParams)
addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, params)
if err != nil {
return commitmentAddress, fmt.Errorf("AddrFromSStxPkScrCommitment error: %v", err)

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 The Decred developers
// Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -10,9 +10,9 @@ import (
"github.com/gin-gonic/gin"
)
func homepage(c *gin.Context) {
func (s *Server) homepage(c *gin.Context) {
c.HTML(http.StatusOK, "homepage.html", gin.H{
"WebApiCache": getCache(),
"WebApiCfg": cfg,
"WebApiCfg": s.cfg,
})
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 The Decred developers
// Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -53,25 +53,23 @@ func withSession(store *sessions.CookieStore) gin.HandlerFunc {
// requireAdmin will only allow the request to proceed if the current session is
// authenticated as an admin, otherwise it will render the login template.
func requireAdmin() gin.HandlerFunc {
return func(c *gin.Context) {
session := c.MustGet(sessionKey).(*sessions.Session)
admin := session.Values["admin"]
func (s *Server) requireAdmin(c *gin.Context) {
session := c.MustGet(sessionKey).(*sessions.Session)
admin := session.Values["admin"]
if admin == nil {
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
"WebApiCache": getCache(),
"WebApiCfg": cfg,
})
c.Abort()
return
}
if admin == nil {
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
"WebApiCache": getCache(),
"WebApiCfg": s.cfg,
})
c.Abort()
return
}
}
// withDcrdClient middleware adds a dcrd client to the request context for
// downstream handlers to make use of.
func withDcrdClient(dcrd rpc.DcrdConnect) gin.HandlerFunc {
func withDcrdClient(dcrd rpc.DcrdConnect, cfg Config) gin.HandlerFunc {
return func(c *gin.Context) {
client, hostname, err := dcrd.Client(c, cfg.NetParams)
// Don't handle the error here, add it to the context and let downstream
@ -85,7 +83,7 @@ func withDcrdClient(dcrd rpc.DcrdConnect) gin.HandlerFunc {
// withWalletClients middleware attempts to add voting wallet clients to the
// request context for downstream handlers to make use of. Downstream handlers
// must handle the case where no wallet clients are connected.
func withWalletClients(wallets rpc.WalletConnect) gin.HandlerFunc {
func withWalletClients(wallets rpc.WalletConnect, cfg Config) gin.HandlerFunc {
return func(c *gin.Context) {
clients, failedConnections := wallets.Clients(c, cfg.NetParams)
if len(clients) == 0 {
@ -116,137 +114,135 @@ func drainAndReplaceBody(req *http.Request) ([]byte, error) {
// Ticket hash, ticket hex, and parent hex are parsed from the request body and
// validated. They are broadcast to the network using SendRawTransaction if dcrd
// is not aware of them.
func broadcastTicket() gin.HandlerFunc {
return func(c *gin.Context) {
const funcName = "broadcastTicket"
func (s *Server) broadcastTicket(c *gin.Context) {
const funcName = "broadcastTicket"
// Read request bytes.
reqBytes, err := drainAndReplaceBody(c.Request)
if err != nil {
log.Warnf("%s: Error reading request (clientIP=%s): %v", funcName, c.ClientIP(), err)
sendErrorWithMsg(err.Error(), errBadRequest, c)
return
}
// Read request bytes.
reqBytes, err := drainAndReplaceBody(c.Request)
if err != nil {
log.Warnf("%s: Error reading request (clientIP=%s): %v", funcName, c.ClientIP(), err)
s.sendErrorWithMsg(err.Error(), errBadRequest, c)
return
}
// Parse request to ensure ticket hash/hex and parent hex are included.
var request struct {
TicketHex string `json:"tickethex" binding:"required"`
TicketHash string `json:"tickethash" binding:"required"`
ParentHex string `json:"parenthex" binding:"required"`
}
if err := binding.JSON.BindBody(reqBytes, &request); err != nil {
log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
sendErrorWithMsg(err.Error(), errBadRequest, c)
return
}
// Parse request to ensure ticket hash/hex and parent hex are included.
var request struct {
TicketHex string `json:"tickethex" binding:"required"`
TicketHash string `json:"tickethash" binding:"required"`
ParentHex string `json:"parenthex" binding:"required"`
}
if err := binding.JSON.BindBody(reqBytes, &request); err != nil {
log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
s.sendErrorWithMsg(err.Error(), errBadRequest, c)
return
}
// Ensure the provided ticket hex is a valid ticket.
msgTx, err := decodeTransaction(request.TicketHex)
if err != nil {
log.Errorf("%s: Failed to decode ticket hex (ticketHash=%s): %v", funcName, request.TicketHash, err)
sendErrorWithMsg("cannot decode ticket hex", errBadRequest, c)
return
}
// Ensure the provided ticket hex is a valid ticket.
msgTx, err := decodeTransaction(request.TicketHex)
if err != nil {
log.Errorf("%s: Failed to decode ticket hex (ticketHash=%s): %v", funcName, request.TicketHash, err)
s.sendErrorWithMsg("cannot decode ticket hex", errBadRequest, c)
return
}
err = isValidTicket(msgTx)
if err != nil {
log.Warnf("%s: Invalid ticket (clientIP=%s, ticketHash=%s): %v",
funcName, c.ClientIP(), request.TicketHash, err)
sendError(errInvalidTicket, c)
return
}
err = isValidTicket(msgTx)
if err != nil {
log.Warnf("%s: Invalid ticket (clientIP=%s, ticketHash=%s): %v",
funcName, c.ClientIP(), request.TicketHash, err)
s.sendError(errInvalidTicket, c)
return
}
// Ensure hex matches hash.
if msgTx.TxHash().String() != request.TicketHash {
log.Warnf("%s: Ticket hex/hash mismatch (clientIP=%s, ticketHash=%s)",
funcName, c.ClientIP(), request.TicketHash)
sendErrorWithMsg("ticket hex does not match hash", errBadRequest, c)
return
}
// Ensure hex matches hash.
if msgTx.TxHash().String() != request.TicketHash {
log.Warnf("%s: Ticket hex/hash mismatch (clientIP=%s, ticketHash=%s)",
funcName, c.ClientIP(), request.TicketHash)
s.sendErrorWithMsg("ticket hex does not match hash", errBadRequest, c)
return
}
// Ensure the provided parent hex is a valid tx.
parentTx, err := decodeTransaction(request.ParentHex)
if err != nil {
log.Errorf("%s: Failed to decode parent hex (ticketHash=%s): %v", funcName, request.TicketHash, err)
sendErrorWithMsg("cannot decode parent hex", errBadRequest, c)
return
}
parentHash := parentTx.TxHash()
// Ensure the provided parent hex is a valid tx.
parentTx, err := decodeTransaction(request.ParentHex)
if err != nil {
log.Errorf("%s: Failed to decode parent hex (ticketHash=%s): %v", funcName, request.TicketHash, err)
s.sendErrorWithMsg("cannot decode parent hex", errBadRequest, c)
return
}
parentHash := parentTx.TxHash()
// Check if local dcrd already knows the parent tx.
dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC)
dcrdErr := c.MustGet(dcrdErrorKey)
if dcrdErr != nil {
log.Errorf("%s: could not get dcrd client: %v", funcName, dcrdErr.(error))
sendError(errInternalError, c)
return
}
// Check if local dcrd already knows the parent tx.
dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC)
dcrdErr := c.MustGet(dcrdErrorKey)
if dcrdErr != nil {
log.Errorf("%s: could not get dcrd client: %v", funcName, dcrdErr.(error))
s.sendError(errInternalError, c)
return
}
_, err = dcrdClient.GetRawTransaction(parentHash.String())
var e *wsrpc.Error
if err == nil {
// No error means dcrd already knows the parent tx, nothing to do.
} else if errors.As(err, &e) && e.Code == rpc.ErrNoTxInfo {
// ErrNoTxInfo means local dcrd is not aware of the parent. We have
// the hex, so we can broadcast it here.
_, err = dcrdClient.GetRawTransaction(parentHash.String())
var e *wsrpc.Error
if err == nil {
// No error means dcrd already knows the parent tx, nothing to do.
} else if errors.As(err, &e) && e.Code == rpc.ErrNoTxInfo {
// ErrNoTxInfo means local dcrd is not aware of the parent. We have
// the hex, so we can broadcast it here.
// Before broadcasting, check that the provided parent hex is
// actually the parent of the ticket.
var found bool
for _, txIn := range msgTx.TxIn {
if !txIn.PreviousOutPoint.Hash.IsEqual(&parentHash) {
continue
}
found = true
break
// Before broadcasting, check that the provided parent hex is
// actually the parent of the ticket.
var found bool
for _, txIn := range msgTx.TxIn {
if !txIn.PreviousOutPoint.Hash.IsEqual(&parentHash) {
continue
}
found = true
break
}
if !found {
log.Errorf("%s: Invalid ticket parent (ticketHash=%s)", funcName, request.TicketHash)
sendErrorWithMsg("invalid ticket parent", errBadRequest, c)
return
}
if !found {
log.Errorf("%s: Invalid ticket parent (ticketHash=%s)", funcName, request.TicketHash)
s.sendErrorWithMsg("invalid ticket parent", errBadRequest, c)
return
}
log.Debugf("%s: Broadcasting parent tx %s (ticketHash=%s)", funcName, parentHash, request.TicketHash)
err = dcrdClient.SendRawTransaction(request.ParentHex)
if err != nil {
log.Errorf("%s: dcrd.SendRawTransaction for parent tx failed (ticketHash=%s): %v",
funcName, request.TicketHash, err)
sendError(errCannotBroadcastTicket, c)
return
}
} else {
log.Errorf("%s: dcrd.GetRawTransaction for ticket parent failed (ticketHash=%s): %v",
log.Debugf("%s: Broadcasting parent tx %s (ticketHash=%s)", funcName, parentHash, request.TicketHash)
err = dcrdClient.SendRawTransaction(request.ParentHex)
if err != nil {
log.Errorf("%s: dcrd.SendRawTransaction for parent tx failed (ticketHash=%s): %v",
funcName, request.TicketHash, err)
sendError(errInternalError, c)
s.sendError(errCannotBroadcastTicket, c)
return
}
// Check if local dcrd already knows the ticket.
_, err = dcrdClient.GetRawTransaction(request.TicketHash)
if err == nil {
// No error means dcrd already knows the ticket, we are done here.
return
}
} else {
log.Errorf("%s: dcrd.GetRawTransaction for ticket parent failed (ticketHash=%s): %v",
funcName, request.TicketHash, err)
s.sendError(errInternalError, c)
return
}
// ErrNoTxInfo means local dcrd is not aware of the ticket. We have the
// hex, so we can broadcast it here.
if errors.As(err, &e) && e.Code == rpc.ErrNoTxInfo {
log.Debugf("%s: Broadcasting ticket (ticketHash=%s)", funcName, request.TicketHash)
err = dcrdClient.SendRawTransaction(request.TicketHex)
if err != nil {
log.Errorf("%s: dcrd.SendRawTransaction for ticket failed (ticketHash=%s): %v",
funcName, request.TicketHash, err)
sendError(errCannotBroadcastTicket, c)
return
}
} else {
log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v",
// Check if local dcrd already knows the ticket.
_, err = dcrdClient.GetRawTransaction(request.TicketHash)
if err == nil {
// No error means dcrd already knows the ticket, we are done here.
return
}
// ErrNoTxInfo means local dcrd is not aware of the ticket. We have the
// hex, so we can broadcast it here.
if errors.As(err, &e) && e.Code == rpc.ErrNoTxInfo {
log.Debugf("%s: Broadcasting ticket (ticketHash=%s)", funcName, request.TicketHash)
err = dcrdClient.SendRawTransaction(request.TicketHex)
if err != nil {
log.Errorf("%s: dcrd.SendRawTransaction for ticket failed (ticketHash=%s): %v",
funcName, request.TicketHash, err)
sendError(errInternalError, c)
s.sendError(errCannotBroadcastTicket, c)
return
}
} else {
log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v",
funcName, request.TicketHash, err)
s.sendError(errInternalError, c)
return
}
}
@ -257,97 +253,94 @@ func broadcastTicket() gin.HandlerFunc {
// does not contain the request body signed with the commitment address.
// Ticket information is added to the request context for downstream handlers to
// use.
func vspAuth() gin.HandlerFunc {
return func(c *gin.Context) {
const funcName = "vspAuth"
func (s *Server) vspAuth(c *gin.Context) {
const funcName = "vspAuth"
// Read request bytes.
reqBytes, err := drainAndReplaceBody(c.Request)
if err != nil {
log.Warnf("%s: Error reading request (clientIP=%s): %v", funcName, c.ClientIP(), err)
sendErrorWithMsg(err.Error(), errBadRequest, c)
return
}
// Add request bytes to request context for downstream handlers to reuse.
// Necessary because the request body reader can only be used once.
c.Set(requestBytesKey, reqBytes)
// Parse request and ensure there is a ticket hash included.
var request struct {
TicketHash string `json:"tickethash" binding:"required"`
}
if err := binding.JSON.BindBody(reqBytes, &request); err != nil {
log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
sendErrorWithMsg(err.Error(), errBadRequest, c)
return
}
hash := request.TicketHash
// Before hitting the db or any RPC, ensure this is a valid ticket hash.
err = validateTicketHash(hash)
if err != nil {
log.Errorf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
sendErrorWithMsg("invalid ticket hash", errBadRequest, c)
return
}
// Check if this ticket already appears in the database.
ticket, ticketFound, err := db.GetTicketByHash(hash)
if err != nil {
log.Errorf("%s: db.GetTicketByHash error (ticketHash=%s): %v", funcName, hash, err)
sendError(errInternalError, c)
return
}
// If the ticket was found in the database, we already know its
// commitment address. Otherwise we need to get it from the chain.
var commitmentAddress string
dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC)
dcrdErr := c.MustGet(dcrdErrorKey)
if dcrdErr != nil {
log.Errorf("%s: could not get dcrd client: %v", funcName, dcrdErr.(error))
sendError(errInternalError, c)
return
}
if ticketFound {
commitmentAddress = ticket.CommitmentAddress
} else {
commitmentAddress, err = getCommitmentAddress(hash, dcrdClient)
if err != nil {
var apiErr *apiError
if errors.Is(err, apiErr) {
sendError(errInvalidTicket, c)
} else {
sendError(errInternalError, c)
}
log.Errorf("%s: (clientIP: %s, ticketHash: %s): %v", funcName, c.ClientIP(), hash, err)
return
}
}
// Ensure a signature is provided.
signature := c.GetHeader("VSP-Client-Signature")
if signature == "" {
log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
sendErrorWithMsg("no VSP-Client-Signature header", errBadRequest, c)
return
}
// Validate request signature to ensure ticket ownership.
err = validateSignature(hash, commitmentAddress, signature, string(reqBytes))
if err != nil {
log.Errorf("%s: Bad signature (clientIP=%s, ticketHash=%s): %v", funcName, err)
sendError(errBadSignature, c)
return
}
// Add ticket information to context so downstream handlers don't need
// to access the db for it.
c.Set(ticketKey, ticket)
c.Set(knownTicketKey, ticketFound)
c.Set(commitmentAddressKey, commitmentAddress)
// Read request bytes.
reqBytes, err := drainAndReplaceBody(c.Request)
if err != nil {
log.Warnf("%s: Error reading request (clientIP=%s): %v", funcName, c.ClientIP(), err)
s.sendErrorWithMsg(err.Error(), errBadRequest, c)
return
}
// Add request bytes to request context for downstream handlers to reuse.
// Necessary because the request body reader can only be used once.
c.Set(requestBytesKey, reqBytes)
// Parse request and ensure there is a ticket hash included.
var request struct {
TicketHash string `json:"tickethash" binding:"required"`
}
if err := binding.JSON.BindBody(reqBytes, &request); err != nil {
log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
s.sendErrorWithMsg(err.Error(), errBadRequest, c)
return
}
hash := request.TicketHash
// Before hitting the db or any RPC, ensure this is a valid ticket hash.
err = validateTicketHash(hash)
if err != nil {
log.Errorf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
s.sendErrorWithMsg("invalid ticket hash", errBadRequest, c)
return
}
// Check if this ticket already appears in the database.
ticket, ticketFound, err := s.db.GetTicketByHash(hash)
if err != nil {
log.Errorf("%s: db.GetTicketByHash error (ticketHash=%s): %v", funcName, hash, err)
s.sendError(errInternalError, c)
return
}
// If the ticket was found in the database, we already know its
// commitment address. Otherwise we need to get it from the chain.
var commitmentAddress string
dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC)
dcrdErr := c.MustGet(dcrdErrorKey)
if dcrdErr != nil {
log.Errorf("%s: could not get dcrd client: %v", funcName, dcrdErr.(error))
s.sendError(errInternalError, c)
return
}
if ticketFound {
commitmentAddress = ticket.CommitmentAddress
} else {
commitmentAddress, err = getCommitmentAddress(hash, dcrdClient, s.cfg.NetParams)
if err != nil {
var apiErr *apiError
if errors.Is(err, apiErr) {
s.sendError(errInvalidTicket, c)
} else {
s.sendError(errInternalError, c)
}
log.Errorf("%s: (clientIP: %s, ticketHash: %s): %v", funcName, c.ClientIP(), hash, err)
return
}
}
// Ensure a signature is provided.
signature := c.GetHeader("VSP-Client-Signature")
if signature == "" {
log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
s.sendErrorWithMsg("no VSP-Client-Signature header", errBadRequest, c)
return
}
// Validate request signature to ensure ticket ownership.
err = validateSignature(hash, commitmentAddress, signature, string(reqBytes), s.db, s.cfg.NetParams)
if err != nil {
log.Errorf("%s: Bad signature (clientIP=%s, ticketHash=%s): %v", funcName, err)
s.sendError(errBadSignature, c)
return
}
// Add ticket information to context so downstream handlers don't need
// to access the db for it.
c.Set(ticketKey, ticket)
c.Set(knownTicketKey, ticketFound)
c.Set(commitmentAddressKey, commitmentAddress)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 The Decred developers
// Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -20,7 +20,7 @@ import (
)
// payFee is the handler for "POST /api/v3/payfee".
func payFee(c *gin.Context) {
func (s *Server) payFee(c *gin.Context) {
const funcName = "payFee"
// Get values which have been added to context by middleware.
@ -30,26 +30,26 @@ func payFee(c *gin.Context) {
dcrdErr := c.MustGet(dcrdErrorKey)
if dcrdErr != nil {
log.Errorf("%s: could not get dcrd client: %v", funcName, dcrdErr.(error))
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
reqBytes := c.MustGet(requestBytesKey).([]byte)
if cfg.VspClosed {
sendError(errVspClosed, c)
if s.cfg.VspClosed {
s.sendError(errVspClosed, c)
return
}
if !knownTicket {
log.Warnf("%s: Unknown ticket (clientIP=%s)", funcName, c.ClientIP())
sendError(errUnknownTicket, c)
s.sendError(errUnknownTicket, c)
return
}
var request payFeeRequest
if err := binding.JSON.BindBody(reqBytes, &request); err != nil {
log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
sendErrorWithMsg(err.Error(), errBadRequest, c)
s.sendErrorWithMsg(err.Error(), errBadRequest, c)
return
}
@ -59,7 +59,7 @@ func payFee(c *gin.Context) {
ticket.FeeTxStatus == database.FeeConfirmed {
log.Warnf("%s: Fee tx already received (clientIP=%s, ticketHash=%s)",
funcName, c.ClientIP(), ticket.Hash)
sendError(errFeeAlreadyReceived, c)
s.sendError(errFeeAlreadyReceived, c)
return
}
@ -67,21 +67,21 @@ func payFee(c *gin.Context) {
rawTicket, err := dcrdClient.GetRawTransaction(ticket.Hash)
if err != nil {
log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v", funcName, ticket.Hash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
// Ensure this ticket is eligible to vote at some point in the future.
canVote, err := dcrdClient.CanTicketVote(rawTicket, ticket.Hash, cfg.NetParams)
canVote, err := dcrdClient.CanTicketVote(rawTicket, ticket.Hash, s.cfg.NetParams)
if err != nil {
log.Errorf("%s: dcrd.CanTicketVote error (ticketHash=%s): %v", funcName, ticket.Hash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
if !canVote {
log.Warnf("%s: Unvotable ticket (clientIP=%s, ticketHash=%s)",
funcName, c.ClientIP(), ticket.Hash)
sendError(errTicketCannotVote, c)
s.sendError(errTicketCannotVote, c)
return
}
@ -89,17 +89,17 @@ func payFee(c *gin.Context) {
if ticket.FeeExpired() {
log.Warnf("%s: Expired payfee request (clientIP=%s, ticketHash=%s)",
funcName, c.ClientIP(), ticket.Hash)
sendError(errFeeExpired, c)
s.sendError(errFeeExpired, c)
return
}
// Validate VotingKey.
votingKey := request.VotingKey
votingWIF, err := dcrutil.DecodeWIF(votingKey, cfg.NetParams.PrivateKeyID)
votingWIF, err := dcrutil.DecodeWIF(votingKey, s.cfg.NetParams.PrivateKeyID)
if err != nil {
log.Warnf("%s: Failed to decode WIF (clientIP=%s, ticketHash=%s): %v",
funcName, c.ClientIP(), ticket.Hash, err)
sendError(errInvalidPrivKey, c)
s.sendError(errInvalidPrivKey, c)
return
}
@ -107,7 +107,7 @@ func payFee(c *gin.Context) {
// the ticket should still be registered.
validVoteChoices := true
err = validConsensusVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), request.VoteChoices)
err = validConsensusVoteChoices(s.cfg.NetParams, currentVoteVersion(s.cfg.NetParams), request.VoteChoices)
if err != nil {
validVoteChoices = false
log.Warnf("%s: Invalid consensus vote choices (clientIP=%s, ticketHash=%s): %v",
@ -135,24 +135,24 @@ func payFee(c *gin.Context) {
if err != nil {
log.Warnf("%s: Failed to decode fee tx hex (clientIP=%s, ticketHash=%s): %v",
funcName, c.ClientIP(), ticket.Hash, err)
sendError(errInvalidFeeTx, c)
s.sendError(errInvalidFeeTx, c)
return
}
err = blockchain.CheckTransactionSanity(feeTx, cfg.NetParams)
err = blockchain.CheckTransactionSanity(feeTx, s.cfg.NetParams)
if err != nil {
log.Warnf("%s: Fee tx failed sanity check (clientIP=%s, ticketHash=%s): %v",
funcName, c.ClientIP(), ticket.Hash, err)
sendError(errInvalidFeeTx, c)
s.sendError(errInvalidFeeTx, c)
return
}
// Decode fee address to get its payment script details.
feeAddr, err := stdaddr.DecodeAddress(ticket.FeeAddress, cfg.NetParams)
feeAddr, err := stdaddr.DecodeAddress(ticket.FeeAddress, s.cfg.NetParams)
if err != nil {
log.Errorf("%s: Failed to decode fee address (ticketHash=%s): %v",
funcName, ticket.Hash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
@ -172,7 +172,7 @@ func payFee(c *gin.Context) {
if feePaid == 0 {
log.Warnf("%s: Fee tx did not include expected payment (ticketHash=%s, feeAddress=%s, clientIP=%s)",
funcName, ticket.Hash, ticket.FeeAddress, c.ClientIP())
sendErrorWithMsg(
s.sendErrorWithMsg(
fmt.Sprintf("feetx did not include any payments for fee address %s", ticket.FeeAddress),
errInvalidFeeTx, c)
return
@ -183,17 +183,17 @@ func payFee(c *gin.Context) {
if feePaid < minFee {
log.Warnf("%s: Fee too small (ticketHash=%s, clientIP=%s): was %s, expected minimum %s",
funcName, ticket.Hash, c.ClientIP(), feePaid, minFee)
sendError(errFeeTooSmall, c)
s.sendError(errFeeTooSmall, c)
return
}
// Decode the provided voting WIF to get its voting rights script.
pkHash := stdaddr.Hash160(votingWIF.PubKey())
wifAddr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(pkHash, cfg.NetParams)
wifAddr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(pkHash, s.cfg.NetParams)
if err != nil {
log.Errorf("%s: Failed to get voting address from WIF (ticketHash=%s, clientIP=%s): %v",
funcName, ticket.Hash, c.ClientIP(), err)
sendError(errInvalidPrivKey, c)
s.sendError(errInvalidPrivKey, c)
return
}
@ -204,7 +204,7 @@ func payFee(c *gin.Context) {
if err != nil {
log.Warnf("%s: Failed to decode ticket hex (ticketHash=%s): %v",
funcName, ticket.Hash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
@ -216,7 +216,7 @@ func payFee(c *gin.Context) {
if actualScriptVer != wantScriptVer || !bytes.Equal(actualScript, wantScript) {
log.Warnf("%s: Voting address does not match provided private key: (ticketHash=%s)",
funcName, ticket.Hash)
sendErrorWithMsg("voting address does not match provided private key",
s.sendErrorWithMsg("voting address does not match provided private key",
errInvalidPrivKey, c)
return
}
@ -242,11 +242,11 @@ func payFee(c *gin.Context) {
ticket.TreasuryPolicy = request.TreasuryPolicy
}
err = db.UpdateTicket(ticket)
err = s.db.UpdateTicket(ticket)
if err != nil {
log.Errorf("%s: db.UpdateTicket error, failed to set fee tx (ticketHash=%s): %v",
funcName, ticket.Hash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
@ -263,12 +263,12 @@ func payFee(c *gin.Context) {
if strings.Contains(err.Error(),
"references outputs of unknown or fully-spent transaction") {
sendError(errCannotBroadcastFeeUnknownOutputs, c)
s.sendError(errCannotBroadcastFeeUnknownOutputs, c)
} else {
sendError(errCannotBroadcastFee, c)
s.sendError(errCannotBroadcastFee, c)
}
err = db.UpdateTicket(ticket)
err = s.db.UpdateTicket(ticket)
if err != nil {
log.Errorf("%s: db.UpdateTicket error, failed to set fee tx error (ticketHash=%s): %v",
funcName, ticket.Hash, err)
@ -279,11 +279,11 @@ func payFee(c *gin.Context) {
ticket.FeeTxStatus = database.FeeBroadcast
err = db.UpdateTicket(ticket)
err = s.db.UpdateTicket(ticket)
if err != nil {
log.Errorf("%s: db.UpdateTicket error, failed to set fee tx as broadcast (ticketHash=%s): %v",
funcName, ticket.Hash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
@ -292,13 +292,13 @@ func payFee(c *gin.Context) {
}
// Send success response to client.
resp, respSig := sendJSONResponse(payFeeResponse{
resp, respSig := s.sendJSONResponse(payFeeResponse{
Timestamp: time.Now().Unix(),
Request: reqBytes,
}, c)
// Store a record of the vote choice change.
err = db.SaveVoteChange(
err = s.db.SaveVoteChange(
ticket.Hash,
database.VoteChangeRecord{
Request: string(reqBytes),

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 The Decred developers
// Copyright (c) 2021-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -26,7 +26,7 @@ type Node interface {
}
// setAltSignAddr is the handler for "POST /api/v3/setaltsignaddr".
func setAltSignAddr(c *gin.Context) {
func (s *Server) setAltSignAddr(c *gin.Context) {
const funcName = "setAltSignAddr"
@ -35,49 +35,49 @@ func setAltSignAddr(c *gin.Context) {
dcrdErr := c.MustGet(dcrdErrorKey)
if dcrdErr != nil {
log.Errorf("%s: could not get dcrd client: %v", funcName, dcrdErr.(error))
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
reqBytes := c.MustGet(requestBytesKey).([]byte)
if cfg.VspClosed {
sendError(errVspClosed, c)
if s.cfg.VspClosed {
s.sendError(errVspClosed, c)
return
}
var request setAltSignAddrRequest
if err := binding.JSON.BindBody(reqBytes, &request); err != nil {
log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
sendErrorWithMsg(err.Error(), errBadRequest, c)
s.sendErrorWithMsg(err.Error(), errBadRequest, c)
return
}
altSignAddr, ticketHash := request.AltSignAddress, request.TicketHash
currentData, err := db.AltSignAddrData(ticketHash)
currentData, err := s.db.AltSignAddrData(ticketHash)
if err != nil {
log.Errorf("%s: db.AltSignAddrData (ticketHash=%s): %v", funcName, ticketHash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
if currentData != nil {
msg := "alternate sign address data already exists"
log.Warnf("%s: %s (ticketHash=%s)", funcName, msg, ticketHash)
sendErrorWithMsg(msg, errBadRequest, c)
s.sendErrorWithMsg(msg, errBadRequest, c)
return
}
// Fail fast if the pubkey doesn't decode properly.
addr, err := stdaddr.DecodeAddressV0(altSignAddr, cfg.NetParams)
addr, err := stdaddr.DecodeAddressV0(altSignAddr, s.cfg.NetParams)
if err != nil {
log.Warnf("%s: Alt sign address cannot be decoded (clientIP=%s): %v", funcName, c.ClientIP(), err)
sendErrorWithMsg(err.Error(), errBadRequest, c)
s.sendErrorWithMsg(err.Error(), errBadRequest, c)
return
}
if _, ok := addr.(*stdaddr.AddressPubKeyHashEcdsaSecp256k1V0); !ok {
log.Warnf("%s: Alt sign address is unexpected type (clientIP=%s, type=%T)", funcName, c.ClientIP(), addr)
sendErrorWithMsg("wrong type for alternate signing address", errBadRequest, c)
s.sendErrorWithMsg("wrong type for alternate signing address", errBadRequest, c)
return
}
@ -85,26 +85,26 @@ func setAltSignAddr(c *gin.Context) {
rawTicket, err := dcrdClient.GetRawTransaction(ticketHash)
if err != nil {
log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v", funcName, ticketHash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
// Ensure this ticket is eligible to vote at some point in the future.
canVote, err := dcrdClient.CanTicketVote(rawTicket, ticketHash, cfg.NetParams)
canVote, err := dcrdClient.CanTicketVote(rawTicket, ticketHash, s.cfg.NetParams)
if err != nil {
log.Errorf("%s: dcrd.CanTicketVote error (ticketHash=%s): %v", funcName, ticketHash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
if !canVote {
log.Warnf("%s: unvotable ticket (clientIP=%s, ticketHash=%s)",
funcName, c.ClientIP(), ticketHash)
sendError(errTicketCannotVote, c)
s.sendError(errTicketCannotVote, c)
return
}
// Send success response to client.
resp, respSig := sendJSONResponse(setAltSignAddrResponse{
resp, respSig := s.sendJSONResponse(setAltSignAddrResponse{
Timestamp: time.Now().Unix(),
Request: reqBytes,
}, c)
@ -117,7 +117,7 @@ func setAltSignAddr(c *gin.Context) {
RespSig: respSig,
}
err = db.InsertAltSignAddr(ticketHash, data)
err = s.db.InsertAltSignAddr(ticketHash, data)
if err != nil {
log.Errorf("%s: db.InsertAltSignAddr error (ticketHash=%s): %v",
funcName, ticketHash, err)

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 The Decred developers
// Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -40,6 +40,7 @@ var (
seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
feeXPub = "feexpub"
maxVoteChangeRecords = 3
api *Server
)
// randBytes returns a byte slice of size n filled with random bytes.
@ -58,8 +59,10 @@ func TestMain(m *testing.M) {
log.SetLevel(slog.LevelTrace)
// Set up some global params.
cfg.NetParams = chaincfg.MainNetParams()
_, signPrivKey, _ = ed25519.GenerateKey(seededRand)
cfg := Config{
NetParams: chaincfg.MainNetParams(),
}
_, signPrivKey, _ := ed25519.GenerateKey(seededRand)
// Create a database to use.
// Ensure we are starting with a clean environment.
@ -74,11 +77,17 @@ func TestMain(m *testing.M) {
if err != nil {
panic(fmt.Errorf("error creating test database: %w", err))
}
db, err = database.Open(ctx, &wg, testDb, time.Hour, maxVoteChangeRecords)
db, err := database.Open(ctx, &wg, testDb, time.Hour, maxVoteChangeRecords)
if err != nil {
panic(fmt.Errorf("error opening test database: %w", err))
}
api = &Server{
cfg: cfg,
signPrivKey: signPrivKey,
db: db,
}
// Run tests.
exitCode := m.Run()
@ -202,12 +211,12 @@ func TestSetAltSignAddress(t *testing.T) {
Resp: string(randBytes(1000)),
RespSig: randString(96, sigCharset),
}
if err := db.InsertAltSignAddr(ticketHash, data); err != nil {
if err := api.db.InsertAltSignAddr(ticketHash, data); err != nil {
t.Fatalf("%q: unable to insert alt sign addr: %v", test.name, err)
}
}
cfg.VspClosed = test.vspClosed
api.cfg.VspClosed = test.vspClosed
tNode := &testNode{
canTicketVote: !test.canTicketNotVote,
@ -227,7 +236,7 @@ func TestSetAltSignAddress(t *testing.T) {
c.Set(dcrdKey, tNode)
c.Set(dcrdErrorKey, dcrdErr)
c.Set(requestBytesKey, b[test.deformReq:])
setAltSignAddr(c)
api.setAltSignAddr(c)
}
r.POST("/", handle)
@ -245,7 +254,7 @@ func TestSetAltSignAddress(t *testing.T) {
t.Errorf("%q: expected status %d, got %d", test.name, test.wantCode, w.Code)
}
altsig, err := db.AltSignAddrData(ticketHash)
altsig, err := api.db.AltSignAddrData(ticketHash)
if err != nil {
t.Fatalf("%q: unable to get alt sign addr data: %v", test.name, err)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 The Decred developers
// Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -16,7 +16,7 @@ import (
)
// setVoteChoices is the handler for "POST /api/v3/setvotechoices".
func setVoteChoices(c *gin.Context) {
func (s *Server) setVoteChoices(c *gin.Context) {
const funcName = "setVoteChoices"
// Get values which have been added to context by middleware.
@ -28,20 +28,20 @@ func setVoteChoices(c *gin.Context) {
// If we cannot set the vote choices on at least one voting wallet right
// now, don't update the database, just return an error.
if len(walletClients) == 0 {
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
if !knownTicket {
log.Warnf("%s: Unknown ticket (clientIP=%s)", funcName, c.ClientIP())
sendError(errUnknownTicket, c)
s.sendError(errUnknownTicket, c)
return
}
if ticket.FeeTxStatus == database.NoFee {
log.Warnf("%s: No fee tx for ticket (clientIP=%s, ticketHash=%s)",
funcName, c.ClientIP(), ticket.Hash)
sendError(errFeeNotReceived, c)
s.sendError(errFeeNotReceived, c)
return
}
@ -49,7 +49,7 @@ func setVoteChoices(c *gin.Context) {
if ticket.Outcome != "" {
log.Warnf("%s: Ticket not eligible to vote (clientIP=%s, ticketHash=%s)",
funcName, c.ClientIP(), ticket.Hash)
sendErrorWithMsg(fmt.Sprintf("ticket not eligible to vote (status=%s)", ticket.Outcome),
s.sendErrorWithMsg(fmt.Sprintf("ticket not eligible to vote (status=%s)", ticket.Outcome),
errTicketCannotVote, c)
return
}
@ -57,17 +57,17 @@ func setVoteChoices(c *gin.Context) {
var request setVoteChoicesRequest
if err := binding.JSON.BindBody(reqBytes, &request); err != nil {
log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
sendErrorWithMsg(err.Error(), errBadRequest, c)
s.sendErrorWithMsg(err.Error(), errBadRequest, c)
return
}
// Return an error if this request has a timestamp older than any previous
// vote change requests. This is to prevent requests from being replayed.
previousChanges, err := db.GetVoteChanges(ticket.Hash)
previousChanges, err := s.db.GetVoteChanges(ticket.Hash)
if err != nil {
log.Errorf("%s: db.GetVoteChanges error (ticketHash=%s): %v",
funcName, ticket.Hash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
@ -79,7 +79,7 @@ func setVoteChoices(c *gin.Context) {
if err != nil {
log.Errorf("%s: Could not unmarshal vote change record (ticketHash=%s): %v",
funcName, ticket.Hash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
@ -87,18 +87,18 @@ func setVoteChoices(c *gin.Context) {
log.Warnf("%s: Request uses invalid timestamp, %d is not greater "+
"than %d (ticketHash=%s)",
funcName, request.Timestamp, prevReq.Timestamp, ticket.Hash)
sendError(errInvalidTimestamp, c)
s.sendError(errInvalidTimestamp, c)
return
}
}
// Validate vote choices (consensus, tspend policy and treasury policy).
err = validConsensusVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), request.VoteChoices)
err = validConsensusVoteChoices(s.cfg.NetParams, currentVoteVersion(s.cfg.NetParams), request.VoteChoices)
if err != nil {
log.Warnf("%s: Invalid consensus vote choices (clientIP=%s, ticketHash=%s): %v",
funcName, c.ClientIP(), ticket.Hash, err)
sendErrorWithMsg(err.Error(), errInvalidVoteChoices, c)
s.sendErrorWithMsg(err.Error(), errInvalidVoteChoices, c)
return
}
@ -106,14 +106,14 @@ func setVoteChoices(c *gin.Context) {
if err != nil {
log.Warnf("%s: Invalid treasury policy (clientIP=%s, ticketHash=%s): %v",
funcName, c.ClientIP(), ticket.Hash, err)
sendErrorWithMsg(err.Error(), errInvalidVoteChoices, c)
s.sendErrorWithMsg(err.Error(), errInvalidVoteChoices, c)
}
err = validTSpendPolicy(request.TSpendPolicy)
if err != nil {
log.Warnf("%s: Invalid tspend policy (clientIP=%s, ticketHash=%s): %v",
funcName, c.ClientIP(), ticket.Hash, err)
sendErrorWithMsg(err.Error(), errInvalidVoteChoices, c)
s.sendErrorWithMsg(err.Error(), errInvalidVoteChoices, c)
}
// Update voting preferences in the database before updating the wallets. DB
@ -131,11 +131,11 @@ func setVoteChoices(c *gin.Context) {
ticket.TreasuryPolicy[newTreasuryKey] = newChoice
}
err = db.UpdateTicket(ticket)
err = s.db.UpdateTicket(ticket)
if err != nil {
log.Errorf("%s: db.UpdateTicket error, failed to set vote choices (ticketHash=%s): %v",
log.Errorf("%s: db.UpdateTicket error, failed to set consensus vote choices (ticketHash=%s): %v",
funcName, ticket.Hash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
@ -179,13 +179,13 @@ func setVoteChoices(c *gin.Context) {
log.Debugf("%s: Vote choices updated (ticketHash=%s)", funcName, ticket.Hash)
// Send success response to client.
resp, respSig := sendJSONResponse(setVoteChoicesResponse{
resp, respSig := s.sendJSONResponse(setVoteChoicesResponse{
Timestamp: time.Now().Unix(),
Request: reqBytes,
}, c)
// Store a record of the vote choice change.
err = db.SaveVoteChange(
err = s.db.SaveVoteChange(
ticket.Hash,
database.VoteChangeRecord{
Request: string(reqBytes),

View File

@ -13,7 +13,7 @@ import (
)
// ticketStatus is the handler for "POST /api/v3/ticketstatus".
func ticketStatus(c *gin.Context) {
func (s *Server) ticketStatus(c *gin.Context) {
const funcName = "ticketStatus"
// Get values which have been added to context by middleware.
@ -23,22 +23,22 @@ func ticketStatus(c *gin.Context) {
if !knownTicket {
log.Warnf("%s: Unknown ticket (clientIP=%s)", funcName, c.ClientIP())
sendError(errUnknownTicket, c)
s.sendError(errUnknownTicket, c)
return
}
var request ticketStatusRequest
if err := binding.JSON.BindBody(reqBytes, &request); err != nil {
log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
sendErrorWithMsg(err.Error(), errBadRequest, c)
s.sendErrorWithMsg(err.Error(), errBadRequest, c)
return
}
// Get altSignAddress from database
altSignAddrData, err := db.AltSignAddrData(ticket.Hash)
altSignAddrData, err := s.db.AltSignAddrData(ticket.Hash)
if err != nil {
log.Errorf("%s: db.AltSignAddrData error (ticketHash=%s): %v", funcName, ticket.Hash, err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return
}
@ -47,7 +47,7 @@ func ticketStatus(c *gin.Context) {
altSignAddr = altSignAddrData.AltSignAddr
}
sendJSONResponse(ticketStatusResponse{
s.sendJSONResponse(ticketStatusResponse{
Timestamp: time.Now().Unix(),
Request: reqBytes,
TicketConfirmed: ticket.Confirmed,

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 The Decred developers
// Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -12,16 +12,16 @@ import (
)
// vspInfo is the handler for "GET /api/v3/vspinfo".
func vspInfo(c *gin.Context) {
func (s *Server) vspInfo(c *gin.Context) {
cachedStats := getCache()
sendJSONResponse(vspInfoResponse{
s.sendJSONResponse(vspInfoResponse{
APIVersions: []int64{3},
Timestamp: time.Now().Unix(),
PubKey: signPubKey,
FeePercentage: cfg.VSPFee,
Network: cfg.NetParams.Name,
VspClosed: cfg.VspClosed,
VspClosedMsg: cfg.VspClosedMsg,
PubKey: s.signPubKey,
FeePercentage: s.cfg.VSPFee,
Network: s.cfg.NetParams.Name,
VspClosed: s.cfg.VspClosed,
VspClosedMsg: s.cfg.VspClosedMsg,
VspdVersion: version.String(),
Voting: cachedStats.Voting,
Voted: cachedStats.Voted,

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 The Decred developers
// Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -63,28 +63,32 @@ const (
commitmentAddressKey = "CommitmentAddress"
)
var cfg Config
var db *database.VspDatabase
var addrGen *addressGenerator
var signPrivKey ed25519.PrivateKey
var signPubKey ed25519.PublicKey
type Server struct {
cfg Config
db *database.VspDatabase
addrGen *addressGenerator
signPrivKey ed25519.PrivateKey
signPubKey ed25519.PublicKey
}
func Start(ctx context.Context, requestShutdown func(), shutdownWg *sync.WaitGroup,
listen string, vdb *database.VspDatabase, dcrd rpc.DcrdConnect, wallets rpc.WalletConnect, config Config) error {
cfg = config
db = vdb
s := &Server{
cfg: config,
db: vdb,
}
var err error
// Get keys for signing API responses from the database.
signPrivKey, signPubKey, err = vdb.KeyPair()
s.signPrivKey, s.signPubKey, err = vdb.KeyPair()
if err != nil {
return fmt.Errorf("db.Keypair error: %w", err)
}
// Populate cached VSP stats before starting webserver.
initCache()
initCache(base64.StdEncoding.EncodeToString(s.signPubKey))
err = updateCache(ctx, vdb, dcrd, config.NetParams, wallets)
if err != nil {
log.Errorf("Could not initialize VSP stats cache: %v", err)
@ -100,7 +104,7 @@ func Start(ctx context.Context, requestShutdown func(), shutdownWg *sync.WaitGro
if err != nil {
return fmt.Errorf("db.GetFeeXPub error: %w", err)
}
addrGen, err = newAddressGenerator(feeXPub, config.NetParams, idx)
s.addrGen, err = newAddressGenerator(feeXPub, config.NetParams, idx)
if err != nil {
return fmt.Errorf("failed to initialize fee address generator: %w", err)
}
@ -120,7 +124,7 @@ func Start(ctx context.Context, requestShutdown func(), shutdownWg *sync.WaitGro
log.Infof("Listening on %s", listen)
srv := http.Server{
Handler: router(cfg.Debug, cookieSecret, dcrd, wallets),
Handler: s.router(cookieSecret, dcrd, wallets),
ReadTimeout: 5 * time.Second, // slow requests should not hold connections opened
WriteTimeout: 60 * time.Second, // hung responses must die
}
@ -157,7 +161,7 @@ func Start(ctx context.Context, requestShutdown func(), shutdownWg *sync.WaitGro
// Use a ticker to update cached VSP stats.
var refresh time.Duration
if cfg.Debug {
if s.cfg.Debug {
refresh = 1 * time.Second
} else {
refresh = 1 * time.Minute
@ -183,10 +187,10 @@ func Start(ctx context.Context, requestShutdown func(), shutdownWg *sync.WaitGro
return nil
}
func router(debugMode bool, cookieSecret []byte, dcrd rpc.DcrdConnect, wallets rpc.WalletConnect) *gin.Engine {
func (s *Server) router(cookieSecret []byte, dcrd rpc.DcrdConnect, wallets rpc.WalletConnect) *gin.Engine {
// With release mode enabled, gin will only read template files once and cache them.
// With release mode disabled, templates will be reloaded on the fly.
if !debugMode {
if !s.cfg.Debug {
gin.SetMode(gin.ReleaseMode)
}
@ -194,9 +198,9 @@ func router(debugMode bool, cookieSecret []byte, dcrd rpc.DcrdConnect, wallets r
// Add custom functions for use in templates.
router.SetFuncMap(template.FuncMap{
"txURL": txURL(cfg.BlockExplorerURL),
"addressURL": addressURL(cfg.BlockExplorerURL),
"blockURL": blockURL(cfg.BlockExplorerURL),
"txURL": txURL(s.cfg.BlockExplorerURL),
"addressURL": addressURL(s.cfg.BlockExplorerURL),
"blockURL": blockURL(s.cfg.BlockExplorerURL),
"dateTime": dateTime,
"stripWss": stripWss,
"indentJSON": indentJSON,
@ -212,7 +216,7 @@ func router(debugMode bool, cookieSecret []byte, dcrd rpc.DcrdConnect, wallets r
// sending no response at all.
router.Use(Recovery())
if debugMode {
if s.cfg.Debug {
// Logger middleware outputs very detailed logging of webserver requests
// to the terminal. Does not get logged to file.
router.Use(gin.Logger())
@ -227,37 +231,37 @@ func router(debugMode bool, cookieSecret []byte, dcrd rpc.DcrdConnect, wallets r
// API routes.
api := router.Group("/api/v3")
api.GET("/vspinfo", vspInfo)
api.POST("/setaltsignaddr", withDcrdClient(dcrd), broadcastTicket(), vspAuth(), setAltSignAddr)
api.POST("/feeaddress", withDcrdClient(dcrd), broadcastTicket(), vspAuth(), feeAddress)
api.POST("/ticketstatus", withDcrdClient(dcrd), vspAuth(), ticketStatus)
api.POST("/payfee", withDcrdClient(dcrd), vspAuth(), payFee)
api.POST("/setvotechoices", withDcrdClient(dcrd), withWalletClients(wallets), vspAuth(), setVoteChoices)
api.GET("/vspinfo", s.vspInfo)
api.POST("/setaltsignaddr", withDcrdClient(dcrd, s.cfg), s.broadcastTicket, s.vspAuth, s.setAltSignAddr)
api.POST("/feeaddress", withDcrdClient(dcrd, s.cfg), s.broadcastTicket, s.vspAuth, s.feeAddress)
api.POST("/ticketstatus", withDcrdClient(dcrd, s.cfg), s.vspAuth, s.ticketStatus)
api.POST("/payfee", withDcrdClient(dcrd, s.cfg), s.vspAuth, s.payFee)
api.POST("/setvotechoices", withDcrdClient(dcrd, s.cfg), withWalletClients(wallets, s.cfg), s.vspAuth, s.setVoteChoices)
// Website routes.
router.GET("", homepage)
router.GET("", s.homepage)
login := router.Group("/admin").Use(
withSession(cookieStore),
)
login.POST("", adminLogin)
login.POST("", s.adminLogin)
admin := router.Group("/admin").Use(
withWalletClients(wallets), withSession(cookieStore), requireAdmin(),
withWalletClients(wallets, s.cfg), withSession(cookieStore), s.requireAdmin,
)
admin.GET("", withDcrdClient(dcrd), adminPage)
admin.POST("/ticket", withDcrdClient(dcrd), ticketSearch)
admin.GET("/backup", downloadDatabaseBackup)
admin.POST("/logout", adminLogout)
admin.GET("", withDcrdClient(dcrd, s.cfg), s.adminPage)
admin.POST("/ticket", withDcrdClient(dcrd, s.cfg), s.ticketSearch)
admin.GET("/backup", s.downloadDatabaseBackup)
admin.POST("/logout", s.adminLogout)
// Require Basic HTTP Auth on /admin/status endpoint.
basic := router.Group("/admin").Use(
withDcrdClient(dcrd), withWalletClients(wallets), gin.BasicAuth(gin.Accounts{
"admin": cfg.AdminPass,
withDcrdClient(dcrd, s.cfg), withWalletClients(wallets, s.cfg), gin.BasicAuth(gin.Accounts{
"admin": s.cfg.AdminPass,
}),
)
basic.GET("/status", statusJSON)
basic.GET("/status", s.statusJSON)
return router
}
@ -265,15 +269,15 @@ func router(debugMode bool, cookieSecret []byte, dcrd rpc.DcrdConnect, wallets r
// sendJSONResponse serializes the provided response, signs it, and sends the
// response to the client with a 200 OK status. Returns the seralized response
// and the signature.
func sendJSONResponse(resp interface{}, c *gin.Context) (string, string) {
func (s *Server) sendJSONResponse(resp interface{}, c *gin.Context) (string, string) {
dec, err := json.Marshal(resp)
if err != nil {
log.Errorf("JSON marshal error: %v", err)
sendError(errInternalError, c)
s.sendError(errInternalError, c)
return "", ""
}
sig := ed25519.Sign(signPrivKey, dec)
sig := ed25519.Sign(s.signPrivKey, dec)
sigStr := base64.StdEncoding.EncodeToString(sig)
c.Writer.Header().Set("VSP-Server-Signature", sigStr)
@ -284,14 +288,14 @@ func sendJSONResponse(resp interface{}, c *gin.Context) (string, string) {
// sendError sends an error response to the client using the default error
// message.
func sendError(e apiError, c *gin.Context) {
func (s *Server) sendError(e apiError, c *gin.Context) {
msg := e.Error()
sendErrorWithMsg(msg, e, c)
s.sendErrorWithMsg(msg, e, c)
}
// sendErrorWithMsg sends an error response to the client using the provided
// error message.
func sendErrorWithMsg(msg string, e apiError, c *gin.Context) {
func (s *Server) sendErrorWithMsg(msg string, e apiError, c *gin.Context) {
status := e.httpStatus()
resp := gin.H{
@ -304,7 +308,7 @@ func sendErrorWithMsg(msg string, e apiError, c *gin.Context) {
if err != nil {
log.Warnf("Sending error response without signature: %v", err)
} else {
sig := ed25519.Sign(signPrivKey, dec)
sig := ed25519.Sign(s.signPrivKey, dec)
c.Writer.Header().Set("VSP-Server-Signature", base64.StdEncoding.EncodeToString(sig))
}