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 }