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 Jul 4, 2022
1 parent e23e372 commit 3f45dee
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 1 deletion.
7 changes: 7 additions & 0 deletions webapi/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ func (s *Server) adminPage(c *gin.Context) {
func (s *Server) ticketSearch(c *gin.Context) {
hash := c.PostForm("hash")

// Before hitting the db, ensure this is a valid ticket hash. Ignore bool.
if err := validateTicketHash(hash); err != nil {
s.log.Errorf("ticketSearch: Invalid ticket hash (ticketHash=%s): %v", hash, err)
c.String(http.StatusBadRequest, "invalid ticket hash")
return
}

ticket, found, err := s.db.GetTicketByHash(hash)
if err != nil {
s.log.Errorf("db.GetTicketByHash error (ticketHash=%s): %v", hash, err)
Expand Down
115 changes: 115 additions & 0 deletions webapi/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ package webapi

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

"github.com/decred/vspd/rpc"
"github.com/gin-gonic/gin"
Expand All @@ -18,6 +21,10 @@ import (
"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 at vsp with pubkey %s on 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 @@ -349,3 +356,111 @@ func (s *Server) vspAuth(c *gin.Context) {
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 (s *Server) ticketSearchAuth(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 {
s.log.Errorf("%s: Could not get dcrd client: %v", funcName, dcrdErr.(error))
c.Set(errorKey, errInternalError)
return
}

currentBlockHeader, err := dcrdClient.GetBestBlockHeader()
if err != nil {
s.log.Errorf("%s: Error getting best block header : %v", funcName, err)
c.Set(errorKey, errInternalError)
// Average blocks per day for the current network.
blocksPerDay := (24 * time.Hour) / s.cfg.NetParams.TargetTimePerBlock
blockWindow := int(currentBlockHeader.Height) / int(blocksPerDay)

decodedByte, err := base64.StdEncoding.DecodeString(encodedString)
if err != nil {
s.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]
vspPublicKey := s.cache.data.PubKey
messageSigned := fmt.Sprintf(TicketSearchMessageFmt, ticketHash, vspPublicKey, blockWindow)

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

// Check if this ticket already appears in the database.
ticket, ticketFound, err := s.db.GetTicketByHash(ticketHash)
if err != nil {
s.log.Errorf("%s: db.GetTicketByHash error (ticketHash=%s): %v", funcName, ticketHash, err)
c.Set(errorKey, errInternalError)
return
}

if !ticketFound {
s.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
if ticketFound {
commitmentAddress = ticket.CommitmentAddress
} else {
commitmentAddress, err = getCommitmentAddress(ticketHash, dcrdClient, s.cfg.NetParams)
if err != nil {
s.log.Errorf("%s: Failed to get commitment address (clientIP=%s, ticketHash=%s): %v",
funcName, c.ClientIP(), ticketHash, err)

var apiErr *apiError
if errors.Is(err, apiErr) {
c.Set(errorKey, errInvalidTicket)
} else {
c.Set(errorKey, errInternalError)
}

return
}
}

// Validate request signature to ensure ticket ownership.
err = validateSignature(ticketHash, commitmentAddress, signature, messageSigned, s.db, s.cfg.NetParams)
if err != nil {
s.log.Errorf("%s: Couldn't validate signature (clientIP=%s, ticketHash=%s): %v",
funcName, c.ClientIP(), ticketHash, 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(errorKey, nil)
}
}
2 changes: 1 addition & 1 deletion webapi/templates/homepage.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ <h1>VSP Overview</h1>

<p class="pt-1 pb-2">A Voting Service Provider (VSP) maintains a pool of always-online voting wallets,
and allows Decred ticket holders to use these wallets to vote their tickets in exchange for a small fee.
VSPs are completely non-custodial - they never hold, manage, or have access to any user funds.
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>
Visit <a href="https://docs.decred.org/proof-of-stake/overview/" target="_blank" rel="noopener noreferrer">docs.decred.org</a>
to find out more about VSPs, tickets, and voting.
</p>
Expand Down
39 changes: 39 additions & 0 deletions webapi/templates/ticket.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{{ template "header" . }}

<div class="vsp-overview pt-4 pb-3 mb-3">
<div class="container">

<div class="d-flex flex-wrap">
<h1>Ticket Search</h1>
</div>
</div>
</div>

<div class="container">
<div class="row">
<div class="col-12 pt-2 pb-4">
<div class="block__content">
<section>
<form action="/ticket" method="post">
<p class="my-1 orange" style="visibility:{{ if .Error }}visible{{ else }}hidden{{ end }};">
{{.Error}}</p>

<div class="mb-3 col col-lg-8 pl-0">
<label for="encodedInput" class="form-label">Encoded Base64 String</label>
<input type="text" name="encoded" size="64" class="form-control shadow-none" minlength="64"
maxlength="500" id="encodedInput" required placeholder="Encoded string" autocomplete="off" value="">
</div>
<button class="btn btn-primary d-block my-2" type="submit">Search</button>
</form>

{{ with .SearchResult }}
{{ template "ticket-search-result" . }}
{{ end }}
</section>
</div>
</div>
</div>
</div>

</div>
{{ template "footer" . }}
71 changes: 71 additions & 0 deletions webapi/ticket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) 2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package webapi

import (
"net/http"

"github.com/decred/vspd/database"
"github.com/gin-gonic/gin"
)

// ticketPage is the handler for "GET /ticket"
func (s *Server) ticketPage(c *gin.Context) {
c.HTML(http.StatusOK, "ticket.html", gin.H{
"WebApiCache": s.cache.getData(),
"WebApiCfg": s.cfg,
})
}

// ticketErrPage returns error message to the ticket page.
func (s *Server) ticketErrPage(c *gin.Context, status int, message string) {
c.HTML(status, "ticket.html", gin.H{
"WebApiCache": s.cache.getData(),
"WebApiCfg": s.cfg,
"Error": message,
})

}

// manualTicketSearch is the handler for "POST /ticket".
func (s *Server) manualTicketSearch(c *gin.Context) {
// Get values which have been added to context by middleware.
err := c.MustGet(errorKey)
if err != nil {
apiErr := err.(apiError)
s.ticketErrPage(c, apiErr.httpStatus(), apiErr.String())
return
}

ticket := c.MustGet(ticketKey).(database.Ticket)
knownTicket := c.MustGet(knownTicketKey).(bool)

voteChanges, err := s.db.GetVoteChanges(ticket.Hash)
if err != nil {
s.log.Errorf("db.GetVoteChanges error (ticket=%s): %v", ticket.Hash, err)
s.ticketErrPage(c, http.StatusBadRequest, "Error getting vote changes from database")
return
}

altSignAddrData, err := s.db.AltSignAddrData(ticket.Hash)
if err != nil {
s.log.Errorf("db.AltSignAddrData error (ticket=%s): %v", ticket.Hash, err)
s.ticketErrPage(c, http.StatusBadRequest, "Error getting alternate signature from database")
return
}

c.HTML(http.StatusOK, "ticket.html", gin.H{
"SearchResult": searchResult{
Hash: ticket.Hash,
Found: knownTicket,
Ticket: ticket,
AltSignAddrData: altSignAddrData,
VoteChanges: voteChanges,
MaxVoteChanges: s.cfg.MaxVoteChangeRecords,
},
"WebApiCache": s.cache.getData(),
"WebApiCfg": s.cfg,
})
}
3 changes: 3 additions & 0 deletions webapi/webapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const (
ticketKey = "Ticket"
knownTicketKey = "KnownTicket"
commitmentAddressKey = "CommitmentAddress"
errorKey = "Error"
)

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

router.GET("", s.homepage)
router.GET("/ticket", s.ticketPage)
router.POST("/ticket", s.withDcrdClient(dcrd), s.ticketSearchAuth, s.manualTicketSearch)

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

0 comments on commit 3f45dee

Please sign in to comment.