feat: exponential backoff polling for monitored deposit addresses#157
feat: exponential backoff polling for monitored deposit addresses#157
Conversation
There was a problem hiding this comment.
Pull request overview
Adds durable, upgrade-safe polling of monitored deposit addresses using a stable-memory schedule with exponential backoff, wiring it into the canister timer loop and extending RPC/event plumbing to support address polling.
Changes:
- Introduces a
StableBTreeMap-backed poll schedule (StorableAccount -> PollEntry) and exposes access/reset helpers. - Adds
getSignaturesForAddressRPC helper + tests, and integrates a new periodic timer task to poll due accounts with exponential backoff. - Extends audit/event/state plumbing with
StoppedMonitoringAccount, plus unit + integration test coverage.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| minter/src/storage/mod.rs | Adds stable poll schedule storage types and accessors. |
| minter/src/state/mod.rs | Removes monitored account on stop; adds PollMonitoredAddresses task type. |
| minter/src/state/event.rs | Introduces StoppedMonitoringAccount event variant. |
| minter/src/state/audit.rs | Applies StoppedMonitoringAccount during audit replay/state transitions. |
| minter/src/rpc/mod.rs | Adds get_signatures_for_address RPC wrapper + error type. |
| minter/src/rpc/tests.rs | Adds unit tests for get_signatures_for_address. |
| minter/src/main.rs | Wires periodic timer to poll_monitored_addresses; maps new event type. |
| minter/src/deposit/automatic/mod.rs | Implements scheduling + polling logic with exponential backoff. |
| minter/src/deposit/automatic/tests.rs | Adds unit tests for scheduling/backoff/max-poll behavior and storable round-trips. |
| libs/types-internal/src/event.rs | Adds StoppedMonitoringAccount to Candid event types; derives PartialEq for comparisons. |
| integration_tests/src/fixtures.rs | Adds update_balance args helper; exposes NUM_RPC_PROVIDERS; adds mock for getSignaturesForAddress. |
| integration_tests/tests/tests.rs | Adds automated deposit polling integration test. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
ef334a9 to
d697e96
Compare
## Summary Part of [DEFI-2780](https://dfinity.atlassian.net/browse/DEFI-2780). Adds a `poll_monitored_addresses` timer that calls `getSignaturesForAddress` for each account in `monitored_accounts`. After polling, each account is removed from monitoring via a `StoppedMonitoringAccount` event. Discovered signatures are currently ignored — upcoming PRs will process them. ### Design The timer follows the same pattern as other timers (`finalize_transactions`, `consolidate_deposits`): it processes up to `MAX_CONCURRENT_RPC_CALLS` accounts per invocation in parallel and reschedules immediately via scopeguard if there is more work remaining. Each account is polled with `getSignaturesForAddress` and then removed from monitoring with a `StoppedMonitoringAccount` event regardless of success or failure. The signature limit per call matches `MAX_TRANSACTIONS_PER_ACCOUNT` (10). A follow-up PR (#157) replaces this simple poll-once-and-remove approach with an exponential backoff schedule (1 min, 2, 4, …, 512 min, up to 10 polls total) backed by a stable-memory `StableBTreeMap` that survives canister upgrades. ### Constants | Constant | Value | Purpose | |---|---|---| | `POLL_MONITORED_ADDRESSES_DELAY` | 1 min | Timer interval | | `MAX_TRANSACTIONS_PER_ACCOUNT` | 10 | Max signatures to fetch per account per poll | 🤖 Generated with [Claude Code](https://claude.com/claude-code) [DEFI-2780]: https://dfinity.atlassian.net/browse/DEFI-2780?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
f248ea0 to
2b694f6
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
744a859 to
480ad33
Compare
6741032 to
41bc2a4
Compare
41bc2a4 to
f5870ac
Compare
f5870ac to
efcc773
Compare
efcc773 to
32375b8
Compare
ad8cb1c to
a140a23
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Add StableSortKeyMap-backed AutomaticDepositCache (stable memory, survives upgrades) - Implement exponential backoff: 1, 2, 4, …, 512 min (10 polls = 1023 min max) - Stop monitoring after MAX_GET_SIGNATURES_FOR_ADDRESS_CALLS (10); emit StoppedMonitoringAccount - Retain stopped entries at next_poll_at=u64::MAX so they are never rescheduled - Rename AutomaticDepositCache entry struct → AutomaticDepositCacheEntry - Add proptest storable round-trip for AutomaticDepositCacheEntry - Consolidate lifecycle tests into should_follow_full_polling_lifecycle (all 10 polls) - Move arb_cache_entry to test_fixtures::arb Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
a140a23 to
dd314a6
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…iter_by_index_up_to - Add public Iter<K, I, V> struct backed by ic_stable_structures::btreemap::Iter, yielding (index, key, value) triples in ascending index order - iter() returns Iter<K, I, V> — a concrete, nameable type (not impl Iterator) - Remove iter_by_index_up_to: callers use iter().take_while(|(i,..)| *i <= max) with standard iterator adapters instead - Remove peek: poll_monitored_addresses uses due.is_empty() as its early-return guard - Remove private iter_raw helper (logic is now in Iter::next directly) - Fix test name: should_store_value_in_by_key → should_store_value_by_key - Update should_index_entry and should_update_index_when_index_changes in mod insert to not depend on peek (use iter and get_with_index respectively) - Remove mod peek tests (method deleted) - Consolidate iter tests into mod iter: basic iteration tests plus filtered-range tests (formerly in mod iter_by_index_up_to) now use iter() + take_while + map Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ial backoff - Replace get_signatures_calls: u8 with rpc_quota_left: u64 (starts at INITIAL_RPC_QUOTA=50) and next_backoff_delay_mins: u64 (starts at 1, doubles on each poll) - Remove MAX_GET_SIGNATURES_FOR_ADDRESS_CALLS and MAX_GET_TRANSACTION_CALLS constants - update_balance: return MonitoringQuotaExhausted when quota is 0; preserve remaining quota when rescheduling a previously stopped account; reset backoff delay to 1 - poll_account: decrement rpc_quota_left on each call; stop when it reaches 0; use saturating arithmetic to avoid overflow on long-running accounts - Add UpdateBalanceError::MonitoringQuotaExhausted to types and DID - Add tests: should_schedule_unmonitored_account, should_not_modify_existing_cache_entry, should_reschedule_account_with_remaining_quota, should_not_start_monitoring_with_exhausted_quota - Consolidate lifecycle test into should_follow_full_polling_lifecycle with quota=3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| /// The monitoring lifecycle state of an account, as derived from the cache. | ||
| pub enum AccountMonitoringState { | ||
| /// No monitoring information has been recorded for this account. | ||
| Unknown, |
There was a problem hiding this comment.
In what scenario is the state Unknown? If an account has never been seen, there simply shouldn't be any record, right?
There was a problem hiding this comment.
I see, it is used in the monitoring_state function. An alternative would be to return Option<AccountMonitoringState>, right?
| entry: AutomaticDepositCacheEntry, | ||
| }, | ||
| /// Polling was stopped after the quota was exhausted, but a subsequent deposit | ||
| /// reset the quota. The account can be rescheduled via `update_balance`. |
There was a problem hiding this comment.
Nit: A deposit may not (necessarily) "reset" the quota as it is prorated. In particular, the quota is not fully replenished if a) it has been fully exhausted and b) the process_deposit call resulted in a mint, in which case cycles are charged for the subsequent consolidation.
This is explained in more detail in the design document in case you haven't seen this part yet.
| /// The RPC quota for this account has been exhausted and no deposit has reset it. | ||
| /// `update_balance` will return `MonitoringQuotaExhausted` until a deposit resets | ||
| /// the quota. | ||
| Exhausted { |
There was a problem hiding this comment.
Doesn't Exhausted mean that there is no quota left? In this case, there is no need to store an AutomaticDepositCacheEntry object, right?
Summary
StableSortKeyMap-backedAutomaticDepositCache(two stable-memory maps keyed by account and poll time) that survives canister upgradesMAX_GET_SIGNATURES_FOR_ADDRESS_CALLS(10) polls with no deposit found, emitsStoppedMonitoringAccountand retains the entry withnext_poll_at = u64::MAXso it is never rescheduledgetSignaturesForAddresscalls quota is still consumed when an RPC call fails🤖 Generated with Claude Code