Skip to content

Commit

Permalink
Add manual ticket search feature.
Browse files Browse the repository at this point in the history
  • Loading branch information
ukane-philemon committed Feb 1, 2022
1 parent 040ed56 commit 4da873b
Show file tree
Hide file tree
Showing 9 changed files with 341 additions and 68 deletions.
6 changes: 6 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type config struct {
VspClosedMsg string `long:"vspclosedmsg" ini-name:"vspclosedmsg" description:"A short message displayed on the webpage and returned by the status API endpoint if vspclosed is true."`
AdminPass string `long:"adminpass" ini-name:"adminpass" description:"Password for accessing admin page."`
Designation string `long:"designation" ini-name:"designation" description:"Short name for the VSP. Customizes the logo in the top toolbar."`
vspURL string `long:"vspURL" ini-name:"vspURL" description:"URL of the VSP."`

// The following flags should be set on CLI only, not via config file.
ShowVersion bool `long:"version" no-ini:"true" description:"Display version information and exit."`
Expand Down Expand Up @@ -297,6 +298,11 @@ func loadConfig() (*config, error) {
cfg.VspClosedMsg = ""
}

// Ensure the VSP url is set.
if cfg.vspURL == "" {
return nil, errors.New("the vspURL option is not set")
}

// Ensure the support email address is set.
if cfg.SupportEmail == "" {
return nil, errors.New("the supportemail option is not set")
Expand Down
1 change: 1 addition & 0 deletions vspd.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func run(ctx context.Context) error {
SupportEmail: cfg.SupportEmail,
VspClosed: cfg.VspClosed,
VspClosedMsg: cfg.VspClosedMsg,
VspURL: cfg.vspURL,
AdminPass: cfg.AdminPass,
Debug: cfg.WebServerDebug,
Designation: cfg.Designation,
Expand Down
8 changes: 8 additions & 0 deletions webapi/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ func adminPage(c *gin.Context) {
func ticketSearch(c *gin.Context) {
hash := c.PostForm("hash")

// Before hitting the db, ensure this is a valid ticket hash. Ignore bool.
_, err := validateTicketHash(c, hash)
if err != nil {
log.Error(err)
c.String(http.StatusBadRequest, "invalid ticket hash")
return
}

ticket, found, err := db.GetTicketByHash(hash)
if err != nil {
log.Errorf("db.GetTicketByHash error (ticketHash=%s): %v", hash, err)
Expand Down
85 changes: 76 additions & 9 deletions webapi/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import (
"fmt"

"github.com/decred/dcrd/blockchain/stake/v4"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/dcrutil/v4"
"github.com/decred/dcrd/wire"
"github.com/decred/vspd/rpc"
"github.com/gin-gonic/gin"
)

Expand Down Expand Up @@ -52,17 +54,26 @@ agendaLoop:
return nil
}

func validateSignature(reqBytes []byte, commitmentAddress string, c *gin.Context) error {
// Ensure a signature is provided.
signature := c.GetHeader("VSP-Client-Signature")
if signature == "" {
return errors.New("no VSP-Client-Signature header")
}
func validateSignature(hash, commitmentAddress, signature, message string, c *gin.Context) error {
firstErr := dcrutil.VerifyMessage(commitmentAddress, signature, message, cfg.NetParams)
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.
altSigData, err := db.AltSignAddrData(hash)
if err != nil {
return fmt.Errorf("db.AltSignAddrData failed (ticketHash=%s): %v", hash, err)

err := dcrutil.VerifyMessage(commitmentAddress, signature, string(reqBytes), cfg.NetParams)
if err != nil {
return err
}

// 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 {
return fmt.Errorf("Bad signature (clientIP=%s, ticketHash=%s)", c.ClientIP(), hash)
}

return firstErr
}

return nil
}

Expand All @@ -88,3 +99,59 @@ func isValidTicket(tx *wire.MsgTx) error {

return nil
}

// validateTicketHash ensures the provided ticket hash is a valid ticket hash.
// A ticket hash should be 64 chars (MaxHashStringSize) and should parse into
// a chainhash.Hash without error.
func validateTicketHash(c *gin.Context, hash string) (bool, error) {
if len(hash) != chainhash.MaxHashStringSize {
return false, fmt.Errorf("Incorrect hash length (clientIP=%s): got %d, expected %d", c.ClientIP(), len(hash), chainhash.MaxHashStringSize)

}
_, err := chainhash.NewHashFromStr(hash)
if err != nil {
return false, fmt.Errorf("Invalid hash (clientIP=%s): %v", c.ClientIP(), err)

}

return true, nil
}

// getCommitmentAddress gets the commitment address of the provided ticket hash
// from the chain.
func getCommitmentAddress(c *gin.Context, hash string) (string, bool, error) {
var commitmentAddress string
dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC)
dcrdErr := c.MustGet(dcrdErrorKey)
if dcrdErr != nil {
return commitmentAddress, false, fmt.Errorf("could not get dcrd client: %v", dcrdErr.(error))

}

resp, err := dcrdClient.GetRawTransaction(hash)
if err != nil {
return commitmentAddress, false, fmt.Errorf("dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v", hash, err)

}

msgTx, err := decodeTransaction(resp.Hex)
if err != nil {
return commitmentAddress, false, fmt.Errorf("Failed to decode ticket hex (ticketHash=%s): %v", hash, err)

}

err = isValidTicket(msgTx)
if err != nil {
return commitmentAddress, true, fmt.Errorf("Invalid ticket (clientIP=%s, ticketHash=%s): %v", c.ClientIP(), hash, err)

}

addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, cfg.NetParams)
if err != nil {
return commitmentAddress, false, fmt.Errorf("AddrFromSStxPkScrCommitment error (ticketHash=%s): %v", hash, err)

}

commitmentAddress = addr.String()
return commitmentAddress, false, nil
}
192 changes: 134 additions & 58 deletions webapi/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,25 @@ package webapi

import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"

"github.com/decred/dcrd/blockchain/stake/v4"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/vspd/rpc"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/gorilla/sessions"
"github.com/jrick/wsrpc/v2"
)

// TicketSearchMessageFmt is the format for the message to be signed
// in order to search for a ticket using the vspd frontend.
const TicketSearchMessageFmt = "I want to check vspd ticket status for ticket %s on VSP %s on block window %d."

// withSession middleware adds a gorilla session to the request context for
// downstream handlers to make use of. Sessions are used by admin pages to
// maintain authentication status.
Expand Down Expand Up @@ -287,17 +292,9 @@ func vspAuth() gin.HandlerFunc {
hash := request.TicketHash

// Before hitting the db or any RPC, ensure this is a valid ticket hash.
// A ticket hash should be 64 chars (MaxHashStringSize) and should parse
// into a chainhash.Hash without error.
if len(hash) != chainhash.MaxHashStringSize {
log.Errorf("%s: Incorrect hash length (clientIP=%s): got %d, expected %d",
funcName, c.ClientIP(), len(hash), chainhash.MaxHashStringSize)
sendErrorWithMsg("invalid ticket hash", errBadRequest, c)
return
}
_, err = chainhash.NewHashFromStr(hash)
if err != nil {
log.Errorf("%s: Invalid hash (clientIP=%s): %v", funcName, c.ClientIP(), err)
validticket, err := validateTicketHash(c, hash)
if !validticket {
log.Errorf("%s: %v", funcName, err)
sendErrorWithMsg("invalid ticket hash", errBadRequest, c)
return
}
Expand All @@ -313,74 +310,153 @@ func vspAuth() gin.HandlerFunc {
// 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
var isInvalid bool

if ticketFound {
commitmentAddress = ticket.CommitmentAddress
} else {
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
}

resp, err := dcrdClient.GetRawTransaction(hash)
commitmentAddress, isInvalid, err = getCommitmentAddress(c, hash)
if err != nil {
log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v", funcName, hash, err)
sendError(errInternalError, c)
if isInvalid {
sendError(errInvalidTicket, c)
} else {
sendError(errInternalError, c)
}
log.Errorf("%s: %v", funcName, err)
return
}
}

msgTx, err := decodeTransaction(resp.Hex)
if err != nil {
log.Errorf("%s: Failed to decode ticket hex (ticketHash=%s): %v", funcName, ticket.Hash, err)
sendError(errInternalError, c)
return
}
// Ensure a signature is provided.
signature := c.GetHeader("VSP-Client-Signature")
if signature == "" {
sendErrorWithMsg("no VSP-Client-Signature header", errBadRequest, c)
return
}

err = isValidTicket(msgTx)
if err != nil {
log.Warnf("%s: Invalid ticket (clientIP=%s, ticketHash=%s): %v", funcName, c.ClientIP(), hash, err)
sendError(errInvalidTicket, c)
return
}
// Validate request signature to ensure ticket ownership.
err = validateSignature(hash, commitmentAddress, signature, string(reqBytes), c)
if err != nil {
log.Errorf("%s: %v", funcName, err)
sendError(errBadSignature, c)
return
}

addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, cfg.NetParams)
if err != nil {
log.Errorf("%s: AddrFromSStxPkScrCommitment error (ticketHash=%s): %v", funcName, hash, err)
sendError(errInternalError, 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)
}

}

// ticketSearchAuth middleware reads the request form body and extracts the
// ticket hash and signature from the base64 string provided. The commitment
// address for the ticket is retrieved from the database if it is known, or
// it is retrieved from the chain if not.
// The middleware errors out if required information is not provided or the
// signature does not contain a message signed with the commitment
// address. Ticket information is added to the request context for downstream
// handlers to use.
func ticketSearchAuth() gin.HandlerFunc {
return func(c *gin.Context) {
funcName := "ticketSearchAuth"

encodedString := c.PostForm("encoded")

// Get information added to context.
dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC)
dcrdErr := c.MustGet(dcrdErrorKey)
if dcrdErr != nil {
log.Warnf("%s: %v", funcName, dcrdErr.(error))
c.Set(errorKey, errInternalError)
return
}

commitmentAddress = addr.String()
currentBlockHeader, err := dcrdClient.GetBestBlockHeader()
if err != nil {
log.Errorf("%s: Error getting best block header : %v", funcName, err)
c.Set(errorKey, errInternalError)
return
}

// Validate request signature to ensure ticket ownership.
err = validateSignature(reqBytes, commitmentAddress, c)
// Average blocks per day for the current network.
blocksPerDay := (24 * time.Hour) / cfg.NetParams.TargetTimePerBlock
blockWindow := int(currentBlockHeader.Height) / int(blocksPerDay)

decodedByte, err := base64.StdEncoding.DecodeString(encodedString)
if err != nil {
log.Errorf("%s: Decoding form data error : %v", funcName, err)
c.Set(errorKey, errBadRequest)
return
}

data := strings.Split(string(decodedByte), ":")
if len(data) != 2 {
c.Set(errorKey, errBadRequest)
return
}

ticketHash := data[0]
signature := data[1]
vspURL := cfg.VspURL
messageSigned := fmt.Sprintf(TicketSearchMessageFmt, ticketHash, vspURL, blockWindow)

// Before hitting the db or any RPC, ensure this is a valid ticket hash.
validticket, err := validateTicketHash(c, ticketHash)
if !validticket {
log.Errorf("%s: %v", funcName, err)
c.Set(errorKey, errInvalidTicket)
return
}

// Check if this ticket already appears in the database.
ticket, ticketFound, err := db.GetTicketByHash(ticketHash)
if err != nil {
// Don't return an error straight away if sig validation fails -
// first check if we have an alternate sign address for this ticket.
altSigData, err := db.AltSignAddrData(hash)
log.Errorf("%s: db.GetTicketByHash error (ticketHash=%s): %v", funcName, ticketHash, err)
c.Set(errorKey, errInternalError)
return
}

if !ticketFound {
log.Warnf("%s: Unknown ticket (clientIP=%s)", funcName, c.ClientIP())
c.Set(errorKey, errUnknownTicket)
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
var isInvalid bool
if ticketFound {
commitmentAddress = ticket.CommitmentAddress
} else {
commitmentAddress, isInvalid, err = getCommitmentAddress(c, ticketHash)
if err != nil {
log.Errorf("%s: db.AltSignAddrData failed (ticketHash=%s): %v", funcName, hash, err)
sendError(errInternalError, c)
if isInvalid {
c.Set(errorKey, errInvalidTicket)
} else {
c.Set(errorKey, errInternalError)
}
log.Errorf("%s: %v", funcName, err)
return
}
}

// 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 || validateSignature(reqBytes, altSigData.AltSignAddr, c) != nil {
log.Warnf("%s: Bad signature (clientIP=%s, ticketHash=%s)", funcName, c.ClientIP(), hash)
sendError(errBadSignature, c)
return
}
// Validate request signature to ensure ticket ownership.
err = validateSignature(ticketHash, commitmentAddress, signature, messageSigned, c)
if err != nil {
log.Errorf("%s: %v", funcName, err)
c.Set(errorKey, errBadSignature)
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)
c.Set(errorKey, nil)
}

}
Loading

0 comments on commit 4da873b

Please sign in to comment.