Skip to content

Commit 4822daf

Browse files
Add manual ticket search feature.
1 parent 040ed56 commit 4822daf

File tree

9 files changed

+342
-68
lines changed

9 files changed

+342
-68
lines changed

config.go

+6
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ type config struct {
6464
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."`
6565
AdminPass string `long:"adminpass" ini-name:"adminpass" description:"Password for accessing admin page."`
6666
Designation string `long:"designation" ini-name:"designation" description:"Short name for the VSP. Customizes the logo in the top toolbar."`
67+
VspUrl string `long:"vspurl" ini-name:"vspurl" description:"URL of the VSP."`
6768

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

301+
// Ensure the VSP url is set.
302+
if cfg.VspUrl == "" {
303+
return nil, errors.New("the vspurl option is not set")
304+
}
305+
300306
// Ensure the support email address is set.
301307
if cfg.SupportEmail == "" {
302308
return nil, errors.New("the supportemail option is not set")

vspd.go

+1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ func run(ctx context.Context) error {
9898
SupportEmail: cfg.SupportEmail,
9999
VspClosed: cfg.VspClosed,
100100
VspClosedMsg: cfg.VspClosedMsg,
101+
VSPUrl: cfg.VspUrl,
101102
AdminPass: cfg.AdminPass,
102103
Debug: cfg.WebServerDebug,
103104
Designation: cfg.Designation,

webapi/admin.go

+8
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ func adminPage(c *gin.Context) {
155155
func ticketSearch(c *gin.Context) {
156156
hash := c.PostForm("hash")
157157

158+
// Before hitting the db, ensure this is a valid ticket hash. Ignore bool.
159+
_, err := validateTicketHash(c, hash)
160+
if err != nil {
161+
log.Error(err)
162+
c.String(http.StatusBadRequest, "invalid ticket hash")
163+
return
164+
}
165+
158166
ticket, found, err := db.GetTicketByHash(hash)
159167
if err != nil {
160168
log.Errorf("db.GetTicketByHash error (ticketHash=%s): %v", hash, err)

webapi/helpers.go

+76-9
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import (
1010
"fmt"
1111

1212
"github.com/decred/dcrd/blockchain/stake/v4"
13+
"github.com/decred/dcrd/chaincfg/chainhash"
1314
"github.com/decred/dcrd/chaincfg/v3"
1415
"github.com/decred/dcrd/dcrutil/v4"
1516
"github.com/decred/dcrd/wire"
17+
"github.com/decred/vspd/rpc"
1618
"github.com/gin-gonic/gin"
1719
)
1820

@@ -52,17 +54,26 @@ agendaLoop:
5254
return nil
5355
}
5456

55-
func validateSignature(reqBytes []byte, commitmentAddress string, c *gin.Context) error {
56-
// Ensure a signature is provided.
57-
signature := c.GetHeader("VSP-Client-Signature")
58-
if signature == "" {
59-
return errors.New("no VSP-Client-Signature header")
60-
}
57+
func validateSignature(hash, commitmentAddress, signature, message string, c *gin.Context) error {
58+
firstErr := dcrutil.VerifyMessage(commitmentAddress, signature, message, cfg.NetParams)
59+
if firstErr != nil {
60+
// Don't return an error straight away if sig validation fails -
61+
// first check if we have an alternate sign address for this ticket.
62+
altSigData, err := db.AltSignAddrData(hash)
63+
if err != nil {
64+
return fmt.Errorf("db.AltSignAddrData failed (ticketHash=%s): %v", hash, err)
6165

62-
err := dcrutil.VerifyMessage(commitmentAddress, signature, string(reqBytes), cfg.NetParams)
63-
if err != nil {
64-
return err
66+
}
67+
68+
// If we have no alternate sign address, or if validating with the
69+
// alt sign addr fails, return an error to the client.
70+
if altSigData == nil || dcrutil.VerifyMessage(altSigData.AltSignAddr, signature, message, cfg.NetParams) != nil {
71+
return fmt.Errorf("Bad signature (clientIP=%s, ticketHash=%s)", c.ClientIP(), hash)
72+
}
73+
74+
return firstErr
6575
}
76+
6677
return nil
6778
}
6879

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

89100
return nil
90101
}
102+
103+
// validateTicketHash ensures the provided ticket hash is a valid ticket hash.
104+
// A ticket hash should be 64 chars (MaxHashStringSize) and should parse into
105+
// a chainhash.Hash without error.
106+
func validateTicketHash(c *gin.Context, hash string) (bool, error) {
107+
if len(hash) != chainhash.MaxHashStringSize {
108+
return false, fmt.Errorf("Incorrect hash length (clientIP=%s): got %d, expected %d", c.ClientIP(), len(hash), chainhash.MaxHashStringSize)
109+
110+
}
111+
_, err := chainhash.NewHashFromStr(hash)
112+
if err != nil {
113+
return false, fmt.Errorf("Invalid hash (clientIP=%s): %v", c.ClientIP(), err)
114+
115+
}
116+
117+
return true, nil
118+
}
119+
120+
// getCommitmentAddress gets the commitment address of the provided ticket hash
121+
// from the chain.
122+
func getCommitmentAddress(c *gin.Context, hash string) (string, bool, error) {
123+
var commitmentAddress string
124+
dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC)
125+
dcrdErr := c.MustGet(dcrdErrorKey)
126+
if dcrdErr != nil {
127+
return commitmentAddress, false, fmt.Errorf("could not get dcrd client: %v", dcrdErr.(error))
128+
129+
}
130+
131+
resp, err := dcrdClient.GetRawTransaction(hash)
132+
if err != nil {
133+
return commitmentAddress, false, fmt.Errorf("dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v", hash, err)
134+
135+
}
136+
137+
msgTx, err := decodeTransaction(resp.Hex)
138+
if err != nil {
139+
return commitmentAddress, false, fmt.Errorf("Failed to decode ticket hex (ticketHash=%s): %v", hash, err)
140+
141+
}
142+
143+
err = isValidTicket(msgTx)
144+
if err != nil {
145+
return commitmentAddress, true, fmt.Errorf("Invalid ticket (clientIP=%s, ticketHash=%s): %v", c.ClientIP(), hash, err)
146+
147+
}
148+
149+
addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, cfg.NetParams)
150+
if err != nil {
151+
return commitmentAddress, false, fmt.Errorf("AddrFromSStxPkScrCommitment error (ticketHash=%s): %v", hash, err)
152+
153+
}
154+
155+
commitmentAddress = addr.String()
156+
return commitmentAddress, false, nil
157+
}

webapi/middleware.go

+134-58
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,25 @@ package webapi
66

77
import (
88
"bytes"
9+
"encoding/base64"
910
"errors"
11+
"fmt"
1012
"io"
1113
"net/http"
1214
"strings"
15+
"time"
1316

14-
"github.com/decred/dcrd/blockchain/stake/v4"
15-
"github.com/decred/dcrd/chaincfg/chainhash"
1617
"github.com/decred/vspd/rpc"
1718
"github.com/gin-gonic/gin"
1819
"github.com/gin-gonic/gin/binding"
1920
"github.com/gorilla/sessions"
2021
"github.com/jrick/wsrpc/v2"
2122
)
2223

24+
// TicketSearchMessageFmt is the format for the message to be signed
25+
// in order to search for a ticket using the vspd frontend.
26+
const TicketSearchMessageFmt = "I want to check vspd ticket status for ticket %s on VSP %s on block window %d."
27+
2328
// withSession middleware adds a gorilla session to the request context for
2429
// downstream handlers to make use of. Sessions are used by admin pages to
2530
// maintain authentication status.
@@ -287,17 +292,9 @@ func vspAuth() gin.HandlerFunc {
287292
hash := request.TicketHash
288293

289294
// Before hitting the db or any RPC, ensure this is a valid ticket hash.
290-
// A ticket hash should be 64 chars (MaxHashStringSize) and should parse
291-
// into a chainhash.Hash without error.
292-
if len(hash) != chainhash.MaxHashStringSize {
293-
log.Errorf("%s: Incorrect hash length (clientIP=%s): got %d, expected %d",
294-
funcName, c.ClientIP(), len(hash), chainhash.MaxHashStringSize)
295-
sendErrorWithMsg("invalid ticket hash", errBadRequest, c)
296-
return
297-
}
298-
_, err = chainhash.NewHashFromStr(hash)
299-
if err != nil {
300-
log.Errorf("%s: Invalid hash (clientIP=%s): %v", funcName, c.ClientIP(), err)
295+
validticket, err := validateTicketHash(c, hash)
296+
if !validticket {
297+
log.Errorf("%s: %v", funcName, err)
301298
sendErrorWithMsg("invalid ticket hash", errBadRequest, c)
302299
return
303300
}
@@ -313,74 +310,153 @@ func vspAuth() gin.HandlerFunc {
313310
// If the ticket was found in the database, we already know its
314311
// commitment address. Otherwise we need to get it from the chain.
315312
var commitmentAddress string
313+
var isInvalid bool
314+
316315
if ticketFound {
317316
commitmentAddress = ticket.CommitmentAddress
318317
} else {
319-
dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC)
320-
dcrdErr := c.MustGet(dcrdErrorKey)
321-
if dcrdErr != nil {
322-
log.Errorf("%s: could not get dcrd client: %v", funcName, dcrdErr.(error))
323-
sendError(errInternalError, c)
324-
return
325-
}
326-
327-
resp, err := dcrdClient.GetRawTransaction(hash)
318+
commitmentAddress, isInvalid, err = getCommitmentAddress(c, hash)
328319
if err != nil {
329-
log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v", funcName, hash, err)
330-
sendError(errInternalError, c)
320+
if isInvalid {
321+
sendError(errInvalidTicket, c)
322+
} else {
323+
sendError(errInternalError, c)
324+
}
325+
log.Errorf("%s: %v", funcName, err)
331326
return
332327
}
328+
}
333329

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

341-
err = isValidTicket(msgTx)
342-
if err != nil {
343-
log.Warnf("%s: Invalid ticket (clientIP=%s, ticketHash=%s): %v", funcName, c.ClientIP(), hash, err)
344-
sendError(errInvalidTicket, c)
345-
return
346-
}
337+
// Validate request signature to ensure ticket ownership.
338+
err = validateSignature(hash, commitmentAddress, signature, string(reqBytes), c)
339+
if err != nil {
340+
log.Errorf("%s: %v", funcName, err)
341+
sendError(errBadSignature, c)
342+
return
343+
}
347344

348-
addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, cfg.NetParams)
349-
if err != nil {
350-
log.Errorf("%s: AddrFromSStxPkScrCommitment error (ticketHash=%s): %v", funcName, hash, err)
351-
sendError(errInternalError, c)
352-
return
353-
}
345+
// Add ticket information to context so downstream handlers don't need
346+
// to access the db for it.
347+
c.Set(ticketKey, ticket)
348+
c.Set(knownTicketKey, ticketFound)
349+
c.Set(commitmentAddressKey, commitmentAddress)
350+
}
351+
352+
}
353+
354+
// ticketSearchAuth middleware reads the request form body and extracts the
355+
// ticket hash and signature from the base64 string provided. The commitment
356+
// address for the ticket is retrieved from the database if it is known, or
357+
// it is retrieved from the chain if not.
358+
// The middleware errors out if required information is not provided or the
359+
// signature does not contain a message signed with the commitment
360+
// address. Ticket information is added to the request context for downstream
361+
// handlers to use.
362+
func ticketSearchAuth() gin.HandlerFunc {
363+
return func(c *gin.Context) {
364+
funcName := "ticketSearchAuth"
365+
366+
encodedString := c.PostForm("encoded")
367+
368+
// Get information added to context.
369+
dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC)
370+
dcrdErr := c.MustGet(dcrdErrorKey)
371+
if dcrdErr != nil {
372+
log.Warnf("%s: %v", funcName, dcrdErr.(error))
373+
c.Set(errorKey, errInternalError)
374+
return
375+
}
354376

355-
commitmentAddress = addr.String()
377+
currentBlockHeader, err := dcrdClient.GetBestBlockHeader()
378+
if err != nil {
379+
log.Errorf("%s: Error getting best block header : %v", funcName, err)
380+
c.Set(errorKey, errInternalError)
381+
return
356382
}
357383

358-
// Validate request signature to ensure ticket ownership.
359-
err = validateSignature(reqBytes, commitmentAddress, c)
384+
// Average blocks per day for the current network.
385+
blocksPerDay := (24 * time.Hour) / cfg.NetParams.TargetTimePerBlock
386+
blockWindow := int(currentBlockHeader.Height) / int(blocksPerDay)
387+
388+
decodedByte, err := base64.StdEncoding.DecodeString(encodedString)
389+
if err != nil {
390+
log.Errorf("%s: Decoding form data error : %v", funcName, err)
391+
c.Set(errorKey, errBadRequest)
392+
return
393+
}
394+
395+
data := strings.Split(string(decodedByte), ":")
396+
if len(data) != 2 {
397+
c.Set(errorKey, errBadRequest)
398+
return
399+
}
400+
401+
ticketHash := data[0]
402+
signature := data[1]
403+
vspURL := cfg.VSPUrl
404+
messageSigned := fmt.Sprintf(TicketSearchMessageFmt, ticketHash, vspURL, blockWindow)
405+
406+
// Before hitting the db or any RPC, ensure this is a valid ticket hash.
407+
validticket, err := validateTicketHash(c, ticketHash)
408+
if !validticket {
409+
log.Errorf("%s: %v", funcName, err)
410+
c.Set(errorKey, errInvalidTicket)
411+
return
412+
}
413+
414+
// Check if this ticket already appears in the database.
415+
ticket, ticketFound, err := db.GetTicketByHash(ticketHash)
360416
if err != nil {
361-
// Don't return an error straight away if sig validation fails -
362-
// first check if we have an alternate sign address for this ticket.
363-
altSigData, err := db.AltSignAddrData(hash)
417+
log.Errorf("%s: db.GetTicketByHash error (ticketHash=%s): %v", funcName, ticketHash, err)
418+
c.Set(errorKey, errInternalError)
419+
return
420+
}
421+
422+
if !ticketFound {
423+
log.Warnf("%s: Unknown ticket (clientIP=%s)", funcName, c.ClientIP())
424+
c.Set(errorKey, errUnknownTicket)
425+
return
426+
}
427+
428+
// If the ticket was found in the database, we already know its
429+
// commitment address. Otherwise we need to get it from the chain.
430+
var commitmentAddress string
431+
var isInvalid bool
432+
if ticketFound {
433+
commitmentAddress = ticket.CommitmentAddress
434+
} else {
435+
commitmentAddress, isInvalid, err = getCommitmentAddress(c, ticketHash)
364436
if err != nil {
365-
log.Errorf("%s: db.AltSignAddrData failed (ticketHash=%s): %v", funcName, hash, err)
366-
sendError(errInternalError, c)
437+
if isInvalid {
438+
c.Set(errorKey, errInvalidTicket)
439+
} else {
440+
c.Set(errorKey, errInternalError)
441+
}
442+
log.Errorf("%s: %v", funcName, err)
367443
return
368444
}
445+
}
369446

370-
// If we have no alternate sign address, or if validating with the
371-
// alt sign addr fails, return an error to the client.
372-
if altSigData == nil || validateSignature(reqBytes, altSigData.AltSignAddr, c) != nil {
373-
log.Warnf("%s: Bad signature (clientIP=%s, ticketHash=%s)", funcName, c.ClientIP(), hash)
374-
sendError(errBadSignature, c)
375-
return
376-
}
447+
// Validate request signature to ensure ticket ownership.
448+
err = validateSignature(ticketHash, commitmentAddress, signature, messageSigned, c)
449+
if err != nil {
450+
log.Errorf("%s: %v", funcName, err)
451+
c.Set(errorKey, errBadSignature)
452+
return
377453
}
378454

379455
// Add ticket information to context so downstream handlers don't need
380456
// to access the db for it.
381457
c.Set(ticketKey, ticket)
382458
c.Set(knownTicketKey, ticketFound)
383-
c.Set(commitmentAddressKey, commitmentAddress)
459+
c.Set(errorKey, nil)
384460
}
385461

386462
}

0 commit comments

Comments
 (0)