Skip to content

Commit cf433bb

Browse files
Add manual ticket search feature.
1 parent e23e372 commit cf433bb

File tree

6 files changed

+237
-1
lines changed

6 files changed

+237
-1
lines changed

webapi/admin.go

+7
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,13 @@ func (s *Server) adminPage(c *gin.Context) {
155155
func (s *Server) 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+
if err := validateTicketHash(hash); err != nil {
160+
s.log.Errorf("ticketSearch: Invalid ticket hash (ticketHash=%s): %v", hash, err)
161+
c.String(http.StatusBadRequest, "invalid ticket hash")
162+
return
163+
}
164+
158165
ticket, found, err := s.db.GetTicketByHash(hash)
159166
if err != nil {
160167
s.log.Errorf("db.GetTicketByHash error (ticketHash=%s): %v", hash, err)

webapi/middleware.go

+115
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ package webapi
66

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

1417
"github.com/decred/vspd/rpc"
1518
"github.com/gin-gonic/gin"
@@ -18,6 +21,10 @@ import (
1821
"github.com/jrick/wsrpc/v2"
1922
)
2023

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 at vsp with pubkey %s on window %d."
27+
2128
// withSession middleware adds a gorilla session to the request context for
2229
// downstream handlers to make use of. Sessions are used by admin pages to
2330
// maintain authentication status.
@@ -349,3 +356,111 @@ func (s *Server) vspAuth(c *gin.Context) {
349356
c.Set(knownTicketKey, ticketFound)
350357
c.Set(commitmentAddressKey, commitmentAddress)
351358
}
359+
360+
// ticketSearchAuth middleware reads the request form body and extracts the
361+
// ticket hash and signature from the base64 string provided. The commitment
362+
// address for the ticket is retrieved from the database if it is known, or it
363+
// is retrieved from the chain if not. The middleware errors out if required
364+
// information is not provided or the signature does not contain a message
365+
// signed with the commitment address. Ticket information is added to the
366+
// request context for downstream handlers to use.
367+
func (s *Server) ticketSearchAuth(c *gin.Context) {
368+
funcName := "ticketSearchAuth"
369+
370+
encodedString := c.PostForm("encoded")
371+
372+
// Get information added to context.
373+
dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC)
374+
dcrdErr := c.MustGet(dcrdErrorKey)
375+
if dcrdErr != nil {
376+
s.log.Errorf("%s: Could not get dcrd client: %v", funcName, dcrdErr.(error))
377+
c.Set(errorKey, errInternalError)
378+
return
379+
}
380+
381+
currentBlockHeader, err := dcrdClient.GetBestBlockHeader()
382+
if err != nil {
383+
s.log.Errorf("%s: Error getting best block header : %v", funcName, err)
384+
c.Set(errorKey, errInternalError)
385+
// Average blocks per day for the current network.
386+
blocksPerDay := (24 * time.Hour) / s.cfg.NetParams.TargetTimePerBlock
387+
blockWindow := int(currentBlockHeader.Height) / int(blocksPerDay)
388+
389+
decodedByte, err := base64.StdEncoding.DecodeString(encodedString)
390+
if err != nil {
391+
s.log.Errorf("%s: Decoding form data error : %v", funcName, err)
392+
c.Set(errorKey, errBadRequest)
393+
return
394+
}
395+
396+
data := strings.Split(string(decodedByte), ":")
397+
if len(data) != 2 {
398+
c.Set(errorKey, errBadRequest)
399+
return
400+
}
401+
402+
ticketHash := data[0]
403+
signature := data[1]
404+
vspPublicKey := s.cache.data.PubKey
405+
messageSigned := fmt.Sprintf(TicketSearchMessageFmt, ticketHash, vspPublicKey, blockWindow)
406+
407+
// Before hitting the db or any RPC, ensure this is a valid ticket hash.
408+
err = validateTicketHash(ticketHash)
409+
if err != nil {
410+
s.log.Errorf("%s: Invalid ticket (clientIP=%s): %v", funcName, c.ClientIP(), err)
411+
c.Set(errorKey, errInvalidTicket)
412+
return
413+
}
414+
415+
// Check if this ticket already appears in the database.
416+
ticket, ticketFound, err := s.db.GetTicketByHash(ticketHash)
417+
if err != nil {
418+
s.log.Errorf("%s: db.GetTicketByHash error (ticketHash=%s): %v", funcName, ticketHash, err)
419+
c.Set(errorKey, errInternalError)
420+
return
421+
}
422+
423+
if !ticketFound {
424+
s.log.Warnf("%s: Unknown ticket (clientIP=%s)", funcName, c.ClientIP())
425+
c.Set(errorKey, errUnknownTicket)
426+
return
427+
}
428+
429+
// If the ticket was found in the database, we already know its
430+
// commitment address. Otherwise we need to get it from the chain.
431+
var commitmentAddress string
432+
if ticketFound {
433+
commitmentAddress = ticket.CommitmentAddress
434+
} else {
435+
commitmentAddress, err = getCommitmentAddress(ticketHash, dcrdClient, s.cfg.NetParams)
436+
if err != nil {
437+
s.log.Errorf("%s: Failed to get commitment address (clientIP=%s, ticketHash=%s): %v",
438+
funcName, c.ClientIP(), ticketHash, err)
439+
440+
var apiErr *apiError
441+
if errors.Is(err, apiErr) {
442+
c.Set(errorKey, errInvalidTicket)
443+
} else {
444+
c.Set(errorKey, errInternalError)
445+
}
446+
447+
return
448+
}
449+
}
450+
451+
// Validate request signature to ensure ticket ownership.
452+
err = validateSignature(ticketHash, commitmentAddress, signature, messageSigned, s.db, s.cfg.NetParams)
453+
if err != nil {
454+
s.log.Errorf("%s: Couldn't validate signature (clientIP=%s, ticketHash=%s): %v",
455+
funcName, c.ClientIP(), ticketHash, err)
456+
c.Set(errorKey, errBadSignature)
457+
return
458+
}
459+
460+
// Add ticket information to context so downstream handlers don't need
461+
// to access the db for it.
462+
c.Set(ticketKey, ticket)
463+
c.Set(knownTicketKey, ticketFound)
464+
c.Set(errorKey, nil)
465+
}
466+
}

webapi/templates/homepage.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ <h1>VSP Overview</h1>
2222

2323
<p class="pt-1 pb-2">A Voting Service Provider (VSP) maintains a pool of always-online voting wallets,
2424
and allows Decred ticket holders to use these wallets to vote their tickets in exchange for a small fee.
25-
VSPs are completely non-custodial - they never hold, manage, or have access to any user funds.
25+
VSPs are completely non-custodial - they never hold, manage, or have access to any user funds. <a href="/ticket" target="_blank" rel="noopener noreferrer">Click here to search tickets</a>. <br>
2626
Visit <a href="https://docs.decred.org/proof-of-stake/overview/" target="_blank" rel="noopener noreferrer">docs.decred.org</a>
2727
to find out more about VSPs, tickets, and voting.
2828
</p>

webapi/templates/ticket.html

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{{ template "header" . }}
2+
3+
<div class="vsp-overview pt-4 pb-3 mb-3">
4+
<div class="container">
5+
6+
<div class="d-flex flex-wrap">
7+
<h1>Ticket Search</h1>
8+
</div>
9+
</div>
10+
</div>
11+
12+
<div class="container">
13+
<div class="row">
14+
<div class="col-12 pt-2 pb-4">
15+
<div class="block__content">
16+
<section>
17+
<form action="/ticket" method="post">
18+
<p class="my-1 orange" style="visibility:{{ if .Error }}visible{{ else }}hidden{{ end }};">
19+
{{.Error}}</p>
20+
21+
<div class="mb-3 col col-lg-8 pl-0">
22+
<label for="encodedInput" class="form-label">Encoded Base64 String</label>
23+
<input type="text" name="encoded" size="64" class="form-control shadow-none" minlength="64"
24+
maxlength="500" id="encodedInput" required placeholder="Encoded string" autocomplete="off" value="">
25+
</div>
26+
<button class="btn btn-primary d-block my-2" type="submit">Search</button>
27+
</form>
28+
29+
{{ with .SearchResult }}
30+
{{ template "ticket-search-result" . }}
31+
{{ end }}
32+
</section>
33+
</div>
34+
</div>
35+
</div>
36+
</div>
37+
38+
</div>
39+
{{ template "footer" . }}

webapi/ticket.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright (c) 2022 The Decred developers
2+
// Use of this source code is governed by an ISC
3+
// license that can be found in the LICENSE file.
4+
5+
package webapi
6+
7+
import (
8+
"net/http"
9+
10+
"github.com/decred/vspd/database"
11+
"github.com/gin-gonic/gin"
12+
)
13+
14+
// ticketPage is the handler for "GET /ticket"
15+
func (s *Server) ticketPage(c *gin.Context) {
16+
c.HTML(http.StatusOK, "ticket.html", gin.H{
17+
"WebApiCache": s.cache.getData(),
18+
"WebApiCfg": s.cfg,
19+
})
20+
}
21+
22+
// ticketErrPage returns error message to the ticket page.
23+
func (s *Server) ticketErrPage(c *gin.Context, status int, message string) {
24+
c.HTML(status, "ticket.html", gin.H{
25+
"WebApiCache": s.cache.getData(),
26+
"WebApiCfg": s.cfg,
27+
"Error": message,
28+
})
29+
30+
}
31+
32+
// manualTicketSearch is the handler for "POST /ticket".
33+
func (s *Server) manualTicketSearch(c *gin.Context) {
34+
35+
// Get values which have been added to context by middleware.
36+
err := c.MustGet(errorKey)
37+
if err != nil {
38+
apiErr := err.(apiError)
39+
s.ticketErrPage(c, apiErr.httpStatus(), apiErr.String())
40+
return
41+
}
42+
43+
ticket := c.MustGet(ticketKey).(database.Ticket)
44+
knownTicket := c.MustGet(knownTicketKey).(bool)
45+
46+
voteChanges, err := s.db.GetVoteChanges(ticket.Hash)
47+
if err != nil {
48+
s.log.Errorf("db.GetVoteChanges error (ticket=%s): %v", ticket.Hash, err)
49+
s.ticketErrPage(c, http.StatusBadRequest, "Error getting vote changes from database")
50+
return
51+
}
52+
53+
altSignAddrData, err := s.db.AltSignAddrData(ticket.Hash)
54+
if err != nil {
55+
s.log.Errorf("db.AltSignAddrData error (ticket=%s): %v", ticket.Hash, err)
56+
s.ticketErrPage(c, http.StatusBadRequest, "Error getting alternate signature from database")
57+
return
58+
}
59+
60+
c.HTML(http.StatusOK, "ticket.html", gin.H{
61+
"SearchResult": searchResult{
62+
Hash: ticket.Hash,
63+
Found: knownTicket,
64+
Ticket: ticket,
65+
AltSignAddrData: altSignAddrData,
66+
VoteChanges: voteChanges,
67+
MaxVoteChanges: s.cfg.MaxVoteChangeRecords,
68+
},
69+
"WebApiCache": s.cache.getData(),
70+
"WebApiCfg": s.cfg,
71+
})
72+
}

webapi/webapi.go

+3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const (
6262
ticketKey = "Ticket"
6363
knownTicketKey = "KnownTicket"
6464
commitmentAddressKey = "CommitmentAddress"
65+
errorKey = "Error"
6566
)
6667

6768
type Server struct {
@@ -244,6 +245,8 @@ func (s *Server) router(cookieSecret []byte, dcrd rpc.DcrdConnect, wallets rpc.W
244245
// Website routes.
245246

246247
router.GET("", s.homepage)
248+
router.GET("/ticket", s.ticketPage)
249+
router.POST("/ticket", s.withDcrdClient(dcrd), s.ticketSearchAuth, s.manualTicketSearch)
247250

248251
login := router.Group("/admin").Use(
249252
s.withSession(cookieStore),

0 commit comments

Comments
 (0)