webapi: Add setaltsig endpoint.
Allow setting the alternate public key that signs json requests from the client. Save a copy of the request and signature in the db.
This commit is contained in:
parent
6e13d23214
commit
6191ddb7c0
124
webapi/setaltsig.go
Normal file
124
webapi/setaltsig.go
Normal file
@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2021 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 (
|
||||
"time"
|
||||
|
||||
"github.com/decred/dcrd/chaincfg/v3"
|
||||
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v3"
|
||||
"github.com/decred/dcrd/txscript/v4/stdaddr"
|
||||
"github.com/decred/vspd/database"
|
||||
"github.com/decred/vspd/rpc"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
)
|
||||
|
||||
// Ensure that Node is satisfied by *rpc.DcrdRPC.
|
||||
var _ Node = (*rpc.DcrdRPC)(nil)
|
||||
|
||||
// Node is satisfied by *rpc.DcrdRPC and retrieves data from the blockchain.
|
||||
type Node interface {
|
||||
CanTicketVote(rawTx *dcrdtypes.TxRawResult, ticketHash string, netParams *chaincfg.Params) (bool, error)
|
||||
GetRawTransaction(txHash string) (*dcrdtypes.TxRawResult, error)
|
||||
}
|
||||
|
||||
// setAltSig is the handler for "POST /api/v3/setaltsig".
|
||||
func setAltSig(c *gin.Context) {
|
||||
|
||||
const funcName = "setAltSig"
|
||||
|
||||
// Get values which have been added to context by middleware.
|
||||
dcrdClient := c.MustGet("DcrdClient").(Node)
|
||||
reqBytes := c.MustGet("RequestBytes").([]byte)
|
||||
|
||||
if cfg.VspClosed {
|
||||
sendError(errVspClosed, c)
|
||||
return
|
||||
}
|
||||
|
||||
var request SetAltSigRequest
|
||||
if err := binding.JSON.BindBody(reqBytes, &request); err != nil {
|
||||
log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
|
||||
sendErrorWithMsg(err.Error(), errBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
altSigAddr, ticketHash := request.AltSigAddress, request.TicketHash
|
||||
|
||||
currentData, err := db.AltSigData(ticketHash)
|
||||
if err != nil {
|
||||
log.Errorf("%s: db.AltSigData (ticketHash=%s): %v", funcName, ticketHash, err)
|
||||
sendError(errInternalError, c)
|
||||
return
|
||||
}
|
||||
if currentData != nil {
|
||||
msg := "alternate signature data already exists"
|
||||
log.Warnf("%s: %s (ticketHash=%s)", funcName, msg, ticketHash)
|
||||
sendErrorWithMsg(msg, errBadRequest, c)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// Fail fast if the pubkey doesn't decode properly.
|
||||
addr, err := stdaddr.DecodeAddressV0(altSigAddr, cfg.NetParams)
|
||||
if err != nil {
|
||||
log.Warnf("%s: Alt sig address cannot be decoded (clientIP=%s): %v", funcName, c.ClientIP(), err)
|
||||
sendErrorWithMsg(err.Error(), errBadRequest, c)
|
||||
return
|
||||
}
|
||||
if _, ok := addr.(*stdaddr.AddressPubKeyHashEcdsaSecp256k1V0); !ok {
|
||||
log.Warnf("%s: Alt sig address is unexpected type (clientIP=%s, type=%T)", funcName, c.ClientIP(), addr)
|
||||
sendErrorWithMsg("wrong type for alternate signing address", errBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Get ticket details.
|
||||
rawTicket, err := dcrdClient.GetRawTransaction(ticketHash)
|
||||
if err != nil {
|
||||
log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v", funcName, ticketHash, err)
|
||||
sendError(errInternalError, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure this ticket is eligible to vote at some point in the future.
|
||||
canVote, err := dcrdClient.CanTicketVote(rawTicket, ticketHash, cfg.NetParams)
|
||||
if err != nil {
|
||||
log.Errorf("%s: dcrd.CanTicketVote error (ticketHash=%s): %v", funcName, ticketHash, err)
|
||||
sendError(errInternalError, c)
|
||||
return
|
||||
}
|
||||
if !canVote {
|
||||
log.Warnf("%s: unvotable ticket (clientIP=%s, ticketHash=%s)",
|
||||
funcName, c.ClientIP(), ticketHash)
|
||||
sendError(errTicketCannotVote, c)
|
||||
return
|
||||
}
|
||||
|
||||
sigStr := c.GetHeader("VSP-Client-Signature")
|
||||
|
||||
// Send success response to client.
|
||||
res, resSig := sendJSONResponse(SetAltSigResponse{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Request: reqBytes,
|
||||
}, c)
|
||||
|
||||
data := &database.AltSigData{
|
||||
AltSigAddr: altSigAddr,
|
||||
Req: reqBytes,
|
||||
ReqSig: sigStr,
|
||||
Res: []byte(res),
|
||||
ResSig: resSig,
|
||||
}
|
||||
|
||||
err = db.InsertAltSig(ticketHash, data)
|
||||
if err != nil {
|
||||
log.Errorf("%s: db.InsertAltSig error, failed to set alt data (ticketHash=%s): %v",
|
||||
funcName, ticketHash, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("%s: New alt sig pubkey set for ticket: (ticketHash=%s)", funcName, ticketHash)
|
||||
}
|
||||
247
webapi/setaltsig_test.go
Normal file
247
webapi/setaltsig_test.go
Normal file
@ -0,0 +1,247 @@
|
||||
// Copyright (c) 2020-2021 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/decred/dcrd/chaincfg/v3"
|
||||
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v3"
|
||||
"github.com/decred/slog"
|
||||
"github.com/decred/vspd/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
sigCharset = "0123456789ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/="
|
||||
hexCharset = "1234567890abcdef"
|
||||
testDb = "test.db"
|
||||
backupDb = "test.db-backup"
|
||||
)
|
||||
|
||||
var (
|
||||
seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
feeXPub = "feexpub"
|
||||
maxVoteChangeRecords = 3
|
||||
)
|
||||
|
||||
func randBytes(n int) []byte {
|
||||
slice := make([]byte, n)
|
||||
if _, err := seededRand.Read(slice); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Set test logger to stdout.
|
||||
backend := slog.NewBackend(os.Stdout)
|
||||
log = backend.Logger("test")
|
||||
log.SetLevel(slog.LevelTrace)
|
||||
|
||||
// Set up some global params.
|
||||
cfg.NetParams = chaincfg.MainNetParams()
|
||||
_, signPrivKey, _ = ed25519.GenerateKey(seededRand)
|
||||
|
||||
// Create a database to use.
|
||||
// Ensure we are starting with a clean environment.
|
||||
os.Remove(testDb)
|
||||
os.Remove(backupDb)
|
||||
|
||||
// Create a new blank database for all tests.
|
||||
var err error
|
||||
var wg sync.WaitGroup
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
err = database.CreateNew(testDb, feeXPub)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("error creating test database: %v", err))
|
||||
}
|
||||
db, err = database.Open(ctx, &wg, testDb, time.Hour, maxVoteChangeRecords)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("error opening test database: %v", err))
|
||||
}
|
||||
|
||||
// Run tests.
|
||||
exitCode := m.Run()
|
||||
|
||||
// Request database shutdown and wait for it to complete.
|
||||
cancel()
|
||||
wg.Wait()
|
||||
db.Close()
|
||||
os.Remove(testDb)
|
||||
os.Remove(backupDb)
|
||||
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
// randString randomly generates a string of the requested length, using only
|
||||
// characters from the provided charset.
|
||||
func randString(length int, charset string) string {
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[seededRand.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Ensure that testNode satisfies Node.
|
||||
var _ Node = (*testNode)(nil)
|
||||
|
||||
type testNode struct {
|
||||
canTicketVote bool
|
||||
canTicketVoteErr error
|
||||
getRawTransactionErr error
|
||||
}
|
||||
|
||||
func (n *testNode) CanTicketVote(_ *dcrdtypes.TxRawResult, _ string, _ *chaincfg.Params) (bool, error) {
|
||||
return n.canTicketVote, n.canTicketVoteErr
|
||||
}
|
||||
|
||||
func (n *testNode) GetRawTransaction(txHash string) (*dcrdtypes.TxRawResult, error) {
|
||||
return nil, n.getRawTransactionErr
|
||||
}
|
||||
|
||||
func TestSetAltSig(t *testing.T) {
|
||||
const testAddr = "DsVoDXNQqyF3V83PJJ5zMdnB4pQuJHBAh15"
|
||||
tests := []struct {
|
||||
name string
|
||||
vspClosed bool
|
||||
deformReq int
|
||||
addr string
|
||||
getRawTransactionErr error
|
||||
canTicketNotVote bool
|
||||
isExistingAltSig bool
|
||||
canTicketVoteErr error
|
||||
wantCode int
|
||||
}{{
|
||||
name: "ok",
|
||||
addr: testAddr,
|
||||
wantCode: http.StatusOK,
|
||||
}, {
|
||||
name: "vsp closed",
|
||||
vspClosed: true,
|
||||
wantCode: http.StatusBadRequest,
|
||||
}, {
|
||||
name: "bad request",
|
||||
deformReq: 1,
|
||||
wantCode: http.StatusBadRequest,
|
||||
}, {
|
||||
name: "bad addr",
|
||||
addr: "xxx",
|
||||
wantCode: http.StatusBadRequest,
|
||||
}, {
|
||||
name: "addr wrong type",
|
||||
addr: "DkM3ZigNyiwHrsXRjkDQ8t8tW6uKGW9g61qEkG3bMqQPQWYEf5X3J",
|
||||
wantCode: http.StatusBadRequest,
|
||||
}, {
|
||||
name: "error getting raw tx from dcrd client",
|
||||
addr: testAddr,
|
||||
getRawTransactionErr: errors.New("get raw transaction error"),
|
||||
wantCode: http.StatusInternalServerError,
|
||||
}, {
|
||||
name: "error getting can vote from dcrd client",
|
||||
addr: testAddr,
|
||||
canTicketVoteErr: errors.New("can ticket vote error"),
|
||||
wantCode: http.StatusInternalServerError,
|
||||
}, {
|
||||
name: "ticket can't vote",
|
||||
addr: testAddr,
|
||||
canTicketNotVote: true,
|
||||
wantCode: http.StatusBadRequest,
|
||||
}, {
|
||||
name: "hist at max",
|
||||
addr: testAddr,
|
||||
isExistingAltSig: true,
|
||||
wantCode: http.StatusBadRequest,
|
||||
}}
|
||||
|
||||
for _, test := range tests {
|
||||
ticketHash := randString(64, hexCharset)
|
||||
req := &SetAltSigRequest{
|
||||
Timestamp: time.Now().Unix(),
|
||||
TicketHash: ticketHash,
|
||||
TicketHex: randString(504, hexCharset),
|
||||
ParentHex: randString(504, hexCharset),
|
||||
AltSigAddress: test.addr,
|
||||
}
|
||||
reqSig := randString(504, hexCharset)
|
||||
b, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if test.isExistingAltSig {
|
||||
data := &database.AltSigData{
|
||||
AltSigAddr: test.addr,
|
||||
Req: b,
|
||||
ReqSig: reqSig,
|
||||
Res: randBytes(1000),
|
||||
ResSig: randString(96, sigCharset),
|
||||
}
|
||||
if err := db.InsertAltSig(ticketHash, data); err != nil {
|
||||
t.Fatalf("%q: unable to insert ticket: %v", test.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
cfg.VspClosed = test.vspClosed
|
||||
|
||||
tNode := &testNode{
|
||||
canTicketVote: !test.canTicketNotVote,
|
||||
canTicketVoteErr: test.canTicketVoteErr,
|
||||
getRawTransactionErr: test.getRawTransactionErr,
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
c, r := gin.CreateTestContext(w)
|
||||
|
||||
handle := func(c *gin.Context) {
|
||||
c.Set("DcrdClient", tNode)
|
||||
c.Set("RequestBytes", b[test.deformReq:])
|
||||
setAltSig(c)
|
||||
}
|
||||
|
||||
r.POST("/", handle)
|
||||
|
||||
c.Request, err = http.NewRequest(http.MethodPost, "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c.Request.Header.Set("VSP-Client-Signature", reqSig)
|
||||
|
||||
r.ServeHTTP(w, c.Request)
|
||||
|
||||
if test.wantCode != w.Code {
|
||||
t.Errorf("%q: expected status %d, got %d", test.name, test.wantCode, w.Code)
|
||||
}
|
||||
|
||||
altsig, err := db.AltSigData(ticketHash)
|
||||
if err != nil {
|
||||
t.Fatalf("%q: unable to get alt sig data: %v", test.name, err)
|
||||
}
|
||||
|
||||
if test.wantCode != http.StatusOK && !test.isExistingAltSig {
|
||||
if altsig != nil {
|
||||
t.Fatalf("%q: expected no alt sig saved for errored state", test.name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !bytes.Equal(b, altsig.Req) || altsig.ReqSig != reqSig {
|
||||
t.Fatalf("%q: expected alt sig data different than actual", test.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -71,3 +71,16 @@ type ticketStatusResponse struct {
|
||||
VoteChoices map[string]string `json:"votechoices"`
|
||||
Request []byte `json:"request"`
|
||||
}
|
||||
|
||||
type SetAltSigRequest struct {
|
||||
Timestamp int64 `json:"timestamp" binding:"required"`
|
||||
TicketHash string `json:"tickethash" binding:"required"`
|
||||
TicketHex string `json:"tickethex" binding:"required"`
|
||||
ParentHex string `json:"parenthex" binding:"required"`
|
||||
AltSigAddress string `json:"altsigaddress" binding:"required"`
|
||||
}
|
||||
|
||||
type SetAltSigResponse struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Request []byte `json:"request"`
|
||||
}
|
||||
|
||||
@ -214,6 +214,7 @@ func router(debugMode bool, cookieSecret []byte, dcrd rpc.DcrdConnect, wallets r
|
||||
|
||||
api := router.Group("/api/v3")
|
||||
api.GET("/vspinfo", vspInfo)
|
||||
api.POST("/setaltsig", withDcrdClient(dcrd), broadcastTicket(), vspAuth(), setAltSig)
|
||||
api.POST("/feeaddress", withDcrdClient(dcrd), broadcastTicket(), vspAuth(), feeAddress)
|
||||
api.POST("/ticketstatus", withDcrdClient(dcrd), vspAuth(), ticketStatus)
|
||||
api.POST("/payfee", withDcrdClient(dcrd), vspAuth(), payFee)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user