Skip to content

feat: add order-take rate limit to prevent spam#828

Open
Matobi98 wants to merge 12 commits into
lnp2pBot:mainfrom
Matobi98:feat/order-take-rate-limit
Open

feat: add order-take rate limit to prevent spam#828
Matobi98 wants to merge 12 commits into
lnp2pBot:mainfrom
Matobi98:feat/order-take-rate-limit

Conversation

@Matobi98

@Matobi98 Matobi98 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Add a rate limit on order taking to prevent spam: users who exceed MAX_ORDERS_TAKE consecutive takes without completing an order are temporarily blocked until the cooldown expires or they complete a trade.

Summary

  • Add MAX_ORDERS_TAKE and ORDER_TAKE_COOLDOWN_HOURS env vars to control the rate limit
  • Track take_order_count and take_order_cooldown_until on the User model
  • Block users from taking new orders once the counter hits the limit, notifying them with the remaining cooldown time
  • Reset the counter and cancel the cooldown when the user completes any order (as maker or taker)

Changes

  • models/user.ts — new fields: take_order_count, take_order_cooldown_until
  • bot/modules/orders/takeOrder.ts — rate limit check in takebuy and takesell
  • util/index.ts — reset counter on order completion (SUCCESS)
  • bot/messages.ts — new orderTakeRateLimitMessage with remaining hours
  • locales/order_take_rate_limit key added in all 10 languages
  • .env-sample — documented new env vars with defaults (MAX_ORDERS_TAKE=10, ORDER_TAKE_COOLDOWN_HOURS=24)

Summary by CodeRabbit

  • New Features
    • Added per-user order-taking rate limiting with configurable maximum accepted orders and a cooldown after hitting the limit.
    • When the limit is reached, users receive localized Telegram notifications with the required wait time (or a prompt to complete an active order to reset).
  • Bug Fixes
    • Reputation updates now automatically reset take counters and cooldowns when no recent successful orders are found.
  • Chores
    • Added new environment variables for rate-limit configuration.
    • Added the new localized message across all supported languages.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds per-user order-taking rate limiting with persisted counters, configurable cooldowns, atomic reservation and rollback, localized rejection messages, and state reset during reputation processing.

Changes

Order-take rate limiting

Layer / File(s) Summary
User model and rate-limit configuration
models/user.ts, .env-sample
Adds persisted take-order counters and cooldown timestamps, plus environment variables for the maximum count and cooldown duration.
Atomic reservation and localized notification
bot/modules/orders/takeOrder.ts, bot/messages.ts, locales/*.yaml
takebuy and takesell reserve slots before saving, release reservations after save failures, and notify blocked users with localized cooldown messages.
Rate-limit state reset
util/index.ts
Clears both users’ counters and cooldown timestamps in the relevant reputation-processing branch.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • lnp2pBot/bot#824: Shares the mutex-controlled takebuy and takesell flow modified by this PR.
  • lnp2pBot/bot#853: Also changes pre-save gating in the order-taking flows.

Suggested reviewers: Luquitasjeffrey, grunch, mostronatorcoder, ermeme

Poem

🐰 I saved each order, one by one,
Then cooled my hops beneath the sun.
In many tongues the message sings,
While finished trades reset the springs.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding an order-take rate limit to prevent spam.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
bot/modules/orders/takeOrder.ts (2)

86-97: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Persisting the user counter before order save can over-penalize users.

At Line 86/147, incrementTakeOrderCount(user) is saved before Line 96/153 order.save(). If order persistence fails, the user still gets count/cooldown updates for a take that never completed. This should be one atomic unit (or at least update the counter only after order save succeeds).

Also applies to: 147-153, 194-195

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bot/modules/orders/takeOrder.ts` around lines 86 - 97, The
incrementTakeOrderCount(user) call is executed before persisting the order,
which will penalize the user if order.save() fails; move the
incrementTakeOrderCount(user) to after a successful await order.save() (or wrap
both operations in a DB transaction if supported) so the counter/cooldown is
only updated once order.status, order.seller_id, taken_at and order.random_image
have been persisted; adjust the same pattern wherever incrementTakeOrderCount
appears (lines referencing incrementTakeOrderCount, order.save,
generateRandomImage, and order.status) to ensure counter updates occur post-save
or inside the same atomic transaction.

62-63: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Per-order mutex does not protect per-user counter updates.

At Line 62 and Line 114, locking is keyed by orderId, so the same user can take different orders concurrently and race on take_order_count via Line 194 user.save(). Use an atomic DB update ($inc + conditional cooldown update) or add a per-user lock to avoid lost updates.

Also applies to: 114-115, 184-195

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bot/modules/orders/takeOrder.ts` around lines 62 - 63, The per-order mutex
(PerOrderIdMutex.instance.runExclusive keyed by orderId) does not prevent
concurrent updates to the same user's take_order_count because different orders
for the same user can run concurrently; replace or augment this by performing an
atomic database update (use a single update with $inc on take_order_count plus
conditional update of the cooldown fields via a conditional operator/aggregation
pipeline) or acquire a per-user lock (e.g., PerUserIdMutex.instance.runExclusive
keyed by user.id) around the critical section that reads/modifies user and calls
user.save(); locate uses of PerOrderIdMutex.instance.runExclusive, the
user.save() call and the take_order_count update and change them to either an
atomic DB operation or wrap the user mutation in a per-user mutex to prevent
lost updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bot/modules/orders/takeOrder.ts`:
- Around line 170-182: The cooldown check is leaving stale state in
user.take_order_count when user.take_order_cooldown_until is in the past; update
the logic in the function in takeOrder.ts that checks
user.take_order_cooldown_until to first detect an expired timestamp and
reset/clear user.take_order_count and user.take_order_cooldown_until before
proceeding, then persist/update the user as needed; continue to call
messages.orderTakeRateLimitMessage(ctx, user, user.take_order_cooldown_until)
only when the cooldown is still in the future. Also apply the same
expired-timestamp reset to the similar block referenced around the 188–193
region.
- Around line 185-186: The env parsing for rate-limit config (maxOrdersTake and
cooldownHours in takeOrder.ts) can yield NaN or non-positive values and disable
cooldown logic; after parsing parseInt(process.env.MAX_ORDERS_TAKE || '10') and
parseInt(process.env.ORDER_TAKE_COOLDOWN_HOURS || '24'), validate each parsed
value (maxOrdersTake and cooldownHours) with Number.isFinite/Number.isInteger
and ensure they are > 0, otherwise reset to safe defaults (e.g., 10 for
maxOrdersTake and 24 for cooldownHours) and optionally clamp to sensible bounds;
update any usages that rely on these variables (e.g., rate-limit checks like
count >= cooldownHours) to use the validated values.

---

Outside diff comments:
In `@bot/modules/orders/takeOrder.ts`:
- Around line 86-97: The incrementTakeOrderCount(user) call is executed before
persisting the order, which will penalize the user if order.save() fails; move
the incrementTakeOrderCount(user) to after a successful await order.save() (or
wrap both operations in a DB transaction if supported) so the counter/cooldown
is only updated once order.status, order.seller_id, taken_at and
order.random_image have been persisted; adjust the same pattern wherever
incrementTakeOrderCount appears (lines referencing incrementTakeOrderCount,
order.save, generateRandomImage, and order.status) to ensure counter updates
occur post-save or inside the same atomic transaction.
- Around line 62-63: The per-order mutex (PerOrderIdMutex.instance.runExclusive
keyed by orderId) does not prevent concurrent updates to the same user's
take_order_count because different orders for the same user can run
concurrently; replace or augment this by performing an atomic database update
(use a single update with $inc on take_order_count plus conditional update of
the cooldown fields via a conditional operator/aggregation pipeline) or acquire
a per-user lock (e.g., PerUserIdMutex.instance.runExclusive keyed by user.id)
around the critical section that reads/modifies user and calls user.save();
locate uses of PerOrderIdMutex.instance.runExclusive, the user.save() call and
the take_order_count update and change them to either an atomic DB operation or
wrap the user mutation in a per-user mutex to prevent lost updates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 870f3837-70a7-4381-a84d-d62b7257d716

📥 Commits

Reviewing files that changed from the base of the PR and between 2a2beea and bcb433d.

📒 Files selected for processing (15)
  • .env-sample
  • bot/messages.ts
  • bot/modules/orders/takeOrder.ts
  • locales/de.yaml
  • locales/en.yaml
  • locales/es.yaml
  • locales/fa.yaml
  • locales/fr.yaml
  • locales/it.yaml
  • locales/ko.yaml
  • locales/pt.yaml
  • locales/ru.yaml
  • locales/uk.yaml
  • models/user.ts
  • util/index.ts

Comment thread bot/modules/orders/takeOrder.ts Outdated
Comment thread bot/modules/orders/takeOrder.ts Outdated

@Luquitasjeffrey Luquitasjeffrey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please solve your conflicts with main to proceed with the review

…e-limit

# Conflicts:
#	bot/messages.ts
#	bot/modules/orders/takeOrder.ts
#	models/user.ts
#	package-lock.json

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Request changes

The stale cooldown/env parsing issues are fixed on the current head, but one blocker remains: the take counter is still updated from stale in-memory state, and it is written before the order itself is persisted. That leaves the spam cap bypassable under concurrent takes from the same user, and it can also penalize a user for a take that never completed because order.save() failed.

Comment thread bot/modules/orders/takeOrder.ts Outdated
Comment thread bot/modules/orders/takeOrder.ts Outdated

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Request changes

The save-before-increment issue is fixed on the current head, but one blocker remains: the take limit is still enforced only after a stale read. checkTakeRateLimit() reads the user document before the atomic $inc, and there is no per-user lock across different orders. Two or more concurrent takes by the same user can all pass the check and push the counter past MAX_ORDERS_TAKE, which defeats the anti-spam protection this PR is meant to add.

There is also still a crash window between incrementing take_order_count and writing take_order_cooldown_until, so a failed second write can leave the cap unenforced until the next successful take.

Please gate the counter with an atomic conditional update or a per-user mutex before any order side effects.

Comment thread bot/modules/orders/takeOrder.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bot/modules/orders/takeOrder.ts`:
- Around line 277-291: The updatedUser === null branch in the order-taking flow
can return without notifying the user when the follow-up User.findById read
lacks take_order_cooldown_until. Add a single retry of the cooldown reservation
or emit a fallback blocked message when current is missing or has no cooldown,
ensuring the branch always provides feedback before returning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 30cf41d9-f361-476a-8bfa-8c6b5fe30113

📥 Commits

Reviewing files that changed from the base of the PR and between 132de43 and dccf554.

📒 Files selected for processing (1)
  • bot/modules/orders/takeOrder.ts

Comment thread bot/modules/orders/takeOrder.ts Outdated

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the current head. The previous concurrency and save-order blockers are resolved, and I do not see any blocking issues remaining.

@Luquitasjeffrey Luquitasjeffrey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept and testing ACK.
Please improve the readibility of mongoose operations

Comment thread bot/modules/orders/takeOrder.ts Outdated
Comment thread bot/modules/orders/takeOrder.ts Outdated

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Request changes

The readability refactor is a good step, but one correctness blocker remains: the new take-limit state is still reset in util/index.ts without the same per-user serialization used in takeOrder.ts. A successful order completion can race with a concurrent take on the same user and overwrite a newer take_order_count / take_order_cooldown_until state. Please guard those resets with the same user-keyed mutex or make them atomic.

Comment thread util/index.ts Outdated

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Approve

I re-reviewed the current head. The previous blocker in util/index.ts is now serialized with the same per-user mutex used by the take flow, so the counter/cooldown reset can no longer race a concurrent take on the same user. I don't see any blocking issues remaining.

@Luquitasjeffrey Luquitasjeffrey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK

@grunch grunch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strict review

Verdict: request changes — no security/fund-loss bugs found, but one notable design issue plus a few adjustments.

Verified locally: tsc --noEmit clean, all 5 concurrency tests pass, 10 locales present with consistent ${hours}, and the message is sent as plain text (good — avoids the MarkdownV2 escaping pitfall).

🔴 HIGH

H1 — Honest takers get penalized for their counterparty's behavior.
take_order_count is only reset to 0 on SUCCESS (fully paid order, ln/pay_request.ts:167) or on cooldown expiry. It is not decremented when a taken order is later cancelled or expires through the normal flow — releaseTakeSlot only fires when order.save() itself throws (takeOrder.ts:88,124). Confirmed by grep: no other code path touches the counter.

Real-world impact in P2P: a buyer who takes 10 sell orders whose sellers never fund the hold invoice (a very common case) reaches MAX_ORDERS_TAKE and is locked out for 24h without doing anything wrong. The design does not distinguish "victim of flaky counterparties" from "spammer".

Suggestion: reset/decrement the counter when a taken order is cancelled or expires with no fault on the taker (in the cancellation/expiration jobs), or only count takes that never progressed.

🟡 MEDIUM

M2 — Incomplete test coverage. The concurrency tests for reserveTakeSlot/releaseTakeSlot are good, but missing:

  • reset-on-completion (handleReputationItems zeroing both counters),
  • a test asserting reserveTakeSlot is actually wired into takebuy/takesell (regression guard),
  • an assertion that the message is not sent on an allowed take.

M3 — Misleading comments. The reserveTakeSlot header and both call sites say "Atomically reserve … in a single conditional update" (takeOrder.ts:77,113,151). The implementation is not an atomic conditional update: it is a findById → save read-modify-write guarded by an in-memory mutex. This is leftover wording from the earlier approach (history shows refactor: use per-user mutex instead of atomic mongo pipelines). It misrepresents the concurrency guarantee — please correct it.

🟢 LOW

L1 — Multi-process deployment. PerUserIdMutex is an in-memory (async-mutex) lock; it only serializes within a single process. I verified the bot starts with bot.launch() (long polling), which is inherently single-process → correct today. But if it ever moves to webhooks with replicas, or this logic is moved into a separate job process, reserveTakeSlot's read-modify-write races and the cap can be bypassed. Worth documenting the single-process assumption next to the class.

L2 — releaseTakeSlot clears a legitimate cooldown. If 10 concurrent takes reserve (count=10, cooldown opened) and one save() fails, the release drops count to 9 and clears the cooldown even though 9 real takes remain. Slight under-count / lets the user escape the cooldown early. Self-consistent and low impact, but worth a comment.

L3 — Silent block. In reserveTakeSlot, when dbUser === null it returns true (blocks) without sending any message (takeOrder.ts:227). Unlikely (middleware creates the user), but it's a block with no user feedback.

L4 — i18n wording. The message says "complete an active order to reset the counter", but the reset actually requires a SUCCESS (paid) order, not merely an active one. Minor wording tweak across the 10 locales.

✅ Done well

  • reserveTakeSlot is called after all validations → a validation rejection never consumes a slot (the comment claims this and it's true).
  • releaseTakeSlot on a failed save() is a thoughtful touch.
  • Hardened env parsing (Number.isInteger/isFinite + fallback).
  • KeyedMutex cleans up the map at lockCount == 0 (no memory leak).
  • Plain-text message, no parse_mode.

Overall the concurrency core is solid and well-tested; the main thing blocking approval is H1 (fairness for honest takers), with M2/M3 as follow-ups.

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the current head strictly. The previous blocking concurrency and fairness issues are resolved, and I do not see any remaining blockers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants