Problem (spike / open-ended)
execute_subscription (stellar_send/src/subscription.rs lines 125-167) advances next_execution_time by exactly one interval_seconds per call (line 150-153: sub.next_execution_time.checked_add(sub.interval_seconds)), regardless of how far now actually is past the previous next_execution_time. The module doc comment explicitly frames this as deliberate: "so a late keeper call doesn't silently drift the cadence forward." But the flip side is: if a keeper has been offline/unavailable for a long stretch (or a subscription simply wasn't executed for many intervals for any reason), the schedule is now arbitrarily "behind," and because execute_subscription has no restriction on how many times it can be called within a short real-world time span (only that now >= next_execution_time for that call), a keeper — or anyone, since execution is permissionless — can call it back-to-back-to-back in rapid succession (limited only by however many intervals have been missed) to "catch up," charging the payer many periods' worth of payments in a single burst.
Why this is worth investigating
This is a genuine design tension, not a simple bug: the "advance by one interval, not to now + interval" choice correctly prevents cadence drift (a real problem this design intentionally solves), but as a side effect it permits an unbounded catch-up burst if a subscription goes unexecuted for a while, which could be surprising to a payer who expected a steady drip and instead sees N payments land within the same block/short time window once a keeper resumes. Whether that's acceptable depends on product expectations (a "financial subscription" user likely wants steady cadence; a background token-purchase DCA-style user might not mind catch-up bursts as harmless).
Open questions
- Should there be a
max_catch_up_executions (or simply "at most one execution per call, but no other explicit cap on how many times per real-world hour the function can be called by different/same callers") — the current code already limits to one execution per call, but nothing stops a keeper from calling it 50 times in the same transaction batch or same block to catch up 50 missed intervals instantly.
- Should catch-up be disallowed entirely (a missed interval is simply skipped,
next_execution_time reset to now + interval_seconds on the next successful call) — trading "payer sometimes get skipped payments" for "payer never gets surprise multi-period bursts"?
- Interacts with the
max_executions/expiry open question in the related issue in this batch — a bounded subscription makes an unbounded catch-up burst less concerning (there's a hard ceiling on total possible damage) than an unbounded one.
Investigation steps
Model both semantics (strict catch-up vs. skip-ahead) against a few realistic keeper-downtime scenarios and evaluate which better matches the payer's reasonable expectations set at create_subscription time; consider whether the choice should even be a per-subscription flag (catch_up: bool) rather than a single global policy.
Testing strategy
Whichever direction is chosen: test_execute_subscription_rapid_catch_up_multiple_calls documenting exactly how many rapid calls succeed given a known backlog, and (if a cap or skip-ahead policy is adopted) a test confirming the new bound is enforced.
Problem (spike / open-ended)
execute_subscription(stellar_send/src/subscription.rslines 125-167) advancesnext_execution_timeby exactly oneinterval_secondsper call (line 150-153:sub.next_execution_time.checked_add(sub.interval_seconds)), regardless of how farnowactually is past the previousnext_execution_time. The module doc comment explicitly frames this as deliberate: "so a late keeper call doesn't silently drift the cadence forward." But the flip side is: if a keeper has been offline/unavailable for a long stretch (or a subscription simply wasn't executed for many intervals for any reason), the schedule is now arbitrarily "behind," and becauseexecute_subscriptionhas no restriction on how many times it can be called within a short real-world time span (only thatnow >= next_execution_timefor that call), a keeper — or anyone, since execution is permissionless — can call it back-to-back-to-back in rapid succession (limited only by however many intervals have been missed) to "catch up," charging the payer many periods' worth of payments in a single burst.Why this is worth investigating
This is a genuine design tension, not a simple bug: the "advance by one interval, not to now + interval" choice correctly prevents cadence drift (a real problem this design intentionally solves), but as a side effect it permits an unbounded catch-up burst if a subscription goes unexecuted for a while, which could be surprising to a payer who expected a steady drip and instead sees N payments land within the same block/short time window once a keeper resumes. Whether that's acceptable depends on product expectations (a "financial subscription" user likely wants steady cadence; a background token-purchase DCA-style user might not mind catch-up bursts as harmless).
Open questions
max_catch_up_executions(or simply "at most one execution per call, but no other explicit cap on how many times per real-world hour the function can be called by different/same callers") — the current code already limits to one execution per call, but nothing stops a keeper from calling it 50 times in the same transaction batch or same block to catch up 50 missed intervals instantly.next_execution_timereset tonow + interval_secondson the next successful call) — trading "payer sometimes get skipped payments" for "payer never gets surprise multi-period bursts"?max_executions/expiryopen question in the related issue in this batch — a bounded subscription makes an unbounded catch-up burst less concerning (there's a hard ceiling on total possible damage) than an unbounded one.Investigation steps
Model both semantics (strict catch-up vs. skip-ahead) against a few realistic keeper-downtime scenarios and evaluate which better matches the payer's reasonable expectations set at
create_subscriptiontime; consider whether the choice should even be a per-subscription flag (catch_up: bool) rather than a single global policy.Testing strategy
Whichever direction is chosen:
test_execute_subscription_rapid_catch_up_multiple_callsdocumenting exactly how many rapid calls succeed given a known backlog, and (if a cap or skip-ahead policy is adopted) a test confirming the new bound is enforced.