github.com/decred/politeia@v1.4.0/politeiawww/legacy/paywall.go (about)

     1  // Copyright (c) 2017-2020 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package legacy
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"time"
    12  
    13  	"github.com/decred/politeia/politeiawww/legacy/user"
    14  	"github.com/decred/politeia/util"
    15  	"github.com/google/uuid"
    16  )
    17  
    18  type paywallPoolMember struct {
    19  	paywallType     string // Used to differentiate between user and proposal paywalls
    20  	address         string // Paywall address
    21  	amount          uint64 // Minimum tx amount required to satisfy paywall
    22  	txNotBefore     int64  // Minimum timestamp for paywall tx
    23  	pollExpiry      int64  // After this time, the paywall address will not be continuously polled
    24  	txID            string // ID of the pending payment tx
    25  	txAmount        uint64 // Amount of the pending payment tx
    26  	txConfirmations uint64 // Number of confirmations of the pending payment tx
    27  }
    28  
    29  const (
    30  	// paywallExpiryDuration is the amount of time the server will watch a paywall address
    31  	// for transactions. It gets reset when the user logs in or makes a call to
    32  	// RouteUserRegistrationPayment.
    33  	paywallExpiryDuration = time.Hour * 24
    34  
    35  	// paywallCheckGap is the amount of time the server sleeps after polling for
    36  	// a paywall address.
    37  	paywallCheckGap = time.Second * 1
    38  
    39  	// paywallTypeUser and paywallTypeProposal are used to signify whether a
    40  	// paywall pool member is a user registration fee paywall or a proposal
    41  	// credit paywall. Different actions are taken by the paywall pool depending
    42  	// on the paywall type.
    43  	paywallTypeUser     = "user"
    44  	paywallTypeProposal = "proposal"
    45  )
    46  
    47  func paywallHasExpired(pollExpiry int64) bool {
    48  	return time.Now().After(time.Unix(pollExpiry, 0))
    49  }
    50  
    51  // paywallIsEnabled returns true if paywall is enabled for the server, false
    52  // otherwise.
    53  func (p *Politeiawww) paywallIsEnabled() bool {
    54  	return p.cfg.PaywallAmount != 0 && p.cfg.PaywallXpub != ""
    55  }
    56  
    57  // initPaywallCheck is intended to be called
    58  func (p *Politeiawww) initPaywallChecker() error {
    59  	if p.cfg.PaywallAmount == 0 {
    60  		// Paywall not configured.
    61  		return nil
    62  	}
    63  
    64  	err := p.addUsersToPaywallPool()
    65  	if err != nil {
    66  		return err
    67  	}
    68  
    69  	// Start the thread that checks for payments.
    70  	go p.checkForPayments()
    71  	return nil
    72  }
    73  
    74  // checkForProposalPayments checks if any of the proposal paywalls in the
    75  // paywall pool have received a payment.  If so, proposal credits are created
    76  // for the user, the user database is updated, and the user is removed from
    77  // the paywall pool.
    78  func (p *Politeiawww) checkForProposalPayments(ctx context.Context, pool map[uuid.UUID]paywallPoolMember) (bool, []uuid.UUID) {
    79  	var userIDsToRemove []uuid.UUID
    80  
    81  	// In theory poolMember could be raced during this call. In practice
    82  	// a race will not occur as long as the paywall does not remove
    83  	// poolMembers from the pool while in the middle of polling poolMember
    84  	// addresses.
    85  	for userID, poolMember := range pool {
    86  		u, err := p.db.UserGetById(userID)
    87  		if err != nil {
    88  			if errors.Is(err, user.ErrShutdown) {
    89  				// The database is shutdown, so stop the thread.
    90  				return false, nil
    91  			}
    92  
    93  			log.Errorf("cannot fetch user by id %v: %v\n", userID, err)
    94  			continue
    95  		}
    96  
    97  		if poolMember.paywallType != paywallTypeProposal {
    98  			continue
    99  		}
   100  
   101  		log.Tracef("Checking proposal paywall address for user %v...", u.Email)
   102  
   103  		paywall := p.mostRecentProposalPaywall(u)
   104  
   105  		// Sanity check
   106  		if paywall == nil {
   107  			continue
   108  		}
   109  
   110  		if paywallHasExpired(paywall.PollExpiry) {
   111  			userIDsToRemove = append(userIDsToRemove, userID)
   112  			log.Tracef("  removing from polling, poll has expired")
   113  			continue
   114  		}
   115  
   116  		tx, err := p.verifyProposalPayment(ctx, u)
   117  		if err != nil {
   118  			if errors.Is(err, user.ErrShutdown) {
   119  				// The database is shutdown, so stop the thread.
   120  				return false, nil
   121  			}
   122  
   123  			log.Errorf("cannot update user with id %v: %v", u.ID, err)
   124  			continue
   125  		}
   126  
   127  		// Removed paywall from the in-memory pool if it has
   128  		// been marked as paid.
   129  		if !p.userHasValidProposalPaywall(u) {
   130  			userIDsToRemove = append(userIDsToRemove, userID)
   131  			log.Tracef("  removing from polling, user just paid")
   132  		} else if tx != nil {
   133  			log.Tracef("  updating pool member with id: %v", userID)
   134  
   135  			// Update pool member if payment tx was found but
   136  			// does not have enough confimrations.
   137  			poolMember.txID = tx.TxID
   138  			poolMember.txAmount = tx.Amount
   139  			poolMember.txConfirmations = tx.Confirmations
   140  
   141  			p.Lock()
   142  			p.userPaywallPool[userID] = poolMember
   143  			p.Unlock()
   144  		}
   145  
   146  		time.Sleep(paywallCheckGap)
   147  	}
   148  
   149  	return true, userIDsToRemove
   150  }
   151  
   152  func (p *Politeiawww) checkForPayments() {
   153  	ctx := context.Background()
   154  	for {
   155  		// Removing pool members from the pool while in the middle of
   156  		// polling can cause a race to occur in checkForProposalPayments.
   157  		userPaywallsToCheck := p.createUserPaywallPoolCopy()
   158  
   159  		// Check new user payments.
   160  		shouldContinue, userIDsToRemove := p.checkForUserPayments(ctx, userPaywallsToCheck)
   161  		if !shouldContinue {
   162  			return
   163  		}
   164  		p.removeUsersFromPool(userIDsToRemove, paywallTypeUser)
   165  
   166  		// Check proposal payments.
   167  		shouldContinue, userIDsToRemove = p.checkForProposalPayments(ctx, userPaywallsToCheck)
   168  		if !shouldContinue {
   169  			return
   170  		}
   171  		p.removeUsersFromPool(userIDsToRemove, paywallTypeProposal)
   172  
   173  		time.Sleep(paywallCheckGap)
   174  	}
   175  }
   176  
   177  // generateNewUserPaywall generates new paywall info, if necessary, and saves
   178  // it in the database.
   179  func (p *Politeiawww) generateNewUserPaywall(u *user.User) error {
   180  	// Check that the paywall is enabled.
   181  	if !p.paywallIsEnabled() {
   182  		return nil
   183  	}
   184  
   185  	// Check that the user either hasn't had paywall information set yet,
   186  	// or it has expired.
   187  	if u.NewUserPaywallAddress != "" &&
   188  		!paywallHasExpired(u.NewUserPaywallPollExpiry) {
   189  		return nil
   190  	}
   191  
   192  	if u.NewUserPaywallAddress == "" {
   193  		address, amount, txNotBefore, err := p.derivePaywallInfo(u)
   194  		if err != nil {
   195  			return err
   196  		}
   197  
   198  		u.NewUserPaywallAddress = address
   199  		u.NewUserPaywallAmount = amount
   200  		u.NewUserPaywallTxNotBefore = txNotBefore
   201  	}
   202  	u.NewUserPaywallPollExpiry = time.Now().Add(paywallExpiryDuration).Unix()
   203  
   204  	err := p.db.UserUpdate(*u)
   205  	if err != nil {
   206  		return err
   207  	}
   208  
   209  	p.addUserToPaywallPoolLock(u, paywallTypeUser)
   210  	return nil
   211  }
   212  
   213  // mostRecentProposalPaywall returns the most recent paywall that has been
   214  // issued to the user.  Just because a paywall is the most recent paywall does
   215  // not guarantee that it is still valid.  Depending on the circumstances, the
   216  // paywall could have already been paid, could have already expired, or could
   217  // still be valid.
   218  func (p *Politeiawww) mostRecentProposalPaywall(user *user.User) *user.ProposalPaywall {
   219  	if len(user.ProposalPaywalls) > 0 {
   220  		return &user.ProposalPaywalls[len(user.ProposalPaywalls)-1]
   221  	}
   222  	return nil
   223  }
   224  
   225  // userHasValidProposalPaywall checks if the user has been issued a paywall
   226  // that has not been paid yet and that has not expired yet.  Only one paywall
   227  // per user can be valid at a time, so if a valid paywall exists for the user,
   228  // it will be the most recent paywall.
   229  func (p *Politeiawww) userHasValidProposalPaywall(user *user.User) bool {
   230  	pp := p.mostRecentProposalPaywall(user)
   231  	return pp != nil && pp.TxID == "" && !paywallHasExpired(pp.PollExpiry)
   232  }
   233  
   234  // generateProposalPaywall creates a new proposal paywall for the user that
   235  // enables them to purchase proposal credits.  Once the paywall is created, the
   236  // user database is updated and the user is added to the paywall pool.
   237  func (p *Politeiawww) generateProposalPaywall(u *user.User) (*user.ProposalPaywall, error) {
   238  	address, amount, txNotBefore, err := p.derivePaywallInfo(u)
   239  	if err != nil {
   240  		return nil, err
   241  	}
   242  	pp := user.ProposalPaywall{
   243  		ID:          uint64(len(u.ProposalPaywalls) + 1),
   244  		CreditPrice: amount,
   245  		Address:     address,
   246  		TxNotBefore: txNotBefore,
   247  		PollExpiry:  time.Now().Add(paywallExpiryDuration).Unix(),
   248  	}
   249  	u.ProposalPaywalls = append(u.ProposalPaywalls, pp)
   250  
   251  	err = p.db.UserUpdate(*u)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	p.addUserToPaywallPoolLock(u, paywallTypeProposal)
   257  	return &pp, nil
   258  }
   259  
   260  // verifyPropoposalPayment checks whether a payment has been sent to the
   261  // user's proposal paywall address. Proposal credits are created and added to
   262  // the user's account if the payment meets the minimum requirements.
   263  func (p *Politeiawww) verifyProposalPayment(ctx context.Context, u *user.User) (*TxDetails, error) {
   264  	paywall := p.mostRecentProposalPaywall(u)
   265  
   266  	// If a TxID exists, the payment has already been verified.
   267  	if paywall.TxID != "" {
   268  		return nil, nil
   269  	}
   270  
   271  	// Fetch txs sent to paywall address
   272  	txs, err := fetchTxsForAddress(ctx, p.params, paywall.Address,
   273  		p.dcrdataHostHTTP())
   274  	if err != nil {
   275  		return nil, fmt.Errorf("FetchTxsForAddress %v: %v",
   276  			paywall.Address, err)
   277  	}
   278  
   279  	// Check for paywall payment tx
   280  	for _, tx := range txs {
   281  		switch {
   282  		case tx.Timestamp < paywall.TxNotBefore && tx.Timestamp != 0:
   283  			continue
   284  		case tx.Amount < paywall.CreditPrice:
   285  			continue
   286  		case tx.Confirmations < p.cfg.MinConfirmationsRequired:
   287  			// Payment tx found but not enough confirmations. Return
   288  			// the tx so that the paywall member can be updated.
   289  			return &tx, nil
   290  		default:
   291  			// Payment tx found that meets all criteria. Create
   292  			// proposal credits and update user db record.
   293  			paywall.TxID = tx.TxID
   294  			paywall.TxAmount = tx.Amount
   295  			paywall.NumCredits = tx.Amount / paywall.CreditPrice
   296  
   297  			// Create proposal credits
   298  			c := make([]user.ProposalCredit, paywall.NumCredits)
   299  			timestamp := time.Now().Unix()
   300  			for i := uint64(0); i < paywall.NumCredits; i++ {
   301  				c[i] = user.ProposalCredit{
   302  					PaywallID:     paywall.ID,
   303  					Price:         paywall.CreditPrice,
   304  					DatePurchased: timestamp,
   305  					TxID:          paywall.TxID,
   306  				}
   307  			}
   308  			u.UnspentProposalCredits = append(u.UnspentProposalCredits, c...)
   309  
   310  			// Update user database.
   311  			err = p.db.UserUpdate(*u)
   312  			if err != nil {
   313  				return nil, fmt.Errorf("database UserUpdate: %v", err)
   314  			}
   315  
   316  			return &tx, nil
   317  		}
   318  	}
   319  
   320  	return nil, nil
   321  }
   322  
   323  // removeUsersFromPool removes the provided user IDs from the the poll pool.
   324  //
   325  // Currently, updating the user db and removing the user from pool isn't an
   326  // atomic operation.  This can lead to a scenario where the user has been
   327  // marked as paid in the db, but has not yet been removed from the pool. If a
   328  // user issues a proposal paywall during this time, the proposal paywall will
   329  // replace the user paywall in the pool. When the pool proceeds to remove the
   330  // user paywall, it will mistakenly remove the proposal paywall instead.
   331  // Proposal credits will not be added to the user's account. The workaround
   332  // until this code gets replaced with websockets is to pass in the paywallType
   333  // when removing a pool member.
   334  //
   335  // This function must be called WITHOUT the mutex held.
   336  func (p *Politeiawww) removeUsersFromPool(userIDsToRemove []uuid.UUID, paywallType string) {
   337  	p.Lock()
   338  	defer p.Unlock()
   339  
   340  	for _, userID := range userIDsToRemove {
   341  		if p.userPaywallPool[userID].paywallType == paywallType {
   342  			delete(p.userPaywallPool, userID)
   343  		}
   344  	}
   345  }
   346  
   347  // addUserToPaywallPool adds a database user to the paywall pool.
   348  //
   349  // This function must be called WITH the mutex held.
   350  func (p *Politeiawww) addUserToPaywallPool(u *user.User, paywallType string) {
   351  	p.userPaywallPool[u.ID] = paywallPoolMember{
   352  		paywallType: paywallType,
   353  		address:     u.NewUserPaywallAddress,
   354  		amount:      u.NewUserPaywallAmount,
   355  		txNotBefore: u.NewUserPaywallTxNotBefore,
   356  		pollExpiry:  u.NewUserPaywallPollExpiry,
   357  	}
   358  }
   359  
   360  // addUserToPaywallPoolLock adds a user and its paywall info to the in-memory pool.
   361  //
   362  // This function must be called WITHOUT the mutex held.
   363  func (p *Politeiawww) addUserToPaywallPoolLock(u *user.User, paywallType string) {
   364  	if !p.paywallIsEnabled() {
   365  		return
   366  	}
   367  
   368  	p.Lock()
   369  	defer p.Unlock()
   370  
   371  	p.addUserToPaywallPool(u, paywallType)
   372  }
   373  
   374  // addUsersToPaywallPool adds a user and its paywall info to the in-memory pool.
   375  //
   376  // This function must be called WITHOUT the mutex held.
   377  func (p *Politeiawww) addUsersToPaywallPool() error {
   378  	p.Lock()
   379  	defer p.Unlock()
   380  
   381  	// Create the in-memory pool of all users who need to pay the paywall.
   382  	err := p.db.AllUsers(func(u *user.User) {
   383  		// Proposal paywalls
   384  		if p.userHasValidProposalPaywall(u) {
   385  			p.addUserToPaywallPool(u, paywallTypeProposal)
   386  			return
   387  		}
   388  
   389  		// User paywalls
   390  		if p.userHasPaid(*u) {
   391  			return
   392  		}
   393  		if u.NewUserVerificationToken != nil {
   394  			return
   395  		}
   396  		if paywallHasExpired(u.NewUserPaywallPollExpiry) {
   397  			return
   398  		}
   399  
   400  		p.addUserToPaywallPool(u, paywallTypeUser)
   401  	})
   402  	if err != nil {
   403  		return err
   404  	}
   405  
   406  	log.Tracef("Adding %v users to paywall pool", len(p.userPaywallPool))
   407  	return nil
   408  }
   409  
   410  // updateUserAsPaid records in the database that the user has paid.
   411  func (p *Politeiawww) updateUserAsPaid(u *user.User, tx string) error {
   412  	u.NewUserPaywallTx = tx
   413  	u.NewUserPaywallPollExpiry = 0
   414  	return p.db.UserUpdate(*u)
   415  }
   416  
   417  // derivePaywallInfo derives a new paywall address for the user.
   418  func (p *Politeiawww) derivePaywallInfo(u *user.User) (string, uint64, int64, error) {
   419  	address, err := util.DeriveChildAddress(p.params,
   420  		p.cfg.PaywallXpub, uint32(u.PaywallAddressIndex))
   421  	if err != nil {
   422  		err = fmt.Errorf("Unable to derive paywall address #%v "+
   423  			"for %v: %v", u.ID.ID(), u.Email, err)
   424  	}
   425  
   426  	return address, p.cfg.PaywallAmount, time.Now().Unix(), err
   427  }
   428  
   429  // createUserPaywallPoolCopy returns a map of the poll pool.
   430  //
   431  // This function must be called WITHOUT the mutex held.
   432  func (p *Politeiawww) createUserPaywallPoolCopy() map[uuid.UUID]paywallPoolMember {
   433  	p.RLock()
   434  	defer p.RUnlock()
   435  
   436  	poolCopy := make(map[uuid.UUID]paywallPoolMember, len(p.userPaywallPool))
   437  
   438  	for k, v := range p.userPaywallPool {
   439  		poolCopy[k] = v
   440  	}
   441  
   442  	return poolCopy
   443  }
   444  
   445  // checkForUserPayments is called periodically to see if payments have come
   446  // through.
   447  func (p *Politeiawww) checkForUserPayments(ctx context.Context, pool map[uuid.UUID]paywallPoolMember) (bool, []uuid.UUID) {
   448  	var userIDsToRemove []uuid.UUID
   449  
   450  	for userID, poolMember := range pool {
   451  		u, err := p.db.UserGetById(userID)
   452  		if err != nil {
   453  			if errors.Is(err, user.ErrShutdown) {
   454  				// The database is shutdown, so stop the
   455  				// thread.
   456  				return false, nil
   457  			}
   458  
   459  			log.Errorf("cannot fetch user by id %v: %v\n",
   460  				userID, err)
   461  			continue
   462  		}
   463  
   464  		if poolMember.paywallType != paywallTypeUser {
   465  			continue
   466  		}
   467  
   468  		log.Tracef("Checking the user paywall address for user %v...",
   469  			u.Email)
   470  
   471  		if p.userHasPaid(*u) {
   472  			// The user could have been marked as paid by
   473  			// RouteUserRegistrationPayment, so just remove him from the
   474  			// in-memory pool.
   475  			userIDsToRemove = append(userIDsToRemove, userID)
   476  			log.Tracef("  removing from polling, user already paid")
   477  			continue
   478  		}
   479  
   480  		if paywallHasExpired(u.NewUserPaywallPollExpiry) {
   481  			userIDsToRemove = append(userIDsToRemove, userID)
   482  			log.Tracef("  removing from polling, poll has expired")
   483  			continue
   484  		}
   485  
   486  		tx, _, err := fetchTxWithBlockExplorers(ctx, p.params, poolMember.address,
   487  			poolMember.amount, poolMember.txNotBefore,
   488  			p.cfg.MinConfirmationsRequired, p.dcrdataHostHTTP())
   489  		if err != nil {
   490  			log.Errorf("cannot fetch tx: %v\n", err)
   491  			continue
   492  		}
   493  
   494  		if tx != "" {
   495  			// Update the user in the database.
   496  			err = p.updateUserAsPaid(u, tx)
   497  			if err != nil {
   498  				if errors.Is(err, user.ErrShutdown) {
   499  					// The database is shutdown, so stop
   500  					// the thread.
   501  					return false, nil
   502  				}
   503  
   504  				log.Errorf("cannot update user with id %v: %v",
   505  					u.ID, err)
   506  				continue
   507  			}
   508  
   509  			// Remove this user from the in-memory pool.
   510  			userIDsToRemove = append(userIDsToRemove, userID)
   511  			log.Tracef("  removing from polling, user just paid")
   512  		}
   513  
   514  		time.Sleep(paywallCheckGap)
   515  	}
   516  
   517  	return true, userIDsToRemove
   518  }
   519  
   520  // userHasPaid returns whether the user has paid the user registration paywall.
   521  func (p *Politeiawww) userHasPaid(u user.User) bool {
   522  	if !p.paywallIsEnabled() {
   523  		return true
   524  	}
   525  	return u.NewUserPaywallTx != ""
   526  }