diff --git a/.gitignore b/.gitignore index 4a7308ee..d561fb4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ -.vscode/ config.toml dcrstakepool vendor .vscode/ -config.go* \ No newline at end of file +controllers/config.go* +testing/ +*.orig + diff --git a/config.go b/config.go index d4ab039e..11364e03 100644 --- a/config.go +++ b/config.go @@ -93,8 +93,8 @@ type config struct { MinServers int `long:"minservers" description:"Minimum number of wallets connected needed to avoid errors"` } -// serviceOptions defines the configuration options for the daemon as a service on -// Windows. +// serviceOptions defines the configuration options for the daemon as a service +// on Windows. type serviceOptions struct { ServiceCommand string `short:"s" long:"service" description:"Service command {install, remove, start, stop}"` } diff --git a/controllers/dcrclient.go b/controllers/dcrclient.go index 4ad5e7b3..a0faa343 100644 --- a/controllers/dcrclient.go +++ b/controllers/dcrclient.go @@ -1,4 +1,5 @@ // dcrclient.go + package controllers import ( @@ -10,6 +11,7 @@ import ( "sync/atomic" "time" + "github.com/decred/dcrd/blockchain/stake" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/dcrjson" "github.com/decred/dcrrpcclient" @@ -59,11 +61,11 @@ var ( ) var ( - ErrSetVoteBitsCoolDown = fmt.Errorf("can not set the vote bits because " + - "last call was too soon") + ErrSetVoteBitsCoolDown = fmt.Errorf("Cannot set the vote bits because " + + "last call was too recent.") ) -// calcNextReqDifficultyResponse +// getNewAddressResponse type getNewAddressResponse struct { address dcrutil.Address err error @@ -159,6 +161,18 @@ type setTicketVoteBitsMsg struct { reply chan setTicketVoteBitsResponse } +// setTicketsVoteBitsResponse +type setTicketsVoteBitsResponse struct { + err error +} + +// setTicketsVoteBitsMsg +type setTicketsVoteBitsMsg struct { + hashes []*chainhash.Hash + votesBits []stake.VoteBits + reply chan setTicketsVoteBitsResponse +} + // getTxOutResponse type getTxOutResponse struct { txOut *dcrjson.GetTxOutResult @@ -227,6 +241,13 @@ type connectionError error func (w *walletSvrManager) walletRPCHandler() { out: for { + select { + case setVoteBitsErr := <-w.setVoteBitsResyncChan: + if setVoteBitsErr != nil { + log.Error("Error syncing vote bits: ", setVoteBitsErr) + } + default: + } select { case m := <-w.msgChan: switch msg := m.(type) { @@ -338,7 +359,8 @@ func (w *walletSvrManager) executeInSequence(fn functionName, msg interface{}) i if !bytes.Equal(addrs[i].ScriptAddress(), addrs[i+1].ScriptAddress()) { log.Infof("getNewAddressFn nonequiv failure on servers "+ - "%v, %v (%v != %v)", i, i+1, addrs[i].ScriptAddress(), addrs[i+1].ScriptAddress()) + "%v, %v (%v != %v)", i, i+1, addrs[i].ScriptAddress(), + addrs[i+1].ScriptAddress()) resp.err = fmt.Errorf("non equivalent address returned") return resp } @@ -509,6 +531,8 @@ func (w *walletSvrManager) executeInSequence(fn functionName, msg interface{}) i if w.servers[i] == nil { continue } + // Returns all tickets - even unconfirmed/mempool - when wallet is + // queried tfar, err := s.TicketsForAddress(tfam.address) if err != nil && (err != dcrrpcclient.ErrClientDisconnect && err != dcrrpcclient.ErrClientShutdown) { @@ -1054,8 +1078,8 @@ func (w *walletSvrManager) checkForSyncness(spuirs []*dcrjson.StakePoolUserInfoR // GetNewAddress // -// This should return equivalent results from all wallet RPCs. If this encounters -// a failure, it should be considered fatal. +// This should return equivalent results from all wallet RPCs. If this +// encounters a failure, it should be considered fatal. func (w *walletSvrManager) GetNewAddress() (dcrutil.Address, error) { // Assert that all servers are online. _, err := w.connected() @@ -1071,8 +1095,8 @@ func (w *walletSvrManager) GetNewAddress() (dcrutil.Address, error) { // ValidateAddress // -// This should return equivalent results from all wallet RPCs. If this encounters -// a failure, it should be considered fatal. +// This should return equivalent results from all wallet RPCs. If this +// encounters a failure, it should be considered fatal. func (w *walletSvrManager) ValidateAddress(addr dcrutil.Address) (*dcrjson.ValidateAddressWalletResult, error) { // Assert that all servers are online. _, err := w.connected() @@ -1091,8 +1115,8 @@ func (w *walletSvrManager) ValidateAddress(addr dcrutil.Address) (*dcrjson.Valid // CreateMultisig // -// This should return equivalent results from all wallet RPCs. If this encounters -// a failure, it should be considered fatal. +// This should return equivalent results from all wallet RPCs. If this +// encounters a failure, it should be considered fatal. func (w *walletSvrManager) CreateMultisig(nreq int, addrs []dcrutil.Address) (*dcrjson.CreateMultiSigResult, error) { // Assert that all servers are online. _, err := w.connected() @@ -1112,8 +1136,8 @@ func (w *walletSvrManager) CreateMultisig(nreq int, addrs []dcrutil.Address) (*d // ImportScript // -// This should return equivalent results from all wallet RPCs. If this encounters -// a failure, it should be considered fatal. +// This should return equivalent results from all wallet RPCs. If this +// encounters a failure, it should be considered fatal. func (w *walletSvrManager) ImportScript(script []byte, height int) error { // Assert that all servers are online. _, err := w.connected() @@ -1197,8 +1221,8 @@ func (w *walletSvrManager) GetTicketsVoteBits(hashes []*chainhash.Hash) (*dcrjso // SetTicketVoteBits // -// This should return equivalent results from all wallet RPCs. If this encounters -// a failure, it should be considered fatal. +// This should return equivalent results from all wallet RPCs. If this +// encounters a failure, it should be considered fatal. func (w *walletSvrManager) SetTicketVoteBits(hash *chainhash.Hash, voteBits uint16) error { // Assert that all servers are online. _, err := w.connected() @@ -1232,6 +1256,44 @@ func (w *walletSvrManager) SetTicketVoteBits(hash *chainhash.Hash, voteBits uint return response.err } +// SetTicketsVoteBits +// +// This should return equivalent results from all wallet RPCs. If this +// encounters a failure, it should be considered fatal. +func (w *walletSvrManager) SetTicketsVoteBits(hashes []*chainhash.Hash, votesBits []stake.VoteBits) error { + // Assert that all servers are online. + _, err := w.connected() + if err != nil { + return connectionError(err) + } + + w.setVoteBitsCoolDownMutex.Lock() + defer w.setVoteBitsCoolDownMutex.Unlock() + + // Throttle how often the user is allowed to change their stake + // vote bits. + // TODO: handle this better + vbSetTime, ok := w.setVoteBitsCoolDownMap[*hashes[0]] + if ok { + if time.Now().Sub(vbSetTime) < allowTimerSetVoteBits { + return ErrSetVoteBitsCoolDown + } + } + + reply := make(chan setTicketsVoteBitsResponse) + w.msgChan <- setTicketsVoteBitsMsg{ + hashes: hashes, + votesBits: votesBits, + reply: reply, + } + + // If the set was successful, reset the timer. + w.setVoteBitsCoolDownMap[*hashes[0]] = time.Now() + + response := <-reply + return response.err +} + // GetTxOut gets a txOut status given a hash and an output index. It returns // nothing if the output is spent, and a standard response if it is unspent. // @@ -1330,8 +1392,8 @@ func NewGetTicketsCacheData(tfar *dcrjson.TicketsForAddressResult) *getTicketsCa return &getTicketsCacheData{tfar, time.Now()} } -// walletSvrManager provides a concurrency safe RPC call manager for handling all -// incoming wallet server requests. +// walletSvrManager provides a concurrency safe RPC call manager for handling +// all incoming wallet server requests. type walletSvrManager struct { servers []*dcrrpcclient.Client serversLen int @@ -1368,11 +1430,18 @@ type walletSvrManager struct { // minServers is the minimum number of servers required before alerting minServers int + setVoteBitsResyncChan chan error + started int32 shutdown int32 msgChan chan interface{} wg sync.WaitGroup quit chan struct{} + + // ticketDataLock is a mutex for vote bits set/get calls. + ticketDataLock sync.RWMutex + //ticketTryLock chan struct{} + ticketDataBlocker int32 } // Start begins the core block handler which processes block and inv messages. @@ -1407,7 +1476,229 @@ func (w *walletSvrManager) IsStopped() bool { return w.shutdown == 1 } -// IsStopped +func (w *walletSvrManager) CheckServers() error { + if w.serversLen == 0 { + return fmt.Errorf("No RPC servers") + } + + for i := range w.servers { + wi, err := w.servers[i].WalletInfo() + if err != nil { + return err + } + if !wi.DaemonConnected { + return fmt.Errorf("Wallet on svr %d not connected\n", i) + } + if !wi.StakeMining { + return fmt.Errorf("Wallet on svr %d not stakemining.\n", i) + } + if !wi.Unlocked { + return fmt.Errorf("Wallet on svr %d not unlocked.\n", i) + } + } + + return nil +} + +// CheckWalletsReady is a way to verify that each wallets' stake manager is up +// and running, before walletRPCHandler has been started running. +func (w *walletSvrManager) CheckWalletsReady() error { + if w.serversLen == 0 { + return fmt.Errorf("No RPC servers") + } + + for i, s := range w.servers { + _, err := s.GetStakeInfo() + if err != nil { + log.Errorf("GetStakeInfo failured on server %v: %v", i, err) + return err + } + } + return nil +} + +func getMinedTickets(cl *dcrrpcclient.Client, th []*chainhash.Hash) []*chainhash.Hash { + var ticketHashesMined []*chainhash.Hash + for _, th := range th { + res, err := cl.GetRawTransactionVerbose(th) + if err == nil && res.Confirmations > 0 { + ticketHashesMined = append(ticketHashesMined, th) + } + } + return ticketHashesMined +} + +// SyncVoteBits ensures that the wallet servers are all in sync with each +// other in terms of vote bits. Call on creation. +func (w *walletSvrManager) SyncVoteBits() error { + // Check for connectivity and if unlocked. + err := w.CheckServers() + if err != nil { + return err + } + + // Check live tickets + // legacyrpc.getTickets excludes spent tickets + ticketHashes, err := w.servers[0].GetTickets(true) + if err != nil { + return err + } + ticketHashesMined := getMinedTickets(w.servers[0], ticketHashes) + numLiveTickets := len(ticketHashesMined) + log.Infof("Excluding %d unmined tickets in votebits sync.", + len(ticketHashes)-numLiveTickets) + + // gsi, err := w.servers[0].GetStakeInfo() + // if err != nil { + // return err + // } + // if int(gsi.Live+gsi.Immature) != numLiveTickets { + // return fmt.Errorf("Number of live tickets inconsistent: %v, %v", + // gsi.Live+gsi.Immature, numLiveTickets) + // } + + // Check number of tickets + + for i, cl := range w.servers { + if i == 0 { + continue + } + + ticketHashes, err = cl.GetTickets(true) + //gsi, err = cl.GetStakeInfo() + if err != nil { + return err + } + + thMined := getMinedTickets(w.servers[0], ticketHashes) + + if numLiveTickets != len(thMined) { + log.Errorf("Non-equivalent number of tickets on servers %v, %v "+ + " (%v, %v)", 0, i, numLiveTickets, len(thMined)) + return fmt.Errorf("non equivalent num elements returned") + } + } + + return w.SyncTicketsVoteBits(ticketHashesMined) +} + +// SyncTicketsVoteBits ensures that the wallet servers are all in sync with each +// other in terms of vote bits of the given tickets. First wallet rules. +func (w *walletSvrManager) SyncTicketsVoteBits(tickets []*chainhash.Hash) error { + if len(tickets) == 0 { + return nil + } + + // Check for connectivity and if unlocked. + err := w.CheckServers() + if err != nil { + return err + } + + // Get a write lock, allowing other get functions to complete + w.ticketDataLock.Lock() + defer w.ticketDataLock.Unlock() + + // Set a flag so other operations, like the web endpoint handlers, do not + // have to block. Tickets POST handler also writes. + if !atomic.CompareAndSwapInt32(&w.ticketDataBlocker, 0, 1) { + return fmt.Errorf("SyncTicketsVoteBits already taking place.") + } + defer atomic.StoreInt32(&w.ticketDataBlocker, 0) + + log.Infof("Beginning resync of vote bits for %d tickets.", len(tickets)) + + // Go through each server, get ticket vote bits + votebitsPerServer := make([]map[chainhash.Hash]uint16, w.serversLen) + + for i, cl := range w.servers { + votebitsPerServer[i] = make(map[chainhash.Hash]uint16) + + votebits, err := cl.GetTicketsVoteBits(tickets) + if err != nil { + return fmt.Errorf("GetTicketsVoteBits failed: %v", err) + } + + vbl := votebits.VoteBitsList + // numTickets := len(vbl) + + for ih, hash := range tickets { + votebitsPerServer[i][*hash] = vbl[ih].VoteBits + } + } + + // Synchronize, using first server's bits if different + // NOTE: This does not check for missing tickets. + masterVotebitsMap := votebitsPerServer[0] + for i, votebitsMap := range votebitsPerServer { + if i == 0 { + continue + } + + for hash, votebits := range votebitsMap { + refVoteBits, ok := masterVotebitsMap[hash] + if !ok { + return fmt.Errorf("Ticket not present on all RPC servers: %v", + hash) + } + if votebits != refVoteBits { + err := w.servers[i].SetTicketVoteBits(&hash, refVoteBits) + if err != nil { + return err + } + } + } + } + + log.Infof("Completed resync of vote bits for %d tickets.", len(tickets)) + + return nil +} + +func (w *walletSvrManager) SyncUserVoteBits(userMultiSigAddress dcrutil.Address) error { + // Check for connectivity and if unlocked. + err := w.CheckServers() + if err != nil { + return err + } + + // Get all live tickets for user + ticketHashes, err := w.GetUnspentUserTickets(userMultiSigAddress) + if err != nil { + return err + } + + return w.SyncTicketsVoteBits(ticketHashes) +} + +// GetUnspentUserTickets gets live and immature tickets for a stakepool user +func (w *walletSvrManager) GetUnspentUserTickets(userMultiSigAddress dcrutil.Address) ([]*chainhash.Hash, error) { + // live tickets only + var tickethashes []*chainhash.Hash + + // TicketsForAddress returns all tickets, not just live, when wallet is + // queried rather than just the node. With StakePoolUserInfo, "live" status + // includes immature, but not spent. + spui, err := w.StakePoolUserInfo(userMultiSigAddress) + if err != nil { + return tickethashes, err + } + + for _, ticket := range spui.Tickets { + // "live" includes immature + if ticket.Status == "live" { + th, err := chainhash.NewHashFromStr(ticket.Ticket) + if err != nil { + log.Errorf("NewHashFromStr failed for %v", ticket) + return tickethashes, err + } + tickethashes = append(tickethashes, th) + } + } + + return tickethashes, nil +} + func (w *walletSvrManager) WalletStatus() ([]*dcrjson.WalletInfoResult, error) { return w.connected() } @@ -1683,7 +1974,9 @@ func connectWalletRPC(walletHost string, walletCert string, walletUser string, w // newWalletSvrManager returns a new decred wallet server manager. // Use Start to begin processing asynchronous block and inv updates. -func newWalletSvrManager(walletHosts []string, walletCerts []string, walletUsers []string, walletPasswords []string, minServers int) (*walletSvrManager, error) { +func newWalletSvrManager(walletHosts []string, walletCerts []string, + walletUsers []string, walletPasswords []string, minServers int) (*walletSvrManager, error) { + var err error localServers := make([]*dcrrpcclient.Client, len(walletHosts), len(walletHosts)) for i := range walletHosts { @@ -1693,6 +1986,7 @@ func newWalletSvrManager(walletHosts []string, walletCerts []string, walletUsers return nil, err } } + wsm := walletSvrManager{ walletHosts: walletHosts, walletCerts: walletCerts, @@ -1700,16 +1994,14 @@ func newWalletSvrManager(walletHosts []string, walletCerts []string, walletUsers walletPasswords: walletPasswords, servers: localServers, serversLen: len(localServers), + cachedStakeInfoTimer: time.Now().Add(-cacheTimerStakeInfo), cachedGetTicketsMap: make(map[string]*getTicketsCacheData), setVoteBitsCoolDownMap: make(map[chainhash.Hash]time.Time), + setVoteBitsResyncChan: make(chan error, 500), msgChan: make(chan interface{}, 500), quit: make(chan struct{}), minServers: minServers, } - // Set the timer to automatically require a new set of stake information - // on startup. - wsm.cachedStakeInfoTimer = time.Now().Add(-cacheTimerStakeInfo) - return &wsm, nil } diff --git a/controllers/main.go b/controllers/main.go index 7e9865a2..61adbda9 100644 --- a/controllers/main.go +++ b/controllers/main.go @@ -5,21 +5,21 @@ import ( "encoding/hex" "errors" "fmt" + "html/template" "net/http" "net/smtp" "strconv" "strings" + "sync/atomic" "time" - "html/template" - "github.com/decred/dcrd/chaincfg" - "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/dcrjson" "github.com/decred/dcrutil" "github.com/decred/dcrutil/hdkeychain" "github.com/decred/dcrwallet/waddrmgr" + "github.com/decred/dcrd/blockchain/stake" "github.com/decred/dcrstakepool/helpers" "github.com/decred/dcrstakepool/models" "github.com/decred/dcrstakepool/system" @@ -41,8 +41,10 @@ const signupEmailTemplate = "A request for an account for __URL__\r\n" + "to verify your email address and finalize registration.\r\n\n" const signupEmailSubject = "Stake pool email verification" -// MainController +// MainController is the wallet RPC controller type. Its methods include the +// route handlers. type MainController struct { + // embed type for c.Env[""] context and ExecuteTemplate helpers system.Controller adminIPs []string @@ -269,7 +271,7 @@ func (controller *MainController) APIAddress(c web.C, r *http.Request) ([]string return nil, "system error", errors.New("unable to process wallet commands") } - models.UpdateUserById(dbMap, uid64, createMultiSig.Address, + models.UpdateUserByID(dbMap, uid64, createMultiSig.Address, createMultiSig.RedeemScript, poolPubKeyAddr, userPubKeyAddr, userFeeAddr.EncodeAddress(), bestBlockHeight) @@ -526,10 +528,35 @@ func (controller *MainController) RPCSync(dbMap *gorp.DbMap) error { if err != nil { return err } + err = walletSvrsSync(controller.rpcServers, multisigScripts) if err != nil { return err } + + // TODO: Wait for wallets to sync, or schedule the vote bits sync somehow. + // For now, just skip full vote bits sync in favor of on-demand user's vote + // bits sync if the wallets are busy at this point. + + // Allow sync to get going before attempting vote bits sync. + time.Sleep(2 * time.Second) + + // Look for that -4 message from wallet that says: "the wallet is + // currently syncing to the best block, please try again later" + err = controller.rpcServers.CheckWalletsReady() + if err != nil /*strings.Contains(err.Error(), "try again later")*/ { + // If importscript is running, it will take a while. + log.Errorf("Wallets are syncing. Unable to initiate votebits sync: %v", + err) + } else { + // Sync vote bits for all tickets owned by the wallet + err = controller.rpcServers.SyncVoteBits() + if err != nil { + log.Error(err) + return err + } + } + return nil } @@ -578,7 +605,7 @@ func (controller *MainController) Address(c web.C, r *http.Request) (string, int c.Env["Network"] = controller.params.Name c.Env["Flash"] = session.Flashes("address") - var widgets = controller.Parse(t, "address", c.Env) + widgets := controller.Parse(t, "address", c.Env) c.Env["Title"] = "Decred Stake Pool - Address" c.Env["Content"] = template.HTML(widgets) @@ -600,6 +627,7 @@ func (controller *MainController) AddressPost(c web.C, r *http.Request) (string, return "/", http.StatusSeeOther } + // Only accept address if user does not already have a PubKeyAddr set. dbMap := controller.GetDbMap(c) user := models.GetUserById(dbMap, session.Values["UserId"].(int64)) if len(user.UserPubKeyAddr) > 0 { @@ -619,6 +647,7 @@ func (controller *MainController) AddressPost(c web.C, r *http.Request) (string, return controller.Address(c, r) } + // Get dcrutil.Address for user from pubkey address string u, err := dcrutil.DecodeAddress(userPubKeyAddr, controller.params) if err != nil { session.AddFlash("Couldn't decode address", "address") @@ -631,6 +660,7 @@ func (controller *MainController) AddressPost(c web.C, r *http.Request) (string, return controller.Address(c, r) } + // Get new address from pool wallets if controller.RPCIsStopped() { return "/error", http.StatusSeeOther } @@ -640,6 +670,7 @@ func (controller *MainController) AddressPost(c web.C, r *http.Request) (string, return "/error", http.StatusSeeOther } + // From new address (pkh), get pubkey address if controller.RPCIsStopped() { return "/error", http.StatusSeeOther } @@ -650,12 +681,14 @@ func (controller *MainController) AddressPost(c web.C, r *http.Request) (string, } poolPubKeyAddr := poolValidateAddress.PubKeyAddr + // Get back Address from pool's new pubkey address p, err := dcrutil.DecodeAddress(poolPubKeyAddr, controller.params) if err != nil { controller.handlePotentialFatalError("DecodeAddress poolPubKeyAddr", err) return "/error", http.StatusSeeOther } + // Create the the multisig script. Result includes a P2SH and RedeemScript. if controller.RPCIsStopped() { return "/error", http.StatusSeeOther } @@ -665,6 +698,7 @@ func (controller *MainController) AddressPost(c web.C, r *http.Request) (string, return "/error", http.StatusSeeOther } + // Serialize the RedeemScript (hex string -> []byte) if controller.RPCIsStopped() { return "/error", http.StatusSeeOther } @@ -681,12 +715,15 @@ func (controller *MainController) AddressPost(c web.C, r *http.Request) (string, controller.handlePotentialFatalError("CreateMultisig DecodeString", err) return "/error", http.StatusSeeOther } + + // Import the RedeemScript err = controller.rpcServers.ImportScript(serializedScript, int(bestBlockHeight)) if err != nil { controller.handlePotentialFatalError("ImportScript", err) return "/error", http.StatusSeeOther } + // Get the pool fees address for this user uid64 := session.Values["UserId"].(int64) userFeeAddr, err := controller.FeeAddressForUserID(int(uid64)) if err != nil { @@ -694,7 +731,9 @@ func (controller *MainController) AddressPost(c web.C, r *http.Request) (string, return "/error", http.StatusSeeOther } - models.UpdateUserById(dbMap, uid64, createMultiSig.Address, + // Update the user's DB entry with multisig, user and pool pubkey + // addresses, and the fee address + models.UpdateUserByID(dbMap, uid64, createMultiSig.Address, createMultiSig.RedeemScript, poolPubKeyAddr, userPubKeyAddr, userFeeAddr.EncodeAddress(), bestBlockHeight) @@ -825,7 +864,7 @@ func (controller *MainController) Error(c web.C, r *http.Request) (string, int) c.Env["RateLimited"] = r.URL.Query().Get("rl") c.Env["Referer"] = r.URL.Query().Get("r") - var widgets = controller.Parse(t, "error", c.Env) + widgets := controller.Parse(t, "error", c.Env) c.Env["Content"] = template.HTML(widgets) return controller.Parse(t, "main", c.Env), http.StatusOK @@ -843,9 +882,12 @@ func (controller *MainController) Index(c web.C, r *http.Request) (string, int) c.Env["PoolLink"] = controller.poolLink t := controller.GetTemplate(c) + //t := c.Env["Template"].(*template.Template) + // execute the named template with data in c.Env widgets := helpers.Parse(t, "home", c.Env) + // With that kind of flags template can "figure out" what route is being rendered c.Env["IsIndex"] = true c.Env["Title"] = "Decred Stake Pool - Welcome" @@ -1204,10 +1246,11 @@ func (controller *MainController) SignIn(c web.C, r *http.Request) (string, int) t := controller.GetTemplate(c) session := controller.GetSession(c) + // Tell main.html what route is being rendered c.Env["IsSignIn"] = true c.Env["Flash"] = session.Flashes("auth") - var widgets = controller.Parse(t, "auth/signin", c.Env) + widgets := controller.Parse(t, "auth/signin", c.Env) c.Env["Title"] = "Decred Stake Pool - Sign In" c.Env["Content"] = template.HTML(widgets) @@ -1223,8 +1266,8 @@ func (controller *MainController) SignInPost(c web.C, r *http.Request) (string, session := controller.GetSession(c) dbMap := controller.GetDbMap(c) + // Validate email and password combination. user, err := helpers.Login(dbMap, email, password) - if err != nil { log.Infof(email+" login failed %v", err) session.AddFlash("Invalid Email or Password", "auth") @@ -1236,6 +1279,8 @@ func (controller *MainController) SignInPost(c web.C, r *http.Request) (string, return controller.SignIn(c, r) } + // If pool is closed and user has not yet provided a pubkey address, do not + // allow login. if controller.closePool { if len(user.UserPubKeyAddr) == 0 { session.AddFlash(controller.closePoolMsg, "auth") @@ -1247,10 +1292,12 @@ func (controller *MainController) SignInPost(c web.C, r *http.Request) (string, session.Values["UserId"] = user.Id + // Go to Address page if multisig script not yet set up. if user.MultiSigAddress == "" { return "/address", http.StatusSeeOther } + // Go to Tickets page if user already set up. return "/tickets", http.StatusSeeOther } @@ -1258,6 +1305,8 @@ func (controller *MainController) SignInPost(c web.C, r *http.Request) (string, func (controller *MainController) SignUp(c web.C, r *http.Request) (string, int) { t := controller.GetTemplate(c) session := controller.GetSession(c) + + // Tell main.html what route is being rendered c.Env["IsSignUp"] = true if controller.smtpHost == "" { c.Env["SMTPDisabled"] = true @@ -1271,7 +1320,7 @@ func (controller *MainController) SignUp(c web.C, r *http.Request) (string, int) c.Env["FlashSuccess"] = session.Flashes("signupSuccess") c.Env["RecaptchaSiteKey"] = controller.recaptchaSiteKey - var widgets = controller.Parse(t, "auth/signup", c.Env) + widgets := controller.Parse(t, "auth/signup", c.Env) c.Env["Title"] = "Decred Stake Pool - Sign Up" c.Env["Content"] = template.HTML(widgets) @@ -1395,7 +1444,7 @@ func (controller *MainController) Stats(c web.C, r *http.Request) (string, int) c.Env["UserCount"] = userCount c.Env["UserCountActive"] = userCountActive - var widgets = controller.Parse(t, "stats", c.Env) + widgets := controller.Parse(t, "stats", c.Env) c.Env["Content"] = template.HTML(widgets) return controller.Parse(t, "main", c.Env), http.StatusOK @@ -1482,7 +1531,7 @@ func (controller *MainController) Status(c web.C, r *http.Request) (string, int) c.Env["WalletInfo"] = walletPageInfo c.Env["RPCStatus"] = rpcstatus - var widgets = controller.Parse(t, "status", c.Env) + widgets := controller.Parse(t, "status", c.Env) c.Env["Content"] = template.HTML(widgets) if controller.RPCIsStopped() { @@ -1516,6 +1565,10 @@ func (controller *MainController) Tickets(c web.C, r *http.Request) (string, int ticketInfoMissed := map[int]TicketInfoHistoric{} ticketInfoVoted := map[int]TicketInfoHistoric{} + responseHeaderMap := make(map[string]string) + c.Env["ResponseHeaderMap"] = responseHeaderMap + // map is a reference type so responseHeaderMap may be modified + t := controller.GetTemplate(c) session := controller.GetSession(c) @@ -1536,68 +1589,116 @@ func (controller *MainController) Tickets(c web.C, r *http.Request) (string, int log.Info("Multisigaddress empty") } - ms, err := dcrutil.DecodeAddress(user.MultiSigAddress, controller.params) + if controller.RPCIsStopped() { + return "/error", http.StatusSeeOther + } + + // Get P2SH Address + multisig, err := dcrutil.DecodeAddress(user.MultiSigAddress, controller.params) if err != nil { c.Env["Error"] = "Invalid multisig data in database" log.Infof("Invalid address %v in database: %v", user.MultiSigAddress, err) } - var widgets = controller.Parse(t, "tickets", c.Env) + w := controller.rpcServers + // TODO: Tell the user if there is a cool-down + + // Attempt a "TryLock" so the page won't block + + // select { + // case <-w.ticketTryLock: + // w.ticketTryLock <- nil + // responseHeaderMap["Retry-After"] = "60" + // c.Env["Content"] = template.HTML("Ticket data resyncing. Please try again later.") + // return controller.Parse(t, "main", c.Env), http.StatusProcessing + // default: + // } + + if atomic.LoadInt32(&w.ticketDataBlocker) != 0 { + // with HTTP 102 we can specify an estimated time + responseHeaderMap["Retry-After"] = "60" + // Render page with messgae to try again later + //c.Env["Content"] = template.HTML("Ticket data resyncing. Please try again later.") + session.AddFlash("Ticket data now resyncing. Please try again later.", "tickets-warning") + c.Env["FlashWarn"] = session.Flashes("tickets-warning") + c.Env["Content"] = template.HTML(controller.Parse(t, "tickets", c.Env)) + return controller.Parse(t, "main", c.Env), http.StatusOK + } + // Vote bits sync is not running, but we also don't want a sync process + // starting. Note that the sync process locks this mutex before setting the + // atomic, so this shouldn't block. + w.ticketDataLock.RLock() + defer w.ticketDataLock.RUnlock() + + widgets := controller.Parse(t, "tickets", c.Env) + + // TODO: how could this happen? if err != nil { - log.Info("err is set") - c.Env["Content"] = template.HTML(widgets) + log.Info(err) widgets = controller.Parse(t, "tickets", c.Env) + c.Env["Content"] = template.HTML(widgets) return controller.Parse(t, "main", c.Env), http.StatusOK } - if controller.RPCIsStopped() { - return "/error", http.StatusSeeOther - } - - spui := new(dcrjson.StakePoolUserInfoResult) - spui, err = controller.rpcServers.StakePoolUserInfo(ms) + // spui := new(dcrjson.StakePoolUserInfoResult) + spui, err := w.StakePoolUserInfo(multisig) if err != nil { - // Log the error, but do not return. Consider reporting - // the error to the user on the page. A blank tickets - // page will be displayed in the meantime. + // Render page with messgae to try again later log.Infof("RPC StakePoolUserInfo failed: %v", err) + session.AddFlash("Unable to retreive stake pool user info.", "main") + c.Env["Flash"] = session.Flashes("main") + return controller.Parse(t, "main", c.Env), http.StatusInternalServerError } + // If the user has tickets, get their info if spui != nil && len(spui.Tickets) > 0 { - var tickethashes []*chainhash.Hash - - for _, ticket := range spui.Tickets { - th, err := chainhash.NewHashFromStr(ticket.Ticket) - if err != nil { - log.Infof("NewHashFromStr failed for %v", ticket) - return "/error?r=/tickets", http.StatusSeeOther - } - tickethashes = append(tickethashes, th) + // Only get or set votebits for live tickets + liveTicketHashes, err := w.GetUnspentUserTickets(multisig) + if err != nil { + return "/error?r=/tickets", http.StatusSeeOther } - // TODO: only get votebits for live tickets. - gtvb, err := controller.rpcServers.GetTicketsVoteBits(tickethashes) + gtvb, err := w.GetTicketsVoteBits(liveTicketHashes) if err != nil { + if err.Error() == "non equivalent votebits returned" { + // Launch a goroutine to repair these tickets vote bits + go w.SyncTicketsVoteBits(liveTicketHashes) + responseHeaderMap["Retry-After"] = "60" + // Render page with messgae to try again later + session.AddFlash("Detected mismatching vote bits. "+ + "Ticket data is now resyncing. Please try again after a "+ + "few minutes.", "tickets") + c.Env["Flash"] = session.Flashes("tickets") + c.Env["Content"] = template.HTML(controller.Parse(t, "tickets", c.Env)) + // Return with a 503 error indicating when to retry + return controller.Parse(t, "main", c.Env), http.StatusServiceUnavailable + } + log.Infof("GetTicketsVoteBits failed %v", err) return "/error?r=/tickets", http.StatusSeeOther } + voteBitMap := make(map[string]uint16) + for i := range liveTicketHashes { + voteBitMap[liveTicketHashes[i].String()] = gtvb.VoteBitsList[i].VoteBits + } + for idx, ticket := range spui.Tickets { - switch { - case ticket.Status == "live": + switch ticket.Status { + case "live": ticketInfoLive[idx] = TicketInfoLive{ Ticket: ticket.Ticket, TicketHeight: ticket.TicketHeight, - VoteBits: gtvb.VoteBitsList[idx].VoteBits, + VoteBits: voteBitMap[ticket.Ticket], //gtvbAll.VoteBitsList[idx].VoteBits, } - case ticket.Status == "missed": + case "missed": ticketInfoMissed[idx] = TicketInfoHistoric{ Ticket: ticket.Ticket, SpentByHeight: ticket.SpentByHeight, TicketHeight: ticket.TicketHeight, } - case ticket.Status == "voted": + case "voted": ticketInfoVoted[idx] = TicketInfoHistoric{ Ticket: ticket.Ticket, SpentBy: ticket.SpentBy, @@ -1617,13 +1718,22 @@ func (controller *MainController) Tickets(c web.C, r *http.Request) (string, int c.Env["TicketsMissed"] = ticketInfoMissed c.Env["TicketsVoted"] = ticketInfoVoted widgets = controller.Parse(t, "tickets", c.Env) + c.Env["Content"] = template.HTML(widgets) + c.Env["Flash"] = session.Flashes("tickets") return controller.Parse(t, "main", c.Env), http.StatusOK } // TicketsPost form submit route. func (controller *MainController) TicketsPost(c web.C, r *http.Request) (string, int) { + w := controller.rpcServers + + // If already processing let /tickets handle this + if atomic.LoadInt32(&w.ticketDataBlocker) != 0 { + return "/tickets", http.StatusSeeOther + } + chooseallow := r.FormValue("chooseallow") var voteBits = uint16(0) @@ -1640,16 +1750,25 @@ func (controller *MainController) TicketsPost(c web.C, r *http.Request) (string, } } + // Look up user, and try very hard to avoid a panic session := controller.GetSession(c) dbMap := controller.GetDbMap(c) - user := models.GetUserById(dbMap, session.Values["UserId"].(int64)) + id, ok := session.Values["UserId"].(int64) + if !ok { + log.Error("No valid UserID") + } + + user := models.GetUserById(dbMap, id) + if user == nil { + log.Error("Unable to find user with ID", id) + } if user.MultiSigAddress == "" { log.Info("Multisigaddress empty") return "/error?r=/tickets", http.StatusSeeOther } - ms, err := dcrutil.DecodeAddress(user.MultiSigAddress, controller.params) + multisig, err := dcrutil.DecodeAddress(user.MultiSigAddress, controller.params) if err != nil { log.Infof("Invalid address %v in database: %v", user.MultiSigAddress, err) return "/error?r=/tickets", http.StatusSeeOther @@ -1658,32 +1777,60 @@ func (controller *MainController) TicketsPost(c web.C, r *http.Request) (string, if controller.RPCIsStopped() { return "/error", http.StatusSeeOther } - spui, err := controller.rpcServers.StakePoolUserInfo(ms) - if err != nil { - log.Infof("RPC StakePoolUserInfo failed: %v", err) - return "/error?r=/tickets", http.StatusSeeOther - } - for _, ticket := range spui.Tickets { - if controller.RPCIsStopped() { - return "/error", http.StatusSeeOther + outPath := "/tickets" + status := http.StatusSeeOther + + // Set this off in a goroutine + // TODO: error on channel + go func() { + // write lock + w.ticketDataLock.Lock() + defer w.ticketDataLock.Unlock() + + var err error + defer func() { w.setVoteBitsResyncChan <- err }() + + if !atomic.CompareAndSwapInt32(&w.ticketDataBlocker, 0, 1) { + return } - th, err := chainhash.NewHashFromStr(ticket.Ticket) + defer atomic.StoreInt32(&w.ticketDataBlocker, 0) + + // Only get or set votebits for live tickets + liveTicketHashes, err := w.GetUnspentUserTickets(multisig) if err != nil { - log.Infof("NewHashFromStr failed for %v", ticket) - return "/error?r=/tickets", http.StatusSeeOther + return + } + + log.Infof("Started setting of vote bits for %d tickets.", + len(liveTicketHashes)) + + vbs := make([]stake.VoteBits, len(liveTicketHashes)) + for i := 0; i < len(liveTicketHashes); i++ { + vbs[i] = stake.VoteBits{Bits: voteBits} + //vbs[i].Bits = voteBits } - err = controller.rpcServers.SetTicketVoteBits(th, voteBits) + + err = controller.rpcServers.SetTicketsVoteBits(liveTicketHashes, vbs) if err != nil { if err == ErrSetVoteBitsCoolDown { - return "/error?r=/tickets&rl=1", http.StatusSeeOther + return } controller.handlePotentialFatalError("SetTicketVoteBits", err) - return "/error?r=/tickets", http.StatusSeeOther + return } - } - return "/tickets", http.StatusSeeOther + log.Infof("Completed setting of vote bits for %d tickets.", + len(liveTicketHashes)) + + return + }() + + // Like a timeout, give the sync some time to process, otherwise /tickets + // will show a message that it is still syncing. + time.Sleep(3 * time.Second) + + return outPath, status } // Logout the user. diff --git a/glide.yaml b/glide.yaml index c9f2f93d..ddd5e1bc 100644 --- a/glide.yaml +++ b/glide.yaml @@ -4,6 +4,7 @@ import: - package: github.com/btcsuite/go-flags - package: github.com/btcsuite/seelog - package: github.com/decred/dcrd + version: v0.4.0 subpackages: - addrmgr - blockchain @@ -15,6 +16,7 @@ import: - txscript - wire - package: github.com/decred/dcrrpcclient + version: f3c620d63cb02aec0c1152a72d3c8669b92a2fb5 - package: github.com/decred/dcrutil subpackages: - hdkeychain diff --git a/helpers/auth.go b/helpers/auth.go index aae43cf0..a9991389 100644 --- a/helpers/auth.go +++ b/helpers/auth.go @@ -130,6 +130,10 @@ func UserIDExists(dbMap *gorp.DbMap, userid int64) (*models.User, error) { return &user, err } +// Login looks up a user by email and validates the provided clear text password +// against the bcrypt hashed password stored in the DB. Returns the *User and an +// error. On failure *User is nil and error is non-nil. On success, error is +// nil. func Login(dbMap *gorp.DbMap, email string, password string) (*models.User, error) { var user models.User err := dbMap.SelectOne(&user, "SELECT * FROM Users WHERE Email = ?", email) diff --git a/models/user.go b/models/user.go index c4bfc06a..fd9bd986 100644 --- a/models/user.go +++ b/models/user.go @@ -95,6 +95,7 @@ func InsertEmailChange(dbMap *gorp.DbMap, emailChange *EmailChange) error { return dbMap.Insert(emailChange) } +// InsertUser inserts a user into the DB func InsertUser(dbMap *gorp.DbMap, user *User) error { return dbMap.Insert(user) } @@ -103,18 +104,24 @@ func InsertPasswordReset(dbMap *gorp.DbMap, passwordReset *PasswordReset) error return dbMap.Insert(passwordReset) } -func UpdateUserById(dbMap *gorp.DbMap, id int64, msa string, mss string, ppka string, upka string, ufa string, height int64) (user *User) { +// UpdateUserByID updates a user, specified by id, in the DB with a new +// multiSigAddr, multiSigScript, multiSigScript, pool pubkey address, +// user pub key address, and fee address. Unchanged are the user's ID, email, +// username and password. +func UpdateUserByID(dbMap *gorp.DbMap, id int64, multiSigAddr string, + multiSigScript string, poolPubKeyAddr string, userPubKeyAddr string, + userFeeAddr string, height int64) (user *User) { err := dbMap.SelectOne(&user, "SELECT * FROM Users WHERE UserId = ?", id) if err != nil { glog.Warningf("Can't get user by id: %v", err) } - user.MultiSigAddress = msa - user.MultiSigScript = mss - user.PoolPubKeyAddr = ppka - user.UserPubKeyAddr = upka - user.UserFeeAddr = ufa + user.MultiSigAddress = multiSigAddr + user.MultiSigScript = multiSigScript + user.PoolPubKeyAddr = poolPubKeyAddr + user.UserPubKeyAddr = userPubKeyAddr + user.UserFeeAddr = userFeeAddr user.HeightRegistered = height _, err = dbMap.Update(user) @@ -122,6 +129,8 @@ func UpdateUserById(dbMap *gorp.DbMap, id int64, msa string, mss string, ppka st if err != nil { glog.Warningf("Couldn't update user: %v", err) } + + // return updated User return } diff --git a/system/core.go b/system/core.go index d27aaeb2..4135ce93 100644 --- a/system/core.go +++ b/system/core.go @@ -108,11 +108,20 @@ func (application *Application) Route(controller interface{}, route string) inte } } + if respHeader, exists := c.Env["ResponseHeaderMap"]; exists { + if hdrMap, ok := respHeader.(map[string]string); ok { + for key, val := range hdrMap { + w.Header().Set(key, val) + } + } + } + switch code { - case http.StatusOK: + case http.StatusOK, http.StatusProcessing, http.StatusServiceUnavailable: if _, exists := c.Env["Content-Type"]; exists { w.Header().Set("Content-Type", c.Env["Content-Type"].(string)) } + w.WriteHeader(code) io.WriteString(w, body) case http.StatusSeeOther, http.StatusFound: http.Redirect(w, r, body, code) diff --git a/views/tickets.html b/views/tickets.html index 55b87fbc..3c1b45ed 100644 --- a/views/tickets.html +++ b/views/tickets.html @@ -2,6 +2,12 @@ {{if .Error}}
{{.Error}}

{{end}} +{{range .Flash}} +

{{.}}
+{{end}} +{{range .FlashWarn}} +
{{.}}
+{{end}}