diff --git a/README.md b/README.md index ce79825..79e9cd0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ ## Design decisions - [gin-gonic](https://github.com/gin-gonic/gin) webserver for both front-end and API. - - API uses JSON encoded reqs/resps in HTTP body. + - Success responses use HTTP status 200 and a JSON encoded body. + - Error responses use either HTTP status 500 or 400, and a JSON encoded error in the body (eg. `{"error":"Description"}') - [bbolt](https://github.com/etcd-io/bbolt) k/v database. - Tickets are stored in a single bucket, using ticket hash as the key and a json encoded representation of the ticket as the value. @@ -33,6 +34,10 @@ - Accountability for both client and server changes to voting preferences. - Consistency checking across connected wallets. +## Notes + +- dcrd must have transaction index enabled so `getrawtransaction` can be used. + ## Issue Tracker The [integrated github issue tracker](https://github.com/jholdstock/dcrvsp/issues) diff --git a/database/database_test.go b/database/database_test.go index da5f290..226d011 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -9,7 +9,11 @@ import ( var ( testDb = "test.db" - ticket = Ticket{ + db *VspDatabase +) + +func exampleTicket() Ticket { + return Ticket{ Hash: "Hash", CommitmentAddress: "Address", CommitmentSignature: "CommitmentSignature", @@ -20,8 +24,7 @@ var ( VotingKey: "VotingKey", Expiration: 4, } - db *VspDatabase -) +} // TestDatabase runs all database tests. func TestDatabase(t *testing.T) { @@ -60,6 +63,7 @@ func TestDatabase(t *testing.T) { func testInsertFeeAddress(t *testing.T) { // Insert a ticket into the database. + ticket := exampleTicket() err := db.InsertFeeAddress(ticket) if err != nil { t.Fatalf("error storing ticket in database: %v", err) @@ -70,9 +74,17 @@ func testInsertFeeAddress(t *testing.T) { if err == nil { t.Fatal("expected an error inserting ticket with duplicate hash") } + + // Inserting a ticket with empty hash should fail. + ticket.Hash = "" + err = db.InsertFeeAddress(ticket) + if err == nil { + t.Fatal("expected an error inserting ticket with no hash") + } } func testGetTicketByHash(t *testing.T) { + ticket := exampleTicket() // Insert a ticket into the database. err := db.InsertFeeAddress(ticket) if err != nil { @@ -107,6 +119,7 @@ func testGetTicketByHash(t *testing.T) { func testGetFeesByFeeAddress(t *testing.T) { // Insert a ticket into the database. + ticket := exampleTicket() err := db.InsertFeeAddress(ticket) if err != nil { t.Fatalf("error storing ticket in database: %v", err) @@ -145,6 +158,7 @@ func testGetFeesByFeeAddress(t *testing.T) { func testInsertFeeAddressVotingKey(t *testing.T) { // Insert a ticket into the database. + ticket := exampleTicket() err := db.InsertFeeAddress(ticket) if err != nil { t.Fatalf("error storing ticket in database: %v", err) @@ -173,6 +187,7 @@ func testInsertFeeAddressVotingKey(t *testing.T) { func testGetInactiveFeeAddresses(t *testing.T) { // Insert a ticket into the database. + ticket := exampleTicket() err := db.InsertFeeAddress(ticket) if err != nil { t.Fatalf("error storing ticket in database: %v", err) diff --git a/database/ticket.go b/database/ticket.go index 004c49f..cf9b121 100644 --- a/database/ticket.go +++ b/database/ticket.go @@ -25,10 +25,11 @@ var ( ) func (vdb *VspDatabase) InsertFeeAddress(ticket Ticket) error { + hashBytes := []byte(ticket.Hash) return vdb.db.Update(func(tx *bolt.Tx) error { ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK) - if ticketBkt.Get([]byte(ticket.Hash)) != nil { + if ticketBkt.Get(hashBytes) != nil { return fmt.Errorf("ticket already exists with hash %s", ticket.Hash) } @@ -37,7 +38,7 @@ func (vdb *VspDatabase) InsertFeeAddress(ticket Ticket) error { return err } - return ticketBkt.Put([]byte(ticket.Hash), ticketBytes) + return ticketBkt.Put(hashBytes, ticketBytes) }) } diff --git a/run_tests.sh b/run_tests.sh index 6264355..9e44ae9 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -30,5 +30,6 @@ golangci-lint run --disable-all --deadline=10m \ --enable=unparam \ --enable=deadcode \ --enable=unused \ + --enable=errcheck \ --enable=asciicheck -# --enable=errcheck \ \ No newline at end of file + \ No newline at end of file diff --git a/webapi/methods.go b/webapi/methods.go index ca8a437..84ddba9 100644 --- a/webapi/methods.go +++ b/webapi/methods.go @@ -2,16 +2,16 @@ package webapi import ( "bytes" - "context" "crypto/ed25519" "encoding/base64" "encoding/hex" "encoding/json" - "errors" "fmt" "net/http" "time" + "github.com/jholdstock/dcrvsp/database" + "decred.org/dcrwallet/wallet/txrules" "github.com/decred/dcrd/blockchain/stake/v3" "github.com/decred/dcrd/chaincfg/chainhash" @@ -21,7 +21,6 @@ import ( "github.com/decred/dcrd/txscript/v3" "github.com/decred/dcrd/wire" "github.com/gin-gonic/gin" - "github.com/jholdstock/dcrvsp/database" ) const ( @@ -32,15 +31,18 @@ func sendJSONResponse(resp interface{}, c *gin.Context) { dec, err := json.Marshal(resp) if err != nil { log.Errorf("JSON marshal error: %v", err) - c.AbortWithStatus(http.StatusInternalServerError) + sendErrorResponse("failed to marshal json", http.StatusInternalServerError, c) return } sig := ed25519.Sign(cfg.SignKey, dec) - c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8") c.Writer.Header().Set("VSP-Signature", hex.EncodeToString(sig)) - c.Writer.WriteHeader(http.StatusOK) - c.Writer.Write(dec) + + c.JSON(http.StatusOK, resp) +} + +func sendErrorResponse(errMsg string, code int, c *gin.Context) { + c.JSON(code, gin.H{"error": errMsg}) } func pubKey(c *gin.Context) { @@ -58,31 +60,28 @@ func fee(c *gin.Context) { } func feeAddress(c *gin.Context) { - dec := json.NewDecoder(c.Request.Body) var feeAddressRequest FeeAddressRequest - err := dec.Decode(&feeAddressRequest) - if err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid json")) + if err := c.ShouldBindJSON(&feeAddressRequest); err != nil { + log.Warnf("Bad request from %s", c.ClientIP()) + sendErrorResponse(err.Error(), http.StatusBadRequest, c) return } // ticketHash ticketHashStr := feeAddressRequest.TicketHash - if len(ticketHashStr) != chainhash.MaxHashStringSize { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid ticket hash")) - return - } txHash, err := chainhash.NewHashFromStr(ticketHashStr) if err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid ticket hash")) + log.Warnf("Invalid ticket hash from %s", c.ClientIP()) + sendErrorResponse("invalid ticket hash", http.StatusBadRequest, c) return } // signature - sanity check signature is in base64 encoding signature := feeAddressRequest.Signature if _, err = base64.StdEncoding.DecodeString(signature); err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid signature")) + log.Warnf("Invalid signature from %s", c.ClientIP()) + sendErrorResponse("invalid signature", http.StatusBadRequest, c) return } @@ -109,53 +108,62 @@ func feeAddress(c *gin.Context) { } */ - ctx := c.Request.Context() - walletClient, err := walletRPC() if err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("wallet RPC error")) + log.Errorf("Failed to dial dcrwallet RPC: %v", err) + sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) return } + ctx := c.Request.Context() + var resp dcrdtypes.TxRawResult - err = walletClient.Call(ctx, "getrawtransaction", &resp, txHash.String(), true) + err = walletClient.Call(ctx, "getrawtransaction", &resp, txHash.String(), 1) if err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("unknown transaction")) + log.Warnf("Could not retrieve tx for %s", c.ClientIP()) + sendErrorResponse("unknown transaction", http.StatusBadRequest, c) return } if resp.Confirmations < 2 || resp.BlockHeight < 0 { - c.AbortWithError(http.StatusBadRequest, errors.New("transaction does not have minimum confirmations")) + log.Warnf("Not enough confs for tx from %s", c.ClientIP()) + sendErrorResponse("transaction does not have minimum confirmations", http.StatusBadRequest, c) return } if resp.Confirmations > int64(uint32(cfg.NetParams.TicketMaturity)+cfg.NetParams.TicketExpiry) { - c.AbortWithError(http.StatusBadRequest, errors.New("transaction too old")) + log.Warnf("Too old tx from %s", c.ClientIP()) + sendErrorResponse("transaction too old", http.StatusBadRequest, c) return } msgHex, err := hex.DecodeString(resp.Hex) if err != nil { - c.AbortWithError(http.StatusInternalServerError, errors.New("unable to decode transaction")) + log.Errorf("Failed to decode tx: %v", err) + sendErrorResponse("unable to decode transaction", http.StatusInternalServerError, c) return } msgTx := wire.NewMsgTx() if err = msgTx.FromBytes(msgHex); err != nil { - c.AbortWithError(http.StatusInternalServerError, errors.New("failed to deserialize transaction")) + log.Errorf("Failed to deserialize tx: %v", err) + sendErrorResponse("failed to deserialize transaction", http.StatusInternalServerError, c) return } if !stake.IsSStx(msgTx) { - c.AbortWithError(http.StatusBadRequest, errors.New("transaction is not a ticket")) + log.Warnf("Non-ticket tx from %s", c.ClientIP()) + sendErrorResponse("transaction is not a ticket", http.StatusBadRequest, c) return } if len(msgTx.TxOut) != 3 { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid ticket")) + log.Warnf("Invalid ticket from %s", c.ClientIP()) + sendErrorResponse("invalid ticket", http.StatusBadRequest, c) return } // Get commitment address addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, cfg.NetParams) if err != nil { - c.AbortWithError(http.StatusInternalServerError, errors.New("failed to get commitment address")) + log.Errorf("Failed to get commitment address: %v", err) + sendErrorResponse("failed to get commitment address", http.StatusInternalServerError, c) return } @@ -163,7 +171,8 @@ func feeAddress(c *gin.Context) { message := fmt.Sprintf("vsp v3 getfeeaddress %s", msgTx.TxHash()) err = dcrutil.VerifyMessage(addr.Address(), signature, message, cfg.NetParams) if err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid signature")) + log.Warnf("Invalid signature from %s", c.ClientIP()) + sendErrorResponse("invalid signature", http.StatusBadRequest, c) return } @@ -173,14 +182,16 @@ func feeAddress(c *gin.Context) { var blockHeader dcrdtypes.GetBlockHeaderVerboseResult err = walletClient.Call(ctx, "getblockheader", &blockHeader, resp.BlockHash, true) if err != nil { - c.AbortWithError(http.StatusInternalServerError, errors.New("RPC server error")) + log.Errorf("GetBlockHeader error: %v", err) + sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) return } var newAddress string err = walletClient.Call(ctx, "getnewaddress", &newAddress, "fees") if err != nil { - c.AbortWithError(http.StatusInternalServerError, errors.New("unable to generate fee address")) + log.Errorf("GetNewAddress error: %v", err) + sendErrorResponse("unable to generate fee address", http.StatusInternalServerError, c) return } @@ -199,10 +210,10 @@ func feeAddress(c *gin.Context) { // VotingKey: set during payfee } - // TODO: Insert into DB err = db.InsertFeeAddress(dbTicket) if err != nil { - c.AbortWithError(http.StatusInternalServerError, errors.New("database error")) + log.Errorf("InsertFeeAddress error: %v", err) + sendErrorResponse("database error", http.StatusInternalServerError, c) return } @@ -215,19 +226,18 @@ func feeAddress(c *gin.Context) { } func payFee(c *gin.Context) { - dec := json.NewDecoder(c.Request.Body) - var payFeeRequest PayFeeRequest - err := dec.Decode(&payFeeRequest) - if err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid json")) + if err := c.ShouldBindJSON(&payFeeRequest); err != nil { + log.Warnf("Bad request from %s", c.ClientIP()) + sendErrorResponse(err.Error(), http.StatusBadRequest, c) return } votingKey := payFeeRequest.VotingKey votingWIF, err := dcrutil.DecodeWIF(votingKey, cfg.NetParams.PrivateKeyID) if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) + log.Errorf("Failed to decode WIF: %v", err) + sendErrorResponse("error decoding WIF", http.StatusInternalServerError, c) return } @@ -236,7 +246,8 @@ func payFee(c *gin.Context) { feeTx := wire.NewMsgTx() err = feeTx.FromBytes(payFeeRequest.Hex) if err != nil { - c.AbortWithError(http.StatusInternalServerError, errors.New("unable to deserialize transaction")) + log.Errorf("Failed to deserialize tx: %v", err) + sendErrorResponse("unable to deserialize transaction", http.StatusInternalServerError, c) return } @@ -244,8 +255,8 @@ func payFee(c *gin.Context) { validFeeAddrs, err := db.GetInactiveFeeAddresses() if err != nil { - log.Errorf("database error: %v", err) - c.AbortWithError(http.StatusInternalServerError, errors.New("database error")) + log.Errorf("GetInactiveFeeAddresses error: %v", err) + sendErrorResponse("database error", http.StatusInternalServerError, c) return } @@ -258,8 +269,8 @@ findAddress: _, addresses, _, err := txscript.ExtractPkScriptAddrs(scriptVersion, txOut.PkScript, cfg.NetParams) if err != nil { - fmt.Printf("Extract: %v", err) - c.AbortWithError(http.StatusInternalServerError, err) + log.Errorf("Extract PK error: %v", err) + sendErrorResponse("extract PK error", http.StatusInternalServerError, c) return } for _, addr := range addresses { @@ -274,28 +285,28 @@ findAddress: } } if feeAddr == "" { - fmt.Printf("feeTx did not invalid any payments") - c.AbortWithError(http.StatusInternalServerError, errors.New("feeTx did not include any payments")) + log.Errorf("feeTx did not include any payments") + sendErrorResponse("feeTx did not include any payments", http.StatusInternalServerError, c) return } ticket, err := db.GetTicketByFeeAddress(feeAddr) if err != nil { - fmt.Printf("GetFeeByAddress: %v", err) - c.AbortWithError(http.StatusInternalServerError, errors.New("database error")) + log.Errorf("GetFeeByAddress: %v", err) + sendErrorResponse("database error", http.StatusInternalServerError, c) return } voteAddr, err := dcrutil.DecodeAddress(ticket.CommitmentAddress, cfg.NetParams) if err != nil { - fmt.Printf("PayFee: DecodeAddress: %v", err) - c.AbortWithError(http.StatusInternalServerError, errors.New("database error")) + log.Errorf("DecodeAddress: %v", err) + sendErrorResponse("database error", http.StatusInternalServerError, c) return } _, err = dcrutil.NewAddressPubKeyHash(dcrutil.Hash160(votingWIF.PubKey()), cfg.NetParams, dcrec.STEcdsaSecp256k1) if err != nil { - fmt.Printf("PayFee: NewAddressPubKeyHash: %v", err) - c.AbortWithError(http.StatusInternalServerError, errors.New("failed to deserialize voting wif")) + log.Errorf("NewAddressPubKeyHash: %v", err) + sendErrorResponse("failed to deserialize voting wif", http.StatusInternalServerError, c) return } @@ -306,132 +317,125 @@ findAddress: // TODO - RPC - get relayfee from wallet relayFee, err := dcrutil.NewAmount(0.0001) if err != nil { - fmt.Printf("PayFee: failed to NewAmount: %v", err) - c.AbortWithError(http.StatusInternalServerError, errors.New("internal error")) + log.Errorf("NewAmount failed: %v", err) + sendErrorResponse("failed to create new amount", http.StatusInternalServerError, c) return } minFee := txrules.StakePoolTicketFee(sDiff, relayFee, int32(ticket.BlockHeight), cfg.VSPFee, cfg.NetParams) if feeAmount < minFee { - fmt.Printf("too cheap: %v %v", feeAmount, minFee) - c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("dont get cheap on me, dodgson (sent:%v required:%v)", feeAmount, minFee)) + log.Errorf("Fee too small: was %v, expected %v", feeAmount, minFee) + sendErrorResponse("fee too small", http.StatusInternalServerError, c) return } // Get vote tx to give to wallet ticketHash, err := chainhash.NewHashFromStr(ticket.Hash) if err != nil { - fmt.Printf("PayFee: NewHashFromStr: %v", err) - c.AbortWithError(http.StatusInternalServerError, errors.New("internal error")) + log.Errorf("NewHashFromStr failed: %v", err) + sendErrorResponse("failed to create hash", http.StatusInternalServerError, c) return } - now := time.Now() - resp, err := PayFee2(c.Request.Context(), ticketHash, votingWIF, feeTx) - if err != nil { - fmt.Printf("PayFee: %v", err) - c.AbortWithError(http.StatusInternalServerError, errors.New("RPC server error")) - return - } - - err = db.InsertFeeAddressVotingKey(voteAddr.Address(), votingWIF.String(), voteBits) - if err != nil { - fmt.Printf("PayFee: InsertVotingKey failed: %v", err) - c.AbortWithError(http.StatusInternalServerError, errors.New("internal error")) - return - } - - sendJSONResponse(payFeeResponse{ - Timestamp: now.Unix(), - TxHash: resp, - Request: payFeeRequest, - }, c) -} - -// PayFee2 is copied from the stakepoold implementation in #625 -func PayFee2(ctx context.Context, ticketHash *chainhash.Hash, votingWIF *dcrutil.WIF, feeTx *wire.MsgTx) (string, error) { - var resp dcrdtypes.TxRawResult - walletClient, err := walletRPC() if err != nil { - fmt.Printf("PayFee: wallet RPC error: %v", err) - return "", errors.New("RPC server error") + log.Errorf("Failed to dial dcrwallet RPC: %v", err) + sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) + return } + ctx := c.Request.Context() + var resp dcrdtypes.TxRawResult + err = walletClient.Call(ctx, "getrawtransaction", &resp, ticketHash.String(), true) if err != nil { - fmt.Printf("PayFee: getrawtransaction: %v", err) - return "", errors.New("RPC server error") + log.Errorf("GetRawTransaction failed: %v", err) + sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) + return } err = walletClient.Call(ctx, "addticket", nil, resp.Hex) if err != nil { - fmt.Printf("PayFee: addticket: %v", err) - return "", errors.New("RPC server error") + log.Errorf("AddTicket failed: %v", err) + sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) + return } err = walletClient.Call(ctx, "importprivkey", nil, votingWIF.String(), "imported", false, 0) if err != nil { - fmt.Printf("PayFee: importprivkey: %v", err) - return "", errors.New("RPC server error") + log.Errorf("ImportPrivKey failed: %v", err) + sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) + return } feeTxBuf := new(bytes.Buffer) feeTxBuf.Grow(feeTx.SerializeSize()) err = feeTx.Serialize(feeTxBuf) if err != nil { - fmt.Printf("PayFee: failed to serialize fee transaction: %v", err) - return "", errors.New("serialization error") + log.Errorf("Serialize tx failed: %v", err) + sendErrorResponse("serialize tx error", http.StatusInternalServerError, c) + return } var res string err = walletClient.Call(ctx, "sendrawtransaction", &res, hex.NewEncoder(feeTxBuf), false) if err != nil { - fmt.Printf("PayFee: sendrawtransaction: %v", err) - return "", errors.New("transaction failed to send") + log.Errorf("SendRawTransaction failed: %v", err) + sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) + return } - return res, nil + + err = db.InsertFeeAddressVotingKey(voteAddr.Address(), votingWIF.String(), voteBits) + if err != nil { + log.Errorf("InsertFeeAddressVotingKey failed: %v", err) + sendErrorResponse("database error", http.StatusInternalServerError, c) + return + } + + sendJSONResponse(payFeeResponse{ + Timestamp: time.Now().Unix(), + TxHash: res, + Request: payFeeRequest, + }, c) } func setVoteBits(c *gin.Context) { - dec := json.NewDecoder(c.Request.Body) - var setVoteBitsRequest SetVoteBitsRequest - err := dec.Decode(&setVoteBitsRequest) - if err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid json")) + if err := c.ShouldBindJSON(&setVoteBitsRequest); err != nil { + log.Warnf("Bad request from %s", c.ClientIP()) + sendErrorResponse(err.Error(), http.StatusBadRequest, c) return } // ticketHash ticketHashStr := setVoteBitsRequest.TicketHash - if len(ticketHashStr) != chainhash.MaxHashStringSize { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid ticket hash")) - return - } txHash, err := chainhash.NewHashFromStr(ticketHashStr) if err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid ticket hash")) + log.Warnf("Invalid ticket hash from %s", c.ClientIP()) + sendErrorResponse("invalid ticket hash", http.StatusBadRequest, c) return } // signature - sanity check signature is in base64 encoding signature := setVoteBitsRequest.Signature if _, err = base64.StdEncoding.DecodeString(signature); err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid signature")) + log.Warnf("Invalid signature from %s", c.ClientIP()) + sendErrorResponse("invalid signature", http.StatusBadRequest, c) return } // votebits voteBits := setVoteBitsRequest.VoteBits if !isValidVoteBits(cfg.NetParams, currentVoteVersion(cfg.NetParams), voteBits) { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid votebits")) + log.Warnf("Invalid votebits from %s", c.ClientIP()) + sendErrorResponse("invalid votebits", http.StatusBadRequest, c) return } ticket, err := db.GetTicketByHash(txHash.String()) if err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid ticket")) + log.Warnf("Invalid ticket from %s", c.ClientIP()) + sendErrorResponse("invalid ticket", http.StatusBadRequest, c) return } @@ -439,7 +443,8 @@ func setVoteBits(c *gin.Context) { message := fmt.Sprintf("vsp v3 setvotebits %d %s %d", setVoteBitsRequest.Timestamp, txHash, voteBits) err = dcrutil.VerifyMessage(ticket.CommitmentAddress, signature, message, cfg.NetParams) if err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("message did not pass verification")) + log.Warnf("Failed to verify message from %s", c.ClientIP()) + sendErrorResponse("message did not pass verification", http.StatusBadRequest, c) return } @@ -455,37 +460,34 @@ func setVoteBits(c *gin.Context) { } func ticketStatus(c *gin.Context) { - dec := json.NewDecoder(c.Request.Body) - var ticketStatusRequest TicketStatusRequest - err := dec.Decode(&ticketStatusRequest) - if err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid json")) + if err := c.ShouldBindJSON(&ticketStatusRequest); err != nil { + log.Warnf("Bad request from %s", c.ClientIP()) + sendErrorResponse(err.Error(), http.StatusBadRequest, c) return } // ticketHash ticketHashStr := ticketStatusRequest.TicketHash - if len(ticketHashStr) != chainhash.MaxHashStringSize { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid ticket hash")) - return - } - _, err = chainhash.NewHashFromStr(ticketHashStr) + _, err := chainhash.NewHashFromStr(ticketHashStr) if err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid ticket hash")) + log.Warnf("Invalid ticket hash from %s", c.ClientIP()) + sendErrorResponse("invalid ticket hash", http.StatusBadRequest, c) return } // signature - sanity check signature is in base64 encoding signature := ticketStatusRequest.Signature if _, err = base64.StdEncoding.DecodeString(signature); err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid signature")) + log.Warnf("Invalid signature from %s", c.ClientIP()) + sendErrorResponse("invalid signature", http.StatusBadRequest, c) return } ticket, err := db.GetTicketByHash(ticketHashStr) if err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid ticket")) + log.Warnf("Invalid ticket from %s", c.ClientIP()) + sendErrorResponse("invalid ticket", http.StatusBadRequest, c) return } @@ -493,7 +495,8 @@ func ticketStatus(c *gin.Context) { message := fmt.Sprintf("vsp v3 ticketstatus %d %s", ticketStatusRequest.Timestamp, ticketHashStr) err = dcrutil.VerifyMessage(ticket.CommitmentAddress, signature, message, cfg.NetParams) if err != nil { - c.AbortWithError(http.StatusBadRequest, errors.New("invalid signature")) + log.Warnf("Invalid signature from %s", c.ClientIP()) + sendErrorResponse("invalid signature", http.StatusBadRequest, c) return } diff --git a/webapi/responses.go b/webapi/responses.go index ba54599..ee645ee 100644 --- a/webapi/responses.go +++ b/webapi/responses.go @@ -1,64 +1,64 @@ package webapi type pubKeyResponse struct { - Timestamp int64 `json:"timestamp"` - PubKey []byte `json:"pubKey"` + Timestamp int64 `json:"timestamp" binding:"required"` + PubKey []byte `json:"pubKey" binding:"required"` } type feeResponse struct { - Timestamp int64 `json:"timestamp"` - Fee float64 `json:"fee"` + Timestamp int64 `json:"timestamp" binding:"required"` + Fee float64 `json:"fee" binding:"required"` } type FeeAddressRequest struct { - Timestamp int64 `json:"timestamp"` - TicketHash string `json:"ticketHash"` - Signature string `json:"signature"` + Timestamp int64 `json:"timestamp" binding:"required"` + TicketHash string `json:"ticketHash" binding:"required"` + Signature string `json:"signature" binding:"required"` } type feeAddressResponse struct { - Timestamp int64 `json:"timestamp"` - FeeAddress string `json:"feeAddress"` - Fee float64 `json:"fee"` - Expiration int64 `json:"expiration"` - Request FeeAddressRequest `json:"request"` + Timestamp int64 `json:"timestamp" binding:"required"` + FeeAddress string `json:"feeAddress" binding:"required"` + Fee float64 `json:"fee" binding:"required"` + Expiration int64 `json:"expiration" binding:"required"` + Request FeeAddressRequest `json:"request" binding:"required"` } type PayFeeRequest struct { - Timestamp int64 `json:"timestamp"` - Hex []byte `json:"feeTx"` - VotingKey string `json:"votingKey"` - VoteBits uint16 `json:"voteBits"` + Timestamp int64 `json:"timestamp" binding:"required"` + Hex []byte `json:"feeTx" binding:"required"` + VotingKey string `json:"votingKey" binding:"required"` + VoteBits uint16 `json:"voteBits" binding:"required"` } type payFeeResponse struct { - Timestamp int64 `json:"timestamp"` - TxHash string `json:"txHash"` - Request PayFeeRequest `json:"request"` + Timestamp int64 `json:"timestamp" binding:"required"` + TxHash string `json:"txHash" binding:"required"` + Request PayFeeRequest `json:"request" binding:"required"` } type SetVoteBitsRequest struct { - Timestamp int64 `json:"timestamp"` - TicketHash string `json:"ticketHash"` - Signature string `json:"commitmentSignature"` - VoteBits uint16 `json:"voteBits"` + Timestamp int64 `json:"timestamp" binding:"required"` + TicketHash string `json:"ticketHash" binding:"required"` + Signature string `json:"commitmentSignature" binding:"required"` + VoteBits uint16 `json:"voteBits" binding:"required"` } type setVoteBitsResponse struct { - Timestamp int64 `json:"timestamp"` - Request SetVoteBitsRequest `json:"request"` - VoteBits uint16 `json:"voteBits"` + Timestamp int64 `json:"timestamp" binding:"required"` + Request SetVoteBitsRequest `json:"request" binding:"required"` + VoteBits uint16 `json:"voteBits" binding:"required"` } type TicketStatusRequest struct { - Timestamp int64 `json:"timestamp"` - TicketHash string `json:"ticketHash"` - Signature string `json:"signature"` + Timestamp int64 `json:"timestamp" binding:"required"` + TicketHash string `json:"ticketHash" binding:"required"` + Signature string `json:"signature" binding:"required"` } type ticketStatusResponse struct { - Timestamp int64 `json:"timestamp"` - Request TicketStatusRequest `json:"request"` - Status string `json:"status"` - VoteBits uint16 `json:"votebits"` + Timestamp int64 `json:"timestamp" binding:"required"` + Request TicketStatusRequest `json:"request" binding:"required"` + Status string `json:"status" binding:"required"` + VoteBits uint16 `json:"votebits" binding:"required"` } diff --git a/webapi/server.go b/webapi/server.go index f15856e..588a79b 100644 --- a/webapi/server.go +++ b/webapi/server.go @@ -27,7 +27,7 @@ var db *database.VspDatabase var walletRPC rpc.Client func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *sync.WaitGroup, - listen string, db *database.VspDatabase, wRPC rpc.Client, releaseMode bool, config Config) error { + listen string, vdb *database.VspDatabase, wRPC rpc.Client, releaseMode bool, config Config) error { // Create TCP listener. var listenConfig net.ListenConfig @@ -74,6 +74,7 @@ func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *s }() cfg = config + db = vdb walletRPC = wRPC return nil