Delay fee broadcast and adding tickets to wallets. (#62)

* Delay fee broadcast and adding tickets to wallets.

- Adds a `background` package which implements a dcrd notification handler.  On each blockconnected notification, tickets with 6+ confirmations are marked confirmed, relevant fee transactions are broadcast, and any fees with 6+ confirmations have their tickets added to voting wallets.
- VSP fee is now an absolute value measured in DCR rather than a percentage. This simplifies the code and is more appropriate for an MVP. We can re-add percentage based fees later.
- Database code for tickets is now simplified to just "Insert/Update", rather than having functions for updating particular fields.
- Pay fee response no longer includes the fee tx hash, because we dont necessarily broadcast the fee tx straight away.

* Const for required confs
This commit is contained in:
Jamie Holdstock 2020-05-27 06:39:38 +01:00 committed by GitHub
parent 86c4195931
commit 87500c3fef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 529 additions and 361 deletions

229
background/background.go Normal file
View File

@ -0,0 +1,229 @@
package background
import (
"context"
"encoding/json"
"time"
"decred.org/dcrwallet/rpc/client/dcrd"
"github.com/jholdstock/dcrvsp/database"
"github.com/jholdstock/dcrvsp/rpc"
)
type NotificationHandler struct {
Ctx context.Context
Db *database.VspDatabase
WalletConnect rpc.Connect
closed chan struct{}
dcrdClient *rpc.DcrdRPC
}
// The number of confirmations required to consider a ticket purchase or a fee
// transaction to be final.
const (
requiredConfs = 6
)
// Notify is called every time a block notification is received from dcrd.
// Notify is never called concurrently. Notify should not return an error
// because that will cause the client to close and no further notifications will
// be received until a new connection is established.
func (n *NotificationHandler) Notify(method string, params json.RawMessage) error {
if method != "blockconnected" {
return nil
}
header, _, err := dcrd.BlockConnected(params)
if err != nil {
log.Errorf("Failed to parse dcrd block notification: %v", err)
return nil
}
log.Debugf("Block notification %d (%s)", header.Height, header.BlockHash().String())
// Step 1/3: Update the database with any tickets which now have 6+
// confirmations.
unconfirmed, err := n.Db.GetUnconfirmedTickets()
if err != nil {
log.Errorf("GetUnconfirmedTickets error: %v", err)
}
for _, ticket := range unconfirmed {
tktTx, err := n.dcrdClient.GetRawTransaction(ticket.Hash)
if err != nil {
log.Errorf("GetRawTransaction error: %v", err)
continue
}
if tktTx.Confirmations >= requiredConfs {
ticket.Confirmed = true
err = n.Db.UpdateTicket(ticket)
if err != nil {
log.Errorf("UpdateTicket error: %v", err)
continue
}
log.Debugf("Ticket confirmed: ticketHash=%s", ticket.Hash)
}
}
// Step 2/3: Broadcast fee tx for tickets which are confirmed.
pending, err := n.Db.GetPendingFees()
if err != nil {
log.Errorf("GetPendingFees error: %v", err)
}
for _, ticket := range pending {
feeTxHash, err := n.dcrdClient.SendRawTransaction(ticket.FeeTxHex)
if err != nil {
// TODO: SendRawTransaction can return a "transcation already
// exists" error, which isnt necessarily a problem here.
log.Errorf("SendRawTransaction error: %v", err)
continue
}
ticket.FeeTxHash = feeTxHash
err = n.Db.UpdateTicket(ticket)
if err != nil {
log.Errorf("UpdateTicket error: %v", err)
continue
}
log.Debugf("Fee tx broadcast for ticket: ticketHash=%s, feeHash=%s", ticket.Hash, feeTxHash)
}
// Step 3/3: Add tickets with confirmed fees to voting wallets.
unconfirmedFees, err := n.Db.GetUnconfirmedFees()
if err != nil {
log.Errorf("GetUnconfirmedFees error: %v", err)
// If this fails, there is nothing more we can do. Return.
return nil
}
// If there are no confirmed fees, there is nothing more to do. Return.
if len(unconfirmedFees) == 0 {
return nil
}
var walletClient *rpc.WalletRPC
walletConn, err := n.WalletConnect()
if err != nil {
log.Errorf("dcrwallet connection error: %v", err)
// If this fails, there is nothing more we can do. Return.
return nil
}
walletClient, err = rpc.WalletClient(n.Ctx, walletConn)
if err != nil {
log.Errorf("dcrwallet client error: %v", err)
// If this fails, there is nothing more we can do. Return.
return nil
}
for _, ticket := range unconfirmedFees {
feeTx, err := n.dcrdClient.GetRawTransaction(ticket.FeeTxHash)
if err != nil {
log.Errorf("GetRawTransaction error: %v", err)
continue
}
// If fee is confirmed, update the database and add ticket to voting
// wallets.
if feeTx.Confirmations >= requiredConfs {
ticket.FeeConfirmed = true
err = n.Db.UpdateTicket(ticket)
if err != nil {
log.Errorf("UpdateTicket error: %v", err)
return nil
}
log.Debugf("Fee tx confirmed for ticket: ticketHash=%s", ticket.Hash)
// Add ticket to the voting wallet.
rawTicket, err := n.dcrdClient.GetRawTransaction(ticket.Hash)
if err != nil {
log.Errorf("GetRawTransaction error: %v", err)
continue
}
err = walletClient.AddTransaction(rawTicket.BlockHash, rawTicket.Hex)
if err != nil {
log.Errorf("AddTransaction error: %v", err)
continue
}
err = walletClient.ImportPrivKey(ticket.VotingWIF)
if err != nil {
log.Errorf("ImportPrivKey error: %v", err)
continue
}
// Update vote choices on voting wallets.
for agenda, choice := range ticket.VoteChoices {
err = walletClient.SetVoteChoice(agenda, choice, ticket.Hash)
if err != nil {
log.Errorf("SetVoteChoice error: %v", err)
continue
}
}
log.Debugf("Ticket added to voting wallet: ticketHash=%s", ticket.Hash)
}
}
return nil
}
func (n *NotificationHandler) Close() error {
close(n.closed)
return nil
}
func (n *NotificationHandler) connect(dcrdConnect rpc.Connect) error {
dcrdConn, err := dcrdConnect()
if err != nil {
return err
}
n.dcrdClient, err = rpc.DcrdClient(n.Ctx, dcrdConn)
if err != nil {
return err
}
err = n.dcrdClient.NotifyBlocks()
if err != nil {
return err
}
log.Info("Subscribed for dcrd block notifications")
// Wait until context is done (dcrvsp is shutting down), or until the
// notifier is closed.
select {
case <-n.Ctx.Done():
return n.Ctx.Err()
case <-n.closed:
return nil
}
}
func Start(n *NotificationHandler, dcrdConnect rpc.Connect) {
// Loop forever attempting to create a connection to the dcrd server.
go func() {
for {
n.closed = make(chan struct{})
err := n.connect(dcrdConnect)
if err != nil {
log.Errorf("dcrd connect error: %v", err)
// If context is done (dcrvsp is shutting down), return,
// otherwise wait 15 seconds and to reconnect.
select {
case <-n.Ctx.Done():
return
case <-time.After(15 * time.Second):
}
}
}
}()
}

26
background/log.go Normal file
View File

@ -0,0 +1,26 @@
package background
import (
"github.com/decred/slog"
)
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var log slog.Logger
// The default amount of logging is none.
func init() {
DisableLog()
}
// DisableLog disables all library log output. Logging output is disabled
// by default until UseLogger is called.
func DisableLog() {
log = slog.Disabled
}
// UseLogger uses a specified Logger to output package logging info.
func UseLogger(logger slog.Logger) {
log = logger
}

View File

@ -19,7 +19,7 @@ import (
var (
defaultListen = ":3000"
defaultLogLevel = "debug"
defaultVSPFee = 0.01
defaultVSPFee = 0.001
defaultNetwork = "testnet"
defaultHomeDir = dcrutil.AppDataDir("dcrvsp", false)
defaultConfigFilename = "dcrvsp.conf"
@ -35,7 +35,7 @@ type config struct {
LogLevel string `long:"loglevel" ini-name:"loglevel" description:"Logging level." choice:"trace" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"critical"`
Network string `long:"network" ini-name:"network" description:"Decred network to use." choice:"testnet" choice:"mainnet" choice:"simnet"`
FeeXPub string `long:"feexpub" ini-name:"feexpub" description:"Cold wallet xpub used for collecting fees."`
VSPFee float64 `long:"vspfee" ini-name:"vspfee" description:"Fee percentage charged for VSP use. eg. 0.01 (1%), 0.05 (5%)."`
VSPFee float64 `long:"vspfee" ini-name:"vspfee" description:"Fee charged for VSP use. Absolute value - eg. 0.01 = 0.01 DCR."`
HomeDir string `long:"homedir" ini-name:"homedir" no-ini:"true" description:"Path to application home directory. Used for storing VSP database and logs."`
ConfigFile string `long:"configfile" ini-name:"configfile" no-ini:"true" description:"Path to configuration file."`
DcrdHost string `long:"dcrdhost" ini-name:"dcrdhost" description:"The ip:port to establish a JSON-RPC connection with dcrd. Should be the same host where dcrvsp is running."`

View File

@ -19,13 +19,14 @@ func exampleTicket() Ticket {
CommitmentAddress: "Address",
FeeAddressIndex: 12345,
FeeAddress: "FeeAddress",
SDiff: 1,
BlockHeight: 2,
VoteChoices: map[string]string{"AgendaID": "Choice"},
VotingKey: "VotingKey",
VSPFee: 0.1,
FeeExpiration: 4,
Confirmed: false,
VoteChoices: map[string]string{"AgendaID": "Choice"},
VotingWIF: "VotingKey",
FeeTxHex: "FeeTransction",
FeeTxHash: "",
FeeConfirmed: true,
}
}
@ -36,11 +37,8 @@ func TestDatabase(t *testing.T) {
// All sub-tests to run.
tests := map[string]func(*testing.T){
"testInsertTicket": testInsertTicket,
"testGetTicketByHash": testGetTicketByHash,
"testSetTicketVotingKey": testSetTicketVotingKey,
"testUpdateExpireAndFee": testUpdateExpireAndFee,
"testUpdateVoteChoices": testUpdateVoteChoices,
"testInsertNewTicket": testInsertNewTicket,
"testGetTicketByHash": testGetTicketByHash,
}
for testName, test := range tests {
@ -64,23 +62,23 @@ func TestDatabase(t *testing.T) {
}
}
func testInsertTicket(t *testing.T) {
func testInsertNewTicket(t *testing.T) {
// Insert a ticket into the database.
ticket := exampleTicket()
err := db.InsertTicket(ticket)
err := db.InsertNewTicket(ticket)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
// Inserting a ticket with the same hash should fail.
err = db.InsertTicket(ticket)
err = db.InsertNewTicket(ticket)
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.InsertTicket(ticket)
err = db.InsertNewTicket(ticket)
if err == nil {
t.Fatal("expected an error inserting ticket with no hash")
}
@ -89,7 +87,7 @@ func testInsertTicket(t *testing.T) {
func testGetTicketByHash(t *testing.T) {
ticket := exampleTicket()
// Insert a ticket into the database.
err := db.InsertTicket(ticket)
err := db.InsertNewTicket(ticket)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
@ -108,13 +106,14 @@ func testGetTicketByHash(t *testing.T) {
retrieved.CommitmentAddress != ticket.CommitmentAddress ||
retrieved.FeeAddressIndex != ticket.FeeAddressIndex ||
retrieved.FeeAddress != ticket.FeeAddress ||
retrieved.SDiff != ticket.SDiff ||
retrieved.BlockHeight != ticket.BlockHeight ||
!reflect.DeepEqual(retrieved.VoteChoices, ticket.VoteChoices) ||
retrieved.VotingKey != ticket.VotingKey ||
retrieved.VSPFee != ticket.VSPFee ||
retrieved.FeeExpiration != ticket.FeeExpiration ||
retrieved.Confirmed != ticket.Confirmed ||
!reflect.DeepEqual(retrieved.VoteChoices, ticket.VoteChoices) ||
retrieved.VotingWIF != ticket.VotingWIF ||
retrieved.FeeTxHex != ticket.FeeTxHex ||
retrieved.FeeTxHash != ticket.FeeTxHash ||
retrieved.FeeExpiration != ticket.FeeExpiration {
retrieved.FeeConfirmed != ticket.FeeConfirmed {
t.Fatal("retrieved ticket value didnt match expected")
}
@ -128,90 +127,7 @@ func testGetTicketByHash(t *testing.T) {
}
}
func testSetTicketVotingKey(t *testing.T) {
// Insert a ticket into the database.
ticket := exampleTicket()
err := db.InsertTicket(ticket)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
// TODO: Add tests for UpdateTicket, CountTickets, GetUnconfirmedTickets,
// GetPendingFees, GetUnconfirmedFees.
// Update values.
newVotingKey := ticket.VotingKey + "2"
newVoteChoices := ticket.VoteChoices
feeTxHash := ticket.FeeTxHash + "3"
newVoteChoices["AgendaID"] = "Different choice"
err = db.SetTicketVotingKey(ticket.Hash, newVotingKey, newVoteChoices, feeTxHash)
if err != nil {
t.Fatalf("error updating votingkey and votechoices: %v", err)
}
// Retrieve ticket from database.
retrieved, _, err := db.GetTicketByHash(ticket.Hash)
if err != nil {
t.Fatalf("error retrieving ticket by ticket hash: %v", err)
}
// Check ticket fields match expected.
if !reflect.DeepEqual(newVoteChoices, retrieved.VoteChoices) ||
feeTxHash != retrieved.FeeTxHash ||
newVotingKey != retrieved.VotingKey {
t.Fatal("retrieved ticket value didnt match expected")
}
}
func testUpdateExpireAndFee(t *testing.T) {
// Insert a ticket into the database.
ticket := exampleTicket()
err := db.InsertTicket(ticket)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
// Update ticket with new values.
newExpiry := ticket.FeeExpiration + 1
newFee := ticket.VSPFee + 1
err = db.UpdateExpireAndFee(ticket.Hash, newExpiry, newFee)
if err != nil {
t.Fatalf("error updating expiry and fee: %v", err)
}
// Get updated ticket
retrieved, _, err := db.GetTicketByHash(ticket.Hash)
if err != nil {
t.Fatalf("error retrieving updated ticket: %v", err)
}
// Check ticket fields match expected.
if retrieved.VSPFee != newFee || retrieved.FeeExpiration != newExpiry {
t.Fatal("retrieved ticket value didnt match expected")
}
}
func testUpdateVoteChoices(t *testing.T) {
// Insert a ticket into the database.
ticket := exampleTicket()
err := db.InsertTicket(ticket)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
// Update ticket with new votechoices.
newVoteChoices := ticket.VoteChoices
newVoteChoices["AgendaID"] = "Different choice"
err = db.UpdateVoteChoices(ticket.Hash, newVoteChoices)
if err != nil {
t.Fatalf("error updating votechoices: %v", err)
}
// Get updated ticket
retrieved, _, err := db.GetTicketByHash(ticket.Hash)
if err != nil {
t.Fatalf("error retrieving updated ticket: %v", err)
}
// Check ticket fields match expected.
if !reflect.DeepEqual(newVoteChoices, retrieved.VoteChoices) {
t.Fatal("retrieved ticket value didnt match expected")
}
}
// TODO: Add tests for ticket.FeeExpired.

View File

@ -9,18 +9,32 @@ import (
bolt "go.etcd.io/bbolt"
)
// TODO: Properly document ticket lifecycle.
// TODO: Shorten json keys, they are stored in the db and duplicated many times.
type Ticket struct {
Hash string `json:"hash"`
CommitmentAddress string `json:"commitmentaddress"`
FeeAddressIndex uint32 `json:"feeaddressindex"`
FeeAddress string `json:"feeaddress"`
SDiff float64 `json:"sdiff"`
BlockHeight int64 `json:"blockheight"`
VoteChoices map[string]string `json:"votechoices"`
VotingKey string `json:"votingkey"`
VSPFee float64 `json:"vspfee"`
FeeExpiration int64 `json:"feeexpiration"`
FeeTxHash string `json:"feetxhash"`
Hash string `json:"hash"`
CommitmentAddress string `json:"commitmentaddress"`
FeeAddressIndex uint32 `json:"feeaddressindex"`
FeeAddress string `json:"feeaddress"`
VSPFee float64 `json:"vspfee"`
FeeExpiration int64 `json:"feeexpiration"`
// Confirmed will be set when the ticket has 6+ confirmations.
Confirmed bool `json:"confirmed"`
// VoteChoices and VotingWIF are set in /payfee.
VoteChoices map[string]string `json:"votechoices"`
VotingWIF string `json:"votingwif"`
// FeeTxHex will be set when the fee tx has been received from the user.
FeeTxHex string `json:"feetxhex"`
// FeeTxHash will be set when the fee tx has been broadcast.
FeeTxHash string `json:"feetxhash"`
// FeeConfirmed will be set when the fee tx has 6+ confirmations.
FeeConfirmed bool `json:"feeconfirmed"`
}
func (t *Ticket) FeeExpired() bool {
@ -32,11 +46,12 @@ var (
ErrNoTicketFound = errors.New("no ticket found")
)
func (vdb *VspDatabase) InsertTicket(ticket Ticket) error {
hashBytes := []byte(ticket.Hash)
func (vdb *VspDatabase) InsertNewTicket(ticket Ticket) error {
return vdb.db.Update(func(tx *bolt.Tx) error {
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
hashBytes := []byte(ticket.Hash)
if ticketBkt.Get(hashBytes) != nil {
return fmt.Errorf("ticket already exists with hash %s", ticket.Hash)
}
@ -45,35 +60,26 @@ func (vdb *VspDatabase) InsertTicket(ticket Ticket) error {
ticketBytes, err := json.Marshal(ticket)
if err != nil {
return err
return fmt.Errorf("could not marshal ticket: %v", err)
}
return ticketBkt.Put(hashBytes, ticketBytes)
})
}
func (vdb *VspDatabase) SetTicketVotingKey(ticketHash, votingKey string, voteChoices map[string]string, feeTxHash string) error {
func (vdb *VspDatabase) UpdateTicket(ticket Ticket) error {
return vdb.db.Update(func(tx *bolt.Tx) error {
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
hashBytes := []byte(ticketHash)
hashBytes := []byte(ticket.Hash)
ticketBytes := ticketBkt.Get(hashBytes)
if ticketBytes == nil {
return ErrNoTicketFound
if ticketBkt.Get(hashBytes) == nil {
return fmt.Errorf("ticket does not exist with hash %s", ticket.Hash)
}
var ticket Ticket
err := json.Unmarshal(ticketBytes, &ticket)
if err != nil {
return fmt.Errorf("could not unmarshal ticket: %v", err)
}
// TODO: Error if a ticket already exists with the same fee address.
ticket.VotingKey = votingKey
ticket.VoteChoices = voteChoices
ticket.FeeTxHash = feeTxHash
ticketBytes, err = json.Marshal(ticket)
ticketBytes, err := json.Marshal(ticket)
if err != nil {
return fmt.Errorf("could not marshal ticket: %v", err)
}
@ -106,59 +112,6 @@ func (vdb *VspDatabase) GetTicketByHash(ticketHash string) (Ticket, bool, error)
return ticket, found, err
}
func (vdb *VspDatabase) UpdateVoteChoices(ticketHash string, voteChoices map[string]string) error {
return vdb.db.Update(func(tx *bolt.Tx) error {
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
hashBytes := []byte(ticketHash)
ticketBytes := ticketBkt.Get(hashBytes)
if ticketBytes == nil {
return ErrNoTicketFound
}
var ticket Ticket
err := json.Unmarshal(ticketBytes, &ticket)
if err != nil {
return fmt.Errorf("could not unmarshal ticket: %v", err)
}
ticket.VoteChoices = voteChoices
ticketBytes, err = json.Marshal(ticket)
if err != nil {
return fmt.Errorf("could not marshal ticket: %v", err)
}
return ticketBkt.Put(hashBytes, ticketBytes)
})
}
func (vdb *VspDatabase) UpdateExpireAndFee(ticketHash string, expiration int64, vspFee float64) error {
return vdb.db.Update(func(tx *bolt.Tx) error {
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
hashBytes := []byte(ticketHash)
ticketBytes := ticketBkt.Get(hashBytes)
if ticketBytes == nil {
return ErrNoTicketFound
}
var ticket Ticket
err := json.Unmarshal(ticketBytes, &ticket)
if err != nil {
return fmt.Errorf("could not unmarshal ticket: %v", err)
}
ticket.FeeExpiration = expiration
ticket.VSPFee = vspFee
ticketBytes, err = json.Marshal(ticket)
if err != nil {
return fmt.Errorf("could not marshal ticket: %v", err)
}
return ticketBkt.Put(hashBytes, ticketBytes)
})
}
func (vdb *VspDatabase) CountTickets() (int, int, error) {
var total, feePaid int
err := vdb.db.View(func(tx *bolt.Tx) error {
@ -182,3 +135,78 @@ func (vdb *VspDatabase) CountTickets() (int, int, error) {
return total, feePaid, err
}
func (vdb *VspDatabase) GetUnconfirmedTickets() ([]Ticket, error) {
var tickets []Ticket
err := vdb.db.View(func(tx *bolt.Tx) error {
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
return ticketBkt.ForEach(func(k, v []byte) error {
var ticket Ticket
err := json.Unmarshal(v, &ticket)
if err != nil {
return fmt.Errorf("could not unmarshal ticket: %v", err)
}
if !ticket.Confirmed {
tickets = append(tickets, ticket)
}
return nil
})
})
return tickets, err
}
func (vdb *VspDatabase) GetPendingFees() ([]Ticket, error) {
var tickets []Ticket
err := vdb.db.View(func(tx *bolt.Tx) error {
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
return ticketBkt.ForEach(func(k, v []byte) error {
var ticket Ticket
err := json.Unmarshal(v, &ticket)
if err != nil {
return fmt.Errorf("could not unmarshal ticket: %v", err)
}
// Add ticket if it is confirmed, and we have a fee tx, and the tx
// is not broadcast yet.
if ticket.Confirmed &&
ticket.FeeTxHex != "" &&
ticket.FeeTxHash == "" {
tickets = append(tickets, ticket)
}
return nil
})
})
return tickets, err
}
func (vdb *VspDatabase) GetUnconfirmedFees() ([]Ticket, error) {
var tickets []Ticket
err := vdb.db.View(func(tx *bolt.Tx) error {
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
return ticketBkt.ForEach(func(k, v []byte) error {
var ticket Ticket
err := json.Unmarshal(v, &ticket)
if err != nil {
return fmt.Errorf("could not unmarshal ticket: %v", err)
}
// Add ticket if fee tx is broadcast but not confirmed yet.
if ticket.FeeTxHash != "" &&
!ticket.FeeConfirmed {
tickets = append(tickets, ticket)
}
return nil
})
})
return tickets, err
}

11
go.mod
View File

@ -4,14 +4,13 @@ go 1.13
require (
decred.org/dcrwallet v1.2.3-0.20200519180100-f1aa4c354e05
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200511175520-d08cb3f72b3b
github.com/decred/dcrd/chaincfg/chainhash v1.0.2
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200511175520-d08cb3f72b3b
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200522182228-0c7cbb53680b
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200522182228-0c7cbb53680b
github.com/decred/dcrd/dcrec v1.0.0
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200517213104-6ade94486839
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200421213827-b60c60ffe98b
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200522182228-0c7cbb53680b
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200522182228-0c7cbb53680b
github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.0
github.com/decred/dcrd/txscript/v3 v3.0.0-20200511175520-d08cb3f72b3b
github.com/decred/dcrd/txscript/v3 v3.0.0-20200522182228-0c7cbb53680b
github.com/decred/dcrd/wire v1.3.0
github.com/decred/slog v1.0.0
github.com/gin-gonic/gin v1.6.3

24
go.sum
View File

@ -14,6 +14,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4=
github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4=
github.com/decred/base58 v1.0.1/go.mod h1:H2ENcsJjye1G7CbRa67kV9OFaui0LGr56ntKKoY5g9c=
github.com/decred/base58 v1.0.2 h1:yupIH6bg+q7KYfBk7oUv3xFjKGb5Ypm4+v/61X4keGY=
@ -22,9 +23,8 @@ github.com/decred/dcrd/addrmgr v1.1.0/go.mod h1:exghL+0+QeVvO4MXezWJ1C2tcpBn3ngf
github.com/decred/dcrd/blockchain/stake/v2 v2.0.2/go.mod h1:o2TT/l/YFdrt15waUdlZ3g90zfSwlA0WgQqHV9UGJF4=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:aDL94kcVJfaaJP+acWUJrlK7g7xEOqTSiFe6bSN3yRQ=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:4zE60yDWlfCDtmqnyP5o1k1K0oyhNn3Tvqo6F93/+RU=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200511175520-d08cb3f72b3b h1:8ChbBKdGbsfAUVWwqUzZIbGHg1z0YpFrVokpNETpal0=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200511175520-d08cb3f72b3b/go.mod h1:4zE60yDWlfCDtmqnyP5o1k1K0oyhNn3Tvqo6F93/+RU=
github.com/decred/dcrd/blockchain/standalone v1.1.0 h1:yclvVGEY09Gf8A4GSAo+NCtL1dW2TYJ4OKp4+g0ICI0=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200522182228-0c7cbb53680b h1:lpObaxbwkkC7ZZDkqJnL7Rj64cBGOdhVELjYcYtyAAA=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200522182228-0c7cbb53680b/go.mod h1:4zE60yDWlfCDtmqnyP5o1k1K0oyhNn3Tvqo6F93/+RU=
github.com/decred/dcrd/blockchain/standalone v1.1.0/go.mod h1:6K8ZgzlWM1Kz2TwXbrtiAvfvIwfAmlzrtpA7CVPCUPE=
github.com/decred/dcrd/blockchain/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:R9rIXU8kEJVC9Z4LAlh9bo9hiT3a+ihys3mCrz4PVao=
github.com/decred/dcrd/certgen v1.1.0/go.mod h1:ivkPLChfjdAgFh7ZQOtl6kJRqVkfrCq67dlq3AbZBQE=
@ -37,8 +37,8 @@ github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215015031-3283587e6add/go.mod h1:
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200511175520-d08cb3f72b3b h1:L6qM+5ISaaYSnNMAFMouu/FGcIN0tP42rJUdtAJqKgY=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200511175520-d08cb3f72b3b/go.mod h1:OHbKBa6UZZOXCU1Y8f9Ta3O+GShto7nB1O0nuEutKq4=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200522182228-0c7cbb53680b h1:pWbJdg3FFix4W4acIkkQuvBp2t1n8PZPSb4uFMxk8wI=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200522182228-0c7cbb53680b/go.mod h1:OHbKBa6UZZOXCU1Y8f9Ta3O+GShto7nB1O0nuEutKq4=
github.com/decred/dcrd/connmgr/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:mvIMJsrOEngogmVrq+tdbPIZchHVgGnVBZeNwj1cW6E=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
@ -66,20 +66,22 @@ github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215015031-3283587e6add/go.mod h1:C
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:48ZLpNNrRIYfqYxmvzMgOZrnTZUU3aTJveWtamCkOxo=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:jFxEd2LWDLvrWlrIiyx9ZGTQjvoFHZ0OVfBdyIX7jSw=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:/CDBC1SOXKrmihavgXviaTr6eVZSAWKQqEbRmacDxgg=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200517213104-6ade94486839 h1:1re/l3jvxknGdZ9K0X5k4FbI0al+nfs09F0acpZEyTM=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200517213104-6ade94486839/go.mod h1:85NtF/fmqL2UDf0/gLhTHG/m/0HQHwG+erQKkwWW27A=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200522182228-0c7cbb53680b h1:eqcKN5ZcXshnTCG3GSAM+J1fxuKyD/ZR7SFg/cXB0eQ=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200522182228-0c7cbb53680b/go.mod h1:85NtF/fmqL2UDf0/gLhTHG/m/0HQHwG+erQKkwWW27A=
github.com/decred/dcrd/gcs/v2 v2.0.0/go.mod h1:3XjKcrtvB+r2ezhIsyNCLk6dRnXRJVyYmsd1P3SkU3o=
github.com/decred/dcrd/gcs/v2 v2.0.2-0.20200312171759-0a8cc56a776e h1:tBOk2P8F9JyRUSp0iRTs4nYEBro1FKBDIbg/UualLWw=
github.com/decred/dcrd/gcs/v2 v2.0.2-0.20200312171759-0a8cc56a776e/go.mod h1:JJGd1m0DrFgV4J2J8HKNB9YVkM06ewQHT6iINis39Z4=
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200421213827-b60c60ffe98b h1:pfhggbZaR/h4mjHwMMgCl4+UnstAssVA7FEezvMKCQo=
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200421213827-b60c60ffe98b/go.mod h1:qKN0WzeSEEZ4fUBsTwKzOPkLP7GqSM6jBUm5Auq9mrM=
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200522182228-0c7cbb53680b h1:V9NS+FoeH6DbTeUt1tsMCO4CI4nfFvw1qi6DDXniufc=
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200522182228-0c7cbb53680b/go.mod h1:qKN0WzeSEEZ4fUBsTwKzOPkLP7GqSM6jBUm5Auq9mrM=
github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.0 h1:uyvwjO+90KHxZIIztobB9cG+qVSHhCT+aGSiZF1vGAg=
github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.0/go.mod h1:c5S+PtQWNIA2aUakgrLhrlopkMadcOv51dWhCEdo49c=
github.com/decred/dcrd/txscript/v2 v2.1.0/go.mod h1:XaJAVrZU4NWRx4UEzTiDAs86op1m8GRJLz24SDBKOi0=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:ATMA8K0SOo+M9Wdbr6dMnAd8qICJi6pXjGLlKsJc99E=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:KsDS7McU1yFaCYR9LCIwk6YnE15YN3wJUDxhKdFqlsc=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200421213827-b60c60ffe98b/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200511175520-d08cb3f72b3b h1:PbEqUN+q0hg/TJdi2IQ0Y/5Qc8GNFrnioXeBXm060n8=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200511175520-d08cb3f72b3b/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200522182228-0c7cbb53680b h1:RHdkVPLSryp1SS+ig0vNpwAfsLKDn22sQXmr7KyH0eE=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200522182228-0c7cbb53680b/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0=
github.com/decred/dcrd/wire v1.3.0 h1:X76I2/a8esUmxXmFpJpAvXEi014IA4twgwcOBeIS8lE=
github.com/decred/dcrd/wire v1.3.0/go.mod h1:fnKGlUY2IBuqnpxx5dYRU5Oiq392OBqAuVjRVSkIoXM=
github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0=
@ -115,6 +117,7 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jrick/bitset v1.0.0 h1:Ws0PXV3PwXqWK2n7Vz6idCdrV/9OrBXgHEJi27ZB9Dw=
github.com/jrick/bitset v1.0.0/go.mod h1:ZOYB5Uvkla7wIEY4FEssPVi3IQXa02arznRaYaAEPe4=
github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
@ -167,6 +170,7 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

4
log.go
View File

@ -8,6 +8,7 @@ import (
"github.com/decred/slog"
"github.com/jrick/logrotate/rotator"
"github.com/jholdstock/dcrvsp/background"
"github.com/jholdstock/dcrvsp/database"
"github.com/jholdstock/dcrvsp/rpc"
"github.com/jholdstock/dcrvsp/webapi"
@ -42,6 +43,7 @@ var (
log = backendLog.Logger("VSP")
dbLog = backendLog.Logger(" DB")
bgLog = backendLog.Logger(" BG")
apiLog = backendLog.Logger("API")
rpcLog = backendLog.Logger("RPC")
)
@ -49,6 +51,7 @@ var (
// Initialize package-global logger variables.
func init() {
database.UseLogger(dbLog)
background.UseLogger(bgLog)
webapi.UseLogger(apiLog)
rpc.UseLogger(rpcLog)
}
@ -57,6 +60,7 @@ func init() {
var subsystemLoggers = map[string]slog.Logger{
"VSP": log,
" DB": dbLog,
" BG": bgLog,
"API": apiLog,
"RPC": rpcLog,
}

21
main.go
View File

@ -8,6 +8,7 @@ import (
"sync"
"time"
"github.com/jholdstock/dcrvsp/background"
"github.com/jholdstock/dcrvsp/database"
"github.com/jholdstock/dcrvsp/rpc"
"github.com/jholdstock/dcrvsp/webapi"
@ -59,7 +60,8 @@ func run(ctx context.Context) error {
// Create RPC client for local dcrd instance (used for broadcasting and
// checking the status of fee transactions).
// Dial once just to validate config.
dcrdConnect := rpc.Setup(ctx, &shutdownWg, cfg.DcrdUser, cfg.DcrdPass, cfg.DcrdHost, cfg.dcrdCert)
dcrdConnect := rpc.Setup(ctx, &shutdownWg, cfg.DcrdUser, cfg.DcrdPass,
cfg.DcrdHost, cfg.dcrdCert, nil)
dcrdConn, err := dcrdConnect()
if err != nil {
log.Errorf("dcrd connection error: %v", err)
@ -77,7 +79,8 @@ func run(ctx context.Context) error {
// Create RPC client for remote dcrwallet instance (used for voting).
// Dial once just to validate config.
walletConnect := rpc.Setup(ctx, &shutdownWg, cfg.WalletUser, cfg.WalletPass, cfg.WalletHost, cfg.walletCert)
walletConnect := rpc.Setup(ctx, &shutdownWg, cfg.WalletUser, cfg.WalletPass,
cfg.WalletHost, cfg.walletCert, nil)
walletConn, err := walletConnect()
if err != nil {
log.Errorf("dcrwallet connection error: %v", err)
@ -93,6 +96,20 @@ func run(ctx context.Context) error {
return err
}
// Create a dcrd client with an attached notification handler which will run
// in the background.
notifHandler := &background.NotificationHandler{
Ctx: ctx,
Db: db,
WalletConnect: walletConnect,
}
dcrdWithNotifHandler := rpc.Setup(ctx, &shutdownWg, cfg.DcrdUser, cfg.DcrdPass,
cfg.DcrdHost, cfg.dcrdCert, notifHandler)
// Start background process which will continually attempt to reconnect to
// dcrd if the connection drops.
background.Start(notifHandler, dcrdWithNotifHandler)
// TODO: This can move into webapi.Start()
signKey, pubKey, err := db.KeyPair()
if err != nil {

View File

@ -26,7 +26,7 @@ type Connect func() (Caller, error)
// function which can be called to access the client. The returned function will
// try to handle any client disconnects by attempting to reconnect, but will
// return an error if a new connection cannot be established.
func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr string, cert []byte) Connect {
func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr string, cert []byte, n wsrpc.Notifier) Connect {
// Create TLS options.
pool := x509.NewCertPool()
@ -54,7 +54,6 @@ func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr str
log.Debugf("RPC already closed (%s)", addr)
default:
log.Debugf("Closing RPC (%s)...", addr)
if err := c.Close(); err != nil {
log.Errorf("Failed to close RPC (%s): %v", addr, err)
} else {
@ -72,7 +71,7 @@ func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr str
if c != nil {
select {
case <-c.Done():
log.Infof("RPC client errored (%v); reconnecting...", c.Err())
log.Debugf("RPC client errored (%v); reconnecting...", c.Err())
c = nil
default:
return c, nil
@ -80,7 +79,7 @@ func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr str
}
var err error
c, err = wsrpc.Dial(ctx, fullAddr, tlsOpt, authOpt)
c, err = wsrpc.Dial(ctx, fullAddr, tlsOpt, authOpt, wsrpc.WithNotifier(n))
if err != nil {
return nil, err
}

View File

@ -3,6 +3,7 @@ package rpc
import (
"context"
"encoding/hex"
"errors"
"fmt"
"github.com/decred/dcrd/blockchain/stake/v3"
@ -45,16 +46,6 @@ func DcrdClient(ctx context.Context, c Caller) (*DcrdRPC, error) {
return &DcrdRPC{c, ctx}, nil
}
func (c *DcrdRPC) GetBlockHeader(blockHash string) (*dcrdtypes.GetBlockHeaderVerboseResult, error) {
verbose := true
var blockHeader dcrdtypes.GetBlockHeaderVerboseResult
err := c.Call(c.ctx, "getblockheader", &blockHeader, blockHash, verbose)
if err != nil {
return nil, err
}
return &blockHeader, nil
}
func (c *DcrdRPC) GetRawTransaction(txHash string) (*dcrdtypes.TxRawResult, error) {
verbose := 1
var resp dcrdtypes.TxRawResult
@ -76,11 +67,11 @@ func (c *DcrdRPC) SendRawTransaction(txHex string) (string, error) {
}
func (c *DcrdRPC) GetTicketCommitmentAddress(ticketHash string, netParams *chaincfg.Params) (string, error) {
// Retrieve and parse the transaction.
resp, err := c.GetRawTransaction(ticketHash)
if err != nil {
return "", err
}
msgHex, err := hex.DecodeString(resp.Hex)
if err != nil {
return "", err
@ -89,6 +80,16 @@ func (c *DcrdRPC) GetTicketCommitmentAddress(ticketHash string, netParams *chain
if err = msgTx.FromBytes(msgHex); err != nil {
return "", err
}
// Ensure transaction is a valid ticket.
if !stake.IsSStx(msgTx) {
return "", errors.New("invalid transcation - not sstx")
}
if len(msgTx.TxOut) != 3 {
return "", fmt.Errorf("invalid transcation - expected 3 outputs, got %d", len(msgTx.TxOut))
}
// Get ticket commitment address.
addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, netParams)
if err != nil {
return "", err
@ -96,3 +97,7 @@ func (c *DcrdRPC) GetTicketCommitmentAddress(ticketHash string, netParams *chain
return addr.Address(), nil
}
func (c *DcrdRPC) NotifyBlocks() error {
return c.Call(c.ctx, "notifyblocks", nil)
}

View File

@ -1,13 +1,10 @@
package webapi
import (
"encoding/hex"
"net/http"
"sync"
"time"
"github.com/decred/dcrd/blockchain/stake/v3"
"github.com/decred/dcrd/wire"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/jholdstock/dcrvsp/database"
@ -59,15 +56,13 @@ func feeAddress(c *gin.Context) {
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 now.After(time.Unix(ticket.FeeExpiration, 0)) {
expire = now.Add(cfg.FeeAddressExpiration).Unix()
VSPFee = cfg.VSPFee
if ticket.FeeExpired() {
ticket.FeeExpiration = now.Add(cfg.FeeAddressExpiration).Unix()
ticket.VSPFee = cfg.VSPFee
err := db.UpdateExpireAndFee(ticket.Hash, expire, VSPFee)
err := db.UpdateTicket(ticket)
if err != nil {
log.Errorf("UpdateExpireAndFee error: %v", err)
log.Errorf("UpdateTicket error: %v", err)
sendErrorResponse("database error", http.StatusInternalServerError, c)
return
}
@ -76,8 +71,8 @@ func feeAddress(c *gin.Context) {
Timestamp: now.Unix(),
Request: feeAddressRequest,
FeeAddress: ticket.FeeAddress,
Fee: VSPFee,
Expiration: expire,
Fee: ticket.VSPFee,
Expiration: ticket.FeeExpiration,
}, c)
return
@ -88,56 +83,25 @@ func feeAddress(c *gin.Context) {
ticketHash := feeAddressRequest.TicketHash
// Ensure ticket exists and is mined.
resp, err := dcrdClient.GetRawTransaction(ticketHash)
// Get transaction details.
rawTx, err := dcrdClient.GetRawTransaction(ticketHash)
if err != nil {
log.Warnf("Could not retrieve tx %s for %s: %v", ticketHash, c.ClientIP(), err)
sendErrorResponse("unknown transaction", http.StatusBadRequest, c)
return
}
if resp.Confirmations < 2 || resp.BlockHeight < 0 {
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) {
// Don't accept tickets which are too old.
if rawTx.Confirmations > int64(uint32(cfg.NetParams.TicketMaturity)+cfg.NetParams.TicketExpiry) {
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 {
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 {
log.Errorf("Failed to deserialize tx: %v", err)
sendErrorResponse("failed to deserialize transaction", http.StatusInternalServerError, c)
return
}
if !stake.IsSStx(msgTx) {
log.Warnf("Non-ticket tx from %s", c.ClientIP())
sendErrorResponse("transaction is not a ticket", http.StatusBadRequest, c)
return
}
if len(msgTx.TxOut) != 3 {
log.Warnf("Invalid ticket from %s", c.ClientIP())
sendErrorResponse("invalid ticket", http.StatusBadRequest, c)
return
}
// get blockheight and sdiff which is required by
// txrules.StakePoolTicketFee, and store them in the database
// for processing by payfee
blockHeader, err := dcrdClient.GetBlockHeader(resp.BlockHash)
if err != nil {
log.Errorf("GetBlockHeader error: %v", err)
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
return
// Check if ticket is fully confirmed.
var confirmed bool
if rawTx.Confirmations >= requiredConfs {
confirmed = true
}
newAddress, newAddressIdx, err := getNewFeeAddress(db, addrGen)
@ -153,22 +117,21 @@ func feeAddress(c *gin.Context) {
CommitmentAddress: commitmentAddress,
FeeAddressIndex: newAddressIdx,
FeeAddress: newAddress,
SDiff: blockHeader.SBits,
BlockHeight: int64(blockHeader.Height),
Confirmed: confirmed,
VSPFee: cfg.VSPFee,
FeeExpiration: expire,
// VotingKey and VoteChoices: set during payfee
}
err = db.InsertTicket(dbTicket)
err = db.InsertNewTicket(dbTicket)
if err != nil {
log.Errorf("InsertTicket error: %v", err)
sendErrorResponse("database error", http.StatusInternalServerError, c)
return
}
log.Debugf("Fee address created for new ticket: feeAddrIdx=%d, "+
"feeAddr=%s, ticketHash=%s", newAddressIdx, newAddress, ticketHash)
log.Debugf("Fee address created for new ticket: tktConfirmed=%t, feeAddrIdx=%d, "+
"feeAddr=%s, ticketHash=%s", confirmed, newAddressIdx, newAddress, ticketHash)
sendJSONResponse(feeAddressResponse{
Timestamp: now.Unix(),

View File

@ -101,7 +101,7 @@ func vspAuth() gin.HandlerFunc {
commitmentAddress, err = dcrdClient.GetTicketCommitmentAddress(hash, cfg.NetParams)
if err != nil {
log.Errorf("GetTicketCommitmentAddress error: %v", err)
sendErrorResponse("database error", http.StatusInternalServerError, c)
sendErrorResponse(err.Error(), http.StatusInternalServerError, c)
return
}
}

View File

@ -1,12 +1,10 @@
package webapi
import (
"bytes"
"encoding/hex"
"net/http"
"time"
"decred.org/dcrwallet/wallet/txrules"
"github.com/decred/dcrd/dcrec"
"github.com/decred/dcrd/dcrutil/v3"
"github.com/decred/dcrd/txscript/v3"
@ -25,7 +23,6 @@ func payFee(c *gin.Context) {
ticket := c.MustGet("Ticket").(database.Ticket)
knownTicket := c.MustGet("KnownTicket").(bool)
dcrdClient := c.MustGet("DcrdClient").(*rpc.DcrdRPC)
walletClient := c.MustGet("WalletClient").(*rpc.WalletRPC)
if !knownTicket {
log.Warnf("Invalid ticket from %s", c.ClientIP())
@ -40,17 +37,22 @@ func payFee(c *gin.Context) {
return
}
// Respond early if fee transaction has already been broadcast for this
// ticket.
if ticket.FeeTxHash != "" {
// Respond early if we already have the fee tx for this ticket.
if ticket.FeeTxHex != "" {
sendJSONResponse(payFeeResponse{
Timestamp: time.Now().Unix(),
TxHash: ticket.FeeTxHash,
Request: payFeeRequest,
}, c)
return
}
// Respond early if the fee for this ticket is expired.
if ticket.FeeExpired() {
log.Warnf("Expired payfee request from %s", c.ClientIP())
sendErrorResponse("fee has expired", http.StatusBadRequest, c)
return
}
// Validate VotingKey.
votingKey := payFeeRequest.VotingKey
votingWIF, err := dcrutil.DecodeWIF(votingKey, cfg.NetParams.PrivateKeyID)
@ -85,21 +87,6 @@ func payFee(c *gin.Context) {
return
}
feeTxBuf := new(bytes.Buffer)
feeTxBuf.Grow(feeTx.SerializeSize())
err = feeTx.Serialize(feeTxBuf)
if err != nil {
log.Errorf("Serialize tx failed: %v", err)
sendErrorResponse("serialize tx error", http.StatusInternalServerError, c)
return
}
if ticket.FeeExpired() {
log.Warnf("Expired payfee request from %s", c.ClientIP())
sendErrorResponse("fee has expired", http.StatusBadRequest, c)
return
}
// Loop through transaction outputs until we find one which pays to the
// expected fee address. Record how much is being paid to the fee address.
var feeAmount dcrutil.Amount
@ -138,79 +125,59 @@ findAddress:
// TODO: DB - validate votingkey against ticket submission address
sDiff := dcrutil.Amount(ticket.SDiff)
// TODO: Relay fee should not be hard coded
relayFee, err := dcrutil.NewAmount(0.0001)
minFee, err := dcrutil.NewAmount(cfg.VSPFee)
if err != nil {
log.Errorf("relayfee failed: %v", err)
sendErrorResponse("relayfee error", http.StatusInternalServerError, c)
log.Errorf("dcrutil.NewAmount: %v", err)
sendErrorResponse("fee error", http.StatusInternalServerError, c)
return
}
minFee := txrules.StakePoolTicketFee(sDiff, relayFee, int32(ticket.BlockHeight), cfg.VSPFee, cfg.NetParams)
if feeAmount < minFee {
log.Errorf("Fee too small: was %v, expected %v", feeAmount, minFee)
log.Warnf("Fee too small: was %v, expected %v", feeAmount, minFee)
sendErrorResponse("fee too small", http.StatusInternalServerError, c)
return
}
// At this point we are satisfied that the request is valid and the FeeTx
// pays sufficient fees to the expected address.
// Proceed to update the database and broadcast the transaction.
// pays sufficient fees to the expected address. Proceed to update the
// database, and if the ticket is confirmed broadcast the transaction.
feeTxHash, err := dcrdClient.SendRawTransaction(hex.EncodeToString(feeTxBuf.Bytes()))
if err != nil {
log.Errorf("SendRawTransaction failed: %v", err)
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
return
}
ticket.VotingWIF = votingWIF.String()
ticket.FeeTxHex = payFeeRequest.FeeTx
ticket.VoteChoices = voteChoices
err = db.SetTicketVotingKey(ticket.Hash, votingWIF.String(), voteChoices, feeTxHash)
err = db.UpdateTicket(ticket)
if err != nil {
log.Errorf("SetTicketVotingKey failed: %v", err)
log.Errorf("InsertTicket failed: %v", err)
sendErrorResponse("database error", http.StatusInternalServerError, c)
return
}
// TODO: Should return a response here. We don't want to add the ticket to
// the voting wallets until the fee tx has been confirmed.
log.Debugf("Fee tx received for ticket: ticketHash=%s", ticket.Hash)
// Add ticket to voting wallets.
rawTicket, err := dcrdClient.GetRawTransaction(ticket.Hash)
if err != nil {
log.Warnf("Could not retrieve tx %s for %s: %v", ticket.Hash, c.ClientIP(), err)
sendErrorResponse("unknown transaction", http.StatusBadRequest, c)
return
}
err = walletClient.AddTransaction(rawTicket.BlockHash, rawTicket.Hex)
if err != nil {
log.Errorf("AddTransaction failed: %v", err)
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
return
}
err = walletClient.ImportPrivKey(votingWIF.String())
if err != nil {
log.Errorf("ImportPrivKey failed: %v", err)
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
return
}
// Update vote choices on voting wallets.
for agenda, choice := range voteChoices {
err = walletClient.SetVoteChoice(agenda, choice, ticket.Hash)
if ticket.Confirmed {
feeTxHash, err := dcrdClient.SendRawTransaction(payFeeRequest.FeeTx)
if err != nil {
log.Errorf("SetVoteChoice failed: %v", err)
// TODO: SendRawTransaction can return a "transcation already
// exists" error, which isnt necessarily a problem here.
log.Errorf("SendRawTransaction failed: %v", err)
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
return
}
ticket.FeeTxHash = feeTxHash
err = db.UpdateTicket(ticket)
if err != nil {
log.Errorf("InsertTicket failed: %v", err)
sendErrorResponse("database error", http.StatusInternalServerError, c)
return
}
log.Debugf("Fee tx broadcast for ticket: ticketHash=%s", ticket.Hash)
}
sendJSONResponse(payFeeResponse{
Timestamp: time.Now().Unix(),
TxHash: feeTxHash,
Request: payFeeRequest,
}, c)
}

View File

@ -42,23 +42,29 @@ func setVoteChoices(c *gin.Context) {
// Update VoteChoices in the database before updating the wallets. DB is
// source of truth and is less likely to error.
err = db.UpdateVoteChoices(ticket.Hash, voteChoices)
ticket.VoteChoices = voteChoices
err = db.UpdateTicket(ticket)
if err != nil {
log.Errorf("UpdateVoteChoices error: %v", err)
log.Errorf("UpdateTicket error: %v", err)
sendErrorResponse("database error", http.StatusInternalServerError, c)
return
}
// Update vote choices on voting wallets.
for agenda, choice := range voteChoices {
err = walletClient.SetVoteChoice(agenda, choice, ticket.Hash)
if err != nil {
log.Errorf("SetVoteChoice failed: %v", err)
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
return
// Update vote choices on voting wallets. Tickets are only added to voting
// wallets if their fee is confirmed.
if ticket.FeeConfirmed {
for agenda, choice := range voteChoices {
err = walletClient.SetVoteChoice(agenda, choice, ticket.Hash)
if err != nil {
log.Errorf("SetVoteChoice failed: %v", err)
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
return
}
}
}
log.Debugf("Vote choices updated for ticket: ticketHash=%s", ticket.Hash)
// TODO: DB - error if given timestamp is older than any previous requests
// TODO: DB - store setvotechoices receipt in log

View File

@ -14,7 +14,7 @@
<table>
<tr><td>Total tickets:</td><td>{{ .TotalTickets }}</td></tr>
<tr><td>FeePaid tickets:</td><td>{{ .FeePaidTickets }}</td></tr>
<tr><td>VSP Fee:</td><td>{{ .VSPFee }}</td></tr>
<tr><td>VSP Fee:</td><td>{{ .VSPFee }} DCR per ticket</td></tr>
<tr><td>Network:</td><td>{{ .Network }}</td></tr>
</table>
<p>Last updated: {{.UpdateTime}}</p>

View File

@ -33,7 +33,6 @@ type PayFeeRequest struct {
type payFeeResponse struct {
Timestamp int64 `json:"timestamp" binding:"required"`
TxHash string `json:"txhash" binding:"required"`
Request PayFeeRequest `json:"request" binding:"required"`
}

View File

@ -27,6 +27,12 @@ type Config struct {
FeeAddressExpiration time.Duration
}
// The number of confirmations required to consider a ticket purchase or a fee
// transaction to be final.
const (
requiredConfs = 6
)
var homepageData *gin.H
var cfg Config
@ -168,13 +174,13 @@ func router(debugMode bool) *gin.Engine {
)
feeOnly.POST("/feeaddress", feeAddress)
feeOnly.GET("/ticketstatus", ticketStatus)
feeOnly.POST("/payfee", payFee)
// These API routes access dcrd and the voting wallets, and they need
// authentication.
both := router.Group("/api").Use(
withDcrdClient(), withWalletClient(), vspAuth(),
)
both.POST("/payfee", payFee)
both.POST("/setvotechoices", setVoteChoices)
return router