Rework client/server authentication. (#58)
* Rework client/server authentication. - Remove Signature from all requests, and instead expect a signature in HTTP header "VSP-Client-Signature". - Remove CommitmentSignatures from the database. - Use a bool flag to indicate when a ticket is missing from the database rather than an error. This commit introduces a lot of duplication into each of the authenticated HTTP handlers. This should be removed in future work which moves the authentication to a dedicated middleware. * Introduce auth and rpc middleware. This removed the duplication added in the previous commit, and also removes the duplication of RPC client error handling.
This commit is contained in:
parent
1ff55f4b30
commit
ac488464c0
@ -58,8 +58,11 @@ ticket details + fee to a VSP, and the VSP will take the fee and vote in return.
|
||||
|
||||
- When dcrvsp is started for the first time, it generates a ed25519 keypair and
|
||||
stores it in the database. This key is used to sign all API responses, and the
|
||||
signature is included in the response header `VSP-Signature`. Error responses
|
||||
signature is included in the response header `VSP-Server-Signature`. Error responses
|
||||
are not signed.
|
||||
- Every client request which references a ticket should include a HTTP header
|
||||
`VSP-Client-Signature`. The value of this header must be a signature of the
|
||||
request body, signed with the commitment address of the referenced ticket.
|
||||
- An xpub key is provided to dcrvsp via config. The first time dcrvsp starts, it
|
||||
imports this xpub to create a new wallet account. This account is used to
|
||||
derive addresses for fee payments.
|
||||
|
||||
@ -17,7 +17,6 @@ func exampleTicket() Ticket {
|
||||
return Ticket{
|
||||
Hash: "Hash",
|
||||
CommitmentAddress: "Address",
|
||||
CommitmentSignature: "CommitmentSignature",
|
||||
FeeAddress: "FeeAddress",
|
||||
SDiff: 1,
|
||||
BlockHeight: 2,
|
||||
@ -95,15 +94,17 @@ func testGetTicketByHash(t *testing.T) {
|
||||
}
|
||||
|
||||
// Retrieve ticket from database.
|
||||
retrieved, err := db.GetTicketByHash(ticket.Hash)
|
||||
retrieved, found, err := db.GetTicketByHash(ticket.Hash)
|
||||
if err != nil {
|
||||
t.Fatalf("error retrieving ticket by ticket hash: %v", err)
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("expected found==true")
|
||||
}
|
||||
|
||||
// Check ticket fields match expected.
|
||||
if retrieved.Hash != ticket.Hash ||
|
||||
retrieved.CommitmentAddress != ticket.CommitmentAddress ||
|
||||
retrieved.CommitmentSignature != ticket.CommitmentSignature ||
|
||||
retrieved.FeeAddress != ticket.FeeAddress ||
|
||||
retrieved.SDiff != ticket.SDiff ||
|
||||
retrieved.BlockHeight != ticket.BlockHeight ||
|
||||
@ -115,10 +116,13 @@ func testGetTicketByHash(t *testing.T) {
|
||||
t.Fatal("retrieved ticket value didnt match expected")
|
||||
}
|
||||
|
||||
// Error if non-existent ticket requested.
|
||||
_, err = db.GetTicketByHash("Not a real ticket hash")
|
||||
if err == nil {
|
||||
t.Fatal("expected an error while retrieving a non-existent ticket")
|
||||
// Check found==false when requesting a non-existent ticket.
|
||||
_, found, err = db.GetTicketByHash("Not a real ticket hash")
|
||||
if err != nil {
|
||||
t.Fatalf("error retrieving ticket by ticket hash: %v", err)
|
||||
}
|
||||
if found {
|
||||
t.Fatal("expected found==false")
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,7 +145,7 @@ func testSetTicketVotingKey(t *testing.T) {
|
||||
}
|
||||
|
||||
// Retrieve ticket from database.
|
||||
retrieved, err := db.GetTicketByHash(ticket.Hash)
|
||||
retrieved, _, err := db.GetTicketByHash(ticket.Hash)
|
||||
if err != nil {
|
||||
t.Fatalf("error retrieving ticket by ticket hash: %v", err)
|
||||
}
|
||||
@ -171,7 +175,7 @@ func testUpdateExpireAndFee(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get updated ticket
|
||||
retrieved, err := db.GetTicketByHash(ticket.Hash)
|
||||
retrieved, _, err := db.GetTicketByHash(ticket.Hash)
|
||||
if err != nil {
|
||||
t.Fatalf("error retrieving updated ticket: %v", err)
|
||||
}
|
||||
@ -199,7 +203,7 @@ func testUpdateVoteChoices(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get updated ticket
|
||||
retrieved, err := db.GetTicketByHash(ticket.Hash)
|
||||
retrieved, _, err := db.GetTicketByHash(ticket.Hash)
|
||||
if err != nil {
|
||||
t.Fatalf("error retrieving updated ticket: %v", err)
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ import (
|
||||
|
||||
type Ticket struct {
|
||||
Hash string `json:"hash"`
|
||||
CommitmentSignature string `json:"commitmentsignature"`
|
||||
CommitmentAddress string `json:"commitmentaddress"`
|
||||
FeeAddress string `json:"feeaddress"`
|
||||
SDiff float64 `json:"sdiff"`
|
||||
@ -82,14 +81,15 @@ func (vdb *VspDatabase) SetTicketVotingKey(ticketHash, votingKey string, voteCho
|
||||
})
|
||||
}
|
||||
|
||||
func (vdb *VspDatabase) GetTicketByHash(ticketHash string) (Ticket, error) {
|
||||
func (vdb *VspDatabase) GetTicketByHash(ticketHash string) (Ticket, bool, error) {
|
||||
var ticket Ticket
|
||||
var found bool
|
||||
err := vdb.db.View(func(tx *bolt.Tx) error {
|
||||
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
|
||||
|
||||
ticketBytes := ticketBkt.Get([]byte(ticketHash))
|
||||
if ticketBytes == nil {
|
||||
return ErrNoTicketFound
|
||||
return nil
|
||||
}
|
||||
|
||||
err := json.Unmarshal(ticketBytes, &ticket)
|
||||
@ -97,10 +97,12 @@ func (vdb *VspDatabase) GetTicketByHash(ticketHash string) (Ticket, error) {
|
||||
return fmt.Errorf("could not unmarshal ticket: %v", err)
|
||||
}
|
||||
|
||||
found = true
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return ticket, err
|
||||
return ticket, found, err
|
||||
}
|
||||
|
||||
func (vdb *VspDatabase) UpdateVoteChoices(ticketHash string, voteChoices map[string]string) error {
|
||||
|
||||
@ -2,11 +2,15 @@ package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
wallettypes "decred.org/dcrwallet/rpc/jsonrpc/types"
|
||||
"github.com/decred/dcrd/blockchain/stake/v3"
|
||||
"github.com/decred/dcrd/chaincfg/v3"
|
||||
"github.com/decred/dcrd/dcrutil/v3"
|
||||
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2"
|
||||
"github.com/decred/dcrd/wire"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -129,3 +133,25 @@ func (c *FeeWalletRPC) GetWalletFee() (dcrutil.Amount, error) {
|
||||
|
||||
return amount, nil
|
||||
}
|
||||
|
||||
func (c *FeeWalletRPC) GetTicketCommitmentAddress(ticketHash string, netParams *chaincfg.Params) (string, error) {
|
||||
resp, err := c.GetRawTransaction(ticketHash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
msgHex, err := hex.DecodeString(resp.Hex)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
msgTx := wire.NewMsgTx()
|
||||
if err = msgTx.FromBytes(msgHex); err != nil {
|
||||
return "", err
|
||||
}
|
||||
addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, netParams)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return addr.Address(), nil
|
||||
}
|
||||
|
||||
@ -1,66 +1,46 @@
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/decred/dcrd/blockchain/stake/v3"
|
||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrutil/v3"
|
||||
"github.com/decred/dcrd/wire"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/jholdstock/dcrvsp/database"
|
||||
"github.com/jholdstock/dcrvsp/rpc"
|
||||
)
|
||||
|
||||
// feeAddress is the handler for "POST /feeaddress".
|
||||
func feeAddress(c *gin.Context) {
|
||||
|
||||
// Get values which have been added to context by middleware.
|
||||
rawRequest := c.MustGet("RawRequest").([]byte)
|
||||
ticket := c.MustGet("Ticket").(database.Ticket)
|
||||
knownTicket := c.MustGet("KnownTicket").(bool)
|
||||
commitmentAddress := c.MustGet("CommitmentAddress").(string)
|
||||
fWalletClient := c.MustGet("FeeWalletClient").(*rpc.FeeWalletRPC)
|
||||
|
||||
var feeAddressRequest FeeAddressRequest
|
||||
if err := c.ShouldBindJSON(&feeAddressRequest); err != nil {
|
||||
if err := binding.JSON.BindBody(rawRequest, &feeAddressRequest); err != nil {
|
||||
log.Warnf("Bad feeaddress request from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse(err.Error(), http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate TicketHash.
|
||||
ticketHashStr := feeAddressRequest.TicketHash
|
||||
txHash, err := chainhash.NewHashFromStr(ticketHashStr)
|
||||
if err != nil {
|
||||
log.Warnf("Invalid ticket hash from %s", c.ClientIP())
|
||||
sendErrorResponse("invalid ticket hash", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Signature - sanity check signature is in base64 encoding.
|
||||
signature := feeAddressRequest.Signature
|
||||
if _, err = base64.StdEncoding.DecodeString(signature); err != nil {
|
||||
log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse("invalid signature", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for existing response
|
||||
ticket, err := db.GetTicketByHash(ticketHashStr)
|
||||
if err != nil && !errors.Is(err, database.ErrNoTicketFound) {
|
||||
log.Errorf("GetTicketByHash error: %v", err)
|
||||
sendErrorResponse("database error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
// Ticket already exists
|
||||
if signature == ticket.CommitmentSignature {
|
||||
// VSP already knows this ticket and has already issued it a fee address.
|
||||
if knownTicket {
|
||||
// If the expiry period has passed we need to issue a new fee.
|
||||
now := time.Now()
|
||||
expire := ticket.FeeExpiration
|
||||
VSPFee := ticket.VSPFee
|
||||
if ticket.FeeExpired() {
|
||||
if now.After(time.Unix(ticket.FeeExpiration, 0)) {
|
||||
expire = now.Add(cfg.FeeAddressExpiration).Unix()
|
||||
VSPFee = cfg.VSPFee
|
||||
|
||||
err = db.UpdateExpireAndFee(ticketHashStr, expire, VSPFee)
|
||||
err := db.UpdateExpireAndFee(ticket.Hash, expire, VSPFee)
|
||||
if err != nil {
|
||||
log.Errorf("UpdateExpireAndFee error: %v", err)
|
||||
sendErrorResponse("database error", http.StatusInternalServerError, c)
|
||||
@ -77,28 +57,16 @@ func feeAddress(c *gin.Context) {
|
||||
|
||||
return
|
||||
}
|
||||
log.Warnf("Invalid signature from %s", c.ClientIP())
|
||||
sendErrorResponse("invalid signature", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
fWalletConn, err := feeWalletConnect()
|
||||
if err != nil {
|
||||
log.Errorf("Fee wallet connection error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
fWalletClient, err := rpc.FeeWalletClient(ctx, fWalletConn)
|
||||
if err != nil {
|
||||
log.Errorf("Fee wallet client error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
// Beyond this point we are processing a new ticket which the VSP has not
|
||||
// seen before.
|
||||
|
||||
resp, err := fWalletClient.GetRawTransaction(txHash.String())
|
||||
ticketHash := feeAddressRequest.TicketHash
|
||||
|
||||
// Ensure ticket exists and is mined.
|
||||
resp, err := fWalletClient.GetRawTransaction(ticketHash)
|
||||
if err != nil {
|
||||
log.Warnf("Could not retrieve tx %s for %s: %v", txHash, c.ClientIP(), err)
|
||||
log.Warnf("Could not retrieve tx %s for %s: %v", ticketHash, c.ClientIP(), err)
|
||||
sendErrorResponse("unknown transaction", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
@ -137,23 +105,6 @@ func feeAddress(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get commitment address
|
||||
addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, cfg.NetParams)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get commitment address: %v", err)
|
||||
sendErrorResponse("failed to get commitment address", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
// verify message
|
||||
message := fmt.Sprintf("vsp v3 getfeeaddress %s", msgTx.TxHash())
|
||||
err = dcrutil.VerifyMessage(addr.Address(), signature, message, cfg.NetParams)
|
||||
if err != nil {
|
||||
log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse("invalid signature", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// get blockheight and sdiff which is required by
|
||||
// txrules.StakePoolTicketFee, and store them in the database
|
||||
// for processing by payfee
|
||||
@ -176,9 +127,8 @@ func feeAddress(c *gin.Context) {
|
||||
expire := now.Add(cfg.FeeAddressExpiration).Unix()
|
||||
|
||||
dbTicket := database.Ticket{
|
||||
Hash: txHash.String(),
|
||||
CommitmentSignature: signature,
|
||||
CommitmentAddress: addr.Address(),
|
||||
Hash: ticketHash,
|
||||
CommitmentAddress: commitmentAddress,
|
||||
FeeAddress: newAddress,
|
||||
SDiff: blockHeader.SBits,
|
||||
BlockHeight: int64(blockHeader.Height),
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/decred/dcrd/chaincfg/v3"
|
||||
"github.com/decred/dcrd/dcrutil/v3"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func currentVoteVersion(params *chaincfg.Params) uint32 {
|
||||
@ -41,3 +44,17 @@ 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")
|
||||
}
|
||||
|
||||
err := dcrutil.VerifyMessage(commitmentAddress, signature, string(reqBytes), cfg.NetParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
124
webapi/middleware.go
Normal file
124
webapi/middleware.go
Normal file
@ -0,0 +1,124 @@
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/jholdstock/dcrvsp/rpc"
|
||||
)
|
||||
|
||||
type ticketHashRequest struct {
|
||||
TicketHash string `json:"tickethash" binding:"required"`
|
||||
}
|
||||
|
||||
// withFeeWalletClient middleware adds a fee wallet client to the request
|
||||
// context for downstream handlers to make use of.
|
||||
func withFeeWalletClient() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
fWalletConn, err := feeWalletConnect()
|
||||
if err != nil {
|
||||
log.Errorf("Fee wallet connection error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
fWalletClient, err := rpc.FeeWalletClient(c, fWalletConn)
|
||||
if err != nil {
|
||||
log.Errorf("Fee wallet client error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("FeeWalletClient", fWalletClient)
|
||||
}
|
||||
}
|
||||
|
||||
// withVotingWalletClient middleware adds a voting wallet client to the request
|
||||
// context for downstream handlers to make use of.
|
||||
func withVotingWalletClient() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
vWalletConn, err := votingWalletConnect()
|
||||
if err != nil {
|
||||
log.Errorf("Voting wallet connection error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
vWalletClient, err := rpc.VotingWalletClient(c, vWalletConn)
|
||||
if err != nil {
|
||||
log.Errorf("Voting wallet client error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("VotingWalletClient", vWalletClient)
|
||||
}
|
||||
}
|
||||
|
||||
// vspAuth middleware reads the request body and extracts the ticket hash. 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 the VSP-Client-Signature header of the request
|
||||
// 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) {
|
||||
// Read request bytes.
|
||||
reqBytes, err := c.GetRawData()
|
||||
if err != nil {
|
||||
log.Warnf("Error reading request from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse(err.Error(), http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Add raw request to context for downstream handlers to use.
|
||||
c.Set("RawRequest", reqBytes)
|
||||
|
||||
// Parse request and ensure there is a ticket hash included.
|
||||
var request ticketHashRequest
|
||||
if err := binding.JSON.BindBody(reqBytes, &request); err != nil {
|
||||
log.Warnf("Bad request from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse(err.Error(), http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
hash := request.TicketHash
|
||||
|
||||
// Check if this ticket already appears in the database.
|
||||
ticket, ticketFound, err := db.GetTicketByHash(hash)
|
||||
if err != nil {
|
||||
log.Errorf("GetTicketByHash error: %v", err)
|
||||
sendErrorResponse("database error", http.StatusInternalServerError, 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
|
||||
if ticketFound {
|
||||
commitmentAddress = ticket.CommitmentAddress
|
||||
} else {
|
||||
fWalletClient := c.MustGet("FeeWalletClient").(*rpc.FeeWalletRPC)
|
||||
commitmentAddress, err = fWalletClient.GetTicketCommitmentAddress(hash, cfg.NetParams)
|
||||
if err != nil {
|
||||
log.Errorf("GetTicketCommitmentAddress error: %v", err)
|
||||
sendErrorResponse("database error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Validate request signature to ensure ticket ownership.
|
||||
err = validateSignature(reqBytes, commitmentAddress, c)
|
||||
if err != nil {
|
||||
log.Warnf("Bad signature from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse("bad signature", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Add ticket information to context so downstream handlers don't need
|
||||
// to access the db for it.
|
||||
c.Set("Ticket", ticket)
|
||||
c.Set("KnownTicket", ticketFound)
|
||||
c.Set("CommitmentAddress", commitmentAddress)
|
||||
}
|
||||
|
||||
}
|
||||
@ -12,32 +12,43 @@ import (
|
||||
"github.com/decred/dcrd/txscript/v3"
|
||||
"github.com/decred/dcrd/wire"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/jholdstock/dcrvsp/database"
|
||||
"github.com/jholdstock/dcrvsp/rpc"
|
||||
)
|
||||
|
||||
// payFee is the handler for "POST /payfee".
|
||||
func payFee(c *gin.Context) {
|
||||
var payFeeRequest PayFeeRequest
|
||||
if err := c.ShouldBindJSON(&payFeeRequest); err != nil {
|
||||
log.Warnf("Bad payfee request from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse(err.Error(), http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
ticket, err := db.GetTicketByHash(payFeeRequest.TicketHash)
|
||||
if err != nil {
|
||||
// Get values which have been added to context by middleware.
|
||||
rawRequest := c.MustGet("RawRequest").([]byte)
|
||||
ticket := c.MustGet("Ticket").(database.Ticket)
|
||||
knownTicket := c.MustGet("KnownTicket").(bool)
|
||||
fWalletClient := c.MustGet("FeeWalletClient").(*rpc.FeeWalletRPC)
|
||||
vWalletClient := c.MustGet("VotingWalletClient").(*rpc.VotingWalletRPC)
|
||||
|
||||
if !knownTicket {
|
||||
log.Warnf("Invalid ticket from %s", c.ClientIP())
|
||||
sendErrorResponse("invalid ticket", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Fee transaction has already been broadcast for this ticket.
|
||||
var payFeeRequest PayFeeRequest
|
||||
if err := binding.JSON.BindBody(rawRequest, &payFeeRequest); err != nil {
|
||||
log.Warnf("Bad payfee request from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse(err.Error(), http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Respond early if fee transaction has already been broadcast for this
|
||||
// ticket.
|
||||
if ticket.FeeTxHash != "" {
|
||||
sendJSONResponse(payFeeResponse{
|
||||
Timestamp: time.Now().Unix(),
|
||||
TxHash: ticket.FeeTxHash,
|
||||
Request: payFeeRequest,
|
||||
}, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate VotingKey.
|
||||
@ -129,21 +140,6 @@ findAddress:
|
||||
|
||||
sDiff := dcrutil.Amount(ticket.SDiff)
|
||||
|
||||
fWalletConn, err := feeWalletConnect()
|
||||
if err != nil {
|
||||
log.Errorf("Fee wallet connection error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
|
||||
fWalletClient, err := rpc.FeeWalletClient(ctx, fWalletConn)
|
||||
if err != nil {
|
||||
log.Errorf("Fee wallet client error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
relayFee, err := fWalletClient.GetWalletFee()
|
||||
if err != nil {
|
||||
log.Errorf("GetWalletFee failed: %v", err)
|
||||
@ -187,19 +183,6 @@ findAddress:
|
||||
return
|
||||
}
|
||||
|
||||
vWalletConn, err := votingWalletConnect()
|
||||
if err != nil {
|
||||
log.Errorf("Voting wallet connection error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
vWalletClient, err := rpc.VotingWalletClient(ctx, vWalletConn)
|
||||
if err != nil {
|
||||
log.Errorf("Voting wallet client error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
err = vWalletClient.AddTransaction(rawTicket.BlockHash, rawTicket.Hex)
|
||||
if err != nil {
|
||||
log.Errorf("AddTransaction failed: %v", err)
|
||||
|
||||
@ -1,85 +1,48 @@
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrutil/v3"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/jholdstock/dcrvsp/database"
|
||||
"github.com/jholdstock/dcrvsp/rpc"
|
||||
)
|
||||
|
||||
// setVoteChoices is the handler for "POST /setvotechoices".
|
||||
func setVoteChoices(c *gin.Context) {
|
||||
|
||||
// Get values which have been added to context by middleware.
|
||||
rawRequest := c.MustGet("RawRequest").([]byte)
|
||||
ticket := c.MustGet("Ticket").(database.Ticket)
|
||||
knownTicket := c.MustGet("KnownTicket").(bool)
|
||||
vWalletClient := c.MustGet("VotingWalletClient").(*rpc.VotingWalletRPC)
|
||||
|
||||
if !knownTicket {
|
||||
log.Warnf("Invalid ticket from %s", c.ClientIP())
|
||||
sendErrorResponse("invalid ticket", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
var setVoteChoicesRequest SetVoteChoicesRequest
|
||||
if err := c.ShouldBindJSON(&setVoteChoicesRequest); err != nil {
|
||||
if err := binding.JSON.BindBody(rawRequest, &setVoteChoicesRequest); err != nil {
|
||||
log.Warnf("Bad setvotechoices request from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse(err.Error(), http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate TicketHash.
|
||||
ticketHashStr := setVoteChoicesRequest.TicketHash
|
||||
txHash, err := chainhash.NewHashFromStr(ticketHashStr)
|
||||
if err != nil {
|
||||
log.Warnf("Invalid ticket hash from %s", c.ClientIP())
|
||||
sendErrorResponse("invalid ticket hash", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Signature - sanity check signature is in base64 encoding.
|
||||
signature := setVoteChoicesRequest.Signature
|
||||
if _, err = base64.StdEncoding.DecodeString(signature); err != nil {
|
||||
log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse("invalid signature", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
voteChoices := setVoteChoicesRequest.VoteChoices
|
||||
err = isValidVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), voteChoices)
|
||||
err := isValidVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), voteChoices)
|
||||
if err != nil {
|
||||
log.Warnf("Invalid votechoices from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse(err.Error(), http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
ticket, err := db.GetTicketByHash(txHash.String())
|
||||
if err != nil {
|
||||
log.Warnf("Invalid ticket from %s", c.ClientIP())
|
||||
sendErrorResponse("invalid ticket", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// verify message
|
||||
message := fmt.Sprintf("vsp v3 setvotechoices %d %s %v", setVoteChoicesRequest.Timestamp, txHash, voteChoices)
|
||||
err = dcrutil.VerifyMessage(ticket.CommitmentAddress, signature, message, cfg.NetParams)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to verify message from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse("message did not pass verification", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
vWalletConn, err := votingWalletConnect()
|
||||
if err != nil {
|
||||
log.Errorf("Voting wallet connection error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
vWalletClient, err := rpc.VotingWalletClient(ctx, vWalletConn)
|
||||
if err != nil {
|
||||
log.Errorf("Voting wallet client error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Update VoteChoices in the database before updating the wallets. DB is
|
||||
// source of truth and is less likely to error.
|
||||
err = db.UpdateVoteChoices(txHash.String(), voteChoices)
|
||||
err = db.UpdateVoteChoices(ticket.Hash, voteChoices)
|
||||
if err != nil {
|
||||
log.Errorf("UpdateVoteChoices error: %v", err)
|
||||
sendErrorResponse("database error", http.StatusInternalServerError, c)
|
||||
|
||||
@ -1,62 +1,39 @@
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrutil/v3"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/jholdstock/dcrvsp/database"
|
||||
)
|
||||
|
||||
// ticketStatus is the handler for "GET /ticketstatus".
|
||||
func ticketStatus(c *gin.Context) {
|
||||
var ticketStatusRequest TicketStatusRequest
|
||||
if err := c.ShouldBindJSON(&ticketStatusRequest); err != nil {
|
||||
log.Warnf("Bad ticketstatus request from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse(err.Error(), http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate TicketHash.
|
||||
ticketHashStr := ticketStatusRequest.TicketHash
|
||||
_, err := chainhash.NewHashFromStr(ticketHashStr)
|
||||
if err != nil {
|
||||
log.Warnf("Invalid ticket hash from %s", c.ClientIP())
|
||||
sendErrorResponse("invalid ticket hash", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
// Get values which have been added to context by middleware.
|
||||
rawRequest := c.MustGet("RawRequest").([]byte)
|
||||
ticket := c.MustGet("Ticket").(database.Ticket)
|
||||
knownTicket := c.MustGet("KnownTicket").(bool)
|
||||
|
||||
// Validate Signature - sanity check signature is in base64 encoding.
|
||||
signature := ticketStatusRequest.Signature
|
||||
if _, err = base64.StdEncoding.DecodeString(signature); err != nil {
|
||||
log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse("invalid signature", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
ticket, err := db.GetTicketByHash(ticketHashStr)
|
||||
if err != nil {
|
||||
if !knownTicket {
|
||||
log.Warnf("Invalid ticket from %s", c.ClientIP())
|
||||
sendErrorResponse("invalid ticket", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// verify message
|
||||
message := fmt.Sprintf("vsp v3 ticketstatus %d %s", ticketStatusRequest.Timestamp, ticketHashStr)
|
||||
err = dcrutil.VerifyMessage(ticket.CommitmentAddress, signature, message, cfg.NetParams)
|
||||
if err != nil {
|
||||
log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse("invalid signature", http.StatusBadRequest, c)
|
||||
var ticketStatusRequest TicketStatusRequest
|
||||
if err := binding.JSON.BindBody(rawRequest, &ticketStatusRequest); err != nil {
|
||||
log.Warnf("Bad ticketstatus request from %s: %v", c.ClientIP(), err)
|
||||
sendErrorResponse(err.Error(), http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
sendJSONResponse(ticketStatusResponse{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Request: ticketStatusRequest,
|
||||
Status: "active", // TODO - active, pending, expired (missed, revoked?)
|
||||
Status: "active",
|
||||
VoteChoices: ticket.VoteChoices,
|
||||
}, c)
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@ type feeResponse struct {
|
||||
type FeeAddressRequest struct {
|
||||
Timestamp int64 `json:"timestamp" binding:"required"`
|
||||
TicketHash string `json:"tickethash" binding:"required"`
|
||||
Signature string `json:"signature" binding:"required"`
|
||||
}
|
||||
|
||||
type feeAddressResponse struct {
|
||||
@ -41,7 +40,6 @@ type payFeeResponse struct {
|
||||
type SetVoteChoicesRequest struct {
|
||||
Timestamp int64 `json:"timestamp" binding:"required"`
|
||||
TicketHash string `json:"tickethash" binding:"required"`
|
||||
Signature string `json:"commitmentsignature" binding:"required"`
|
||||
VoteChoices map[string]string `json:"votechoices" binding:"required"`
|
||||
}
|
||||
|
||||
@ -54,7 +52,6 @@ type setVoteChoicesResponse struct {
|
||||
type TicketStatusRequest struct {
|
||||
Timestamp int64 `json:"timestamp" binding:"required"`
|
||||
TicketHash string `json:"tickethash" binding:"required"`
|
||||
Signature string `json:"signature" binding:"required"`
|
||||
}
|
||||
|
||||
type ticketStatusResponse struct {
|
||||
|
||||
@ -145,17 +145,25 @@ func router(debugMode bool) *gin.Engine {
|
||||
// Serve static web resources
|
||||
router.Static("/public", "webapi/public/")
|
||||
|
||||
// These routes have no extra middleware. They can be accessed by anybody.
|
||||
router.GET("/", homepage)
|
||||
router.GET("/api/fee", fee)
|
||||
router.GET("/api/pubkey", pubKey)
|
||||
|
||||
api := router.Group("/api")
|
||||
{
|
||||
api.GET("/fee", fee)
|
||||
api.POST("/feeaddress", feeAddress)
|
||||
api.GET("/pubkey", pubKey)
|
||||
api.POST("/payfee", payFee)
|
||||
api.POST("/setvotechoices", setVoteChoices)
|
||||
api.GET("/ticketstatus", ticketStatus)
|
||||
}
|
||||
// These API routes access the fee wallet and they need authentication.
|
||||
feeOnly := router.Group("/api").Use(
|
||||
withFeeWalletClient(), vspAuth(),
|
||||
)
|
||||
feeOnly.POST("/feeaddress", feeAddress)
|
||||
feeOnly.GET("/ticketstatus", ticketStatus)
|
||||
|
||||
// These API routes access the fee wallet and the voting wallets, and they
|
||||
// need authentication.
|
||||
both := router.Group("/api").Use(
|
||||
withFeeWalletClient(), withVotingWalletClient(), vspAuth(),
|
||||
)
|
||||
both.POST("/payfee", payFee)
|
||||
both.POST("/setvotechoices", setVoteChoices)
|
||||
|
||||
return router
|
||||
}
|
||||
@ -188,7 +196,7 @@ func sendJSONResponse(resp interface{}, c *gin.Context) {
|
||||
}
|
||||
|
||||
sig := ed25519.Sign(cfg.SignKey, dec)
|
||||
c.Writer.Header().Set("VSP-Signature", hex.EncodeToString(sig))
|
||||
c.Writer.Header().Set("VSP-Server-Signature", hex.EncodeToString(sig))
|
||||
|
||||
c.AbortWithStatusJSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user