Repository navigation
feat(minter): snapshot the watchlist before every upgrade - #285
gregorydemay wants to merge 130 commits into
Conversation
Replace the outdated section 3.1.1, which described per-transaction crediting with getSignaturesForAddress and quotas replenished by the removed process_deposit endpoint, with a design that builds on the balance sweep of deposit_sol: a free registration endpoint, a scan timer reading watched deposit addresses in batches with getMultipleAccounts, a schedule adapted from the ckETH minter, a fixed fee in SOL deducted from the minted amount, and a watchlist persisted only as a bounded snapshot at pre_upgrade. Every change to the interface, the arguments and the event log is additive, since the minter is deployed on mainnet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
State that a re-armed account is reported as watching again, that the main account receives the sweepable amount minus the fee share, correct the worst-case cost arithmetic and state its success assumption, and distinguish a failed upgrade from a reinstallation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ring Move the sequence diagram of the automatic flow ahead of its explanation, remove the semicolon that broke the mermaid parser on GitHub, and define the deposit id where it is first used in the section. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he constraint State that scan ticks are guarded like the other timers so that they never overlap, and base the automatic deposit fee constraint on the single-signature fee share that a pending mint actually bears. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Drop the separate status query: calling the registration endpoint again returns the stored registration or the id of the deposit in flight, as deposit_eth does in the ckETH minter. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… signature lookup Spell out that scan ticks never overlap, that the fee derivation is a single-attempt estimate with re-arm costs absorbed by the minter, and that only manual deposits are retried by the caller. Remove getSignaturesForAddress from the RPC inventory and cost tables, since the minter no longer uses it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A re-armed entry restarts its schedule from the first gap, bounded by the remaining window, and a re-arm only ever follows a detection. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Record the watchlist snapshot on every upgrade so that an older one cannot be replayed after the watchlist emptied, and let the transaction submission section account for deposits queued by the balance scan. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The window ends with the last scan, and the next registration call starts a new window rather than reporting a registration without a next scan. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…it ids Follow the ckETH minter: a registration freezes neither the minimum deposit amount nor the automatic deposit fee, both are quoted and the values in force at detection apply, the fee being recorded with the queued deposit. deposit_sol compares the latest deposit id of the account before and after its balance read, and a registration is allowed while a deposit_sol call is in progress. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…edule Measure gaps from the previous scan as before, but end the window at a fixed 24 hours after registration, as the ckETH minter does, so that the accumulated tick and call delays have slack and clients and the re-arm share one unambiguous boundary. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ence Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…owner rationale The fee recorded at detection matters for deposits an upgrade finds not yet credited; a credited deposit has its mint amount in CreditedSweep. The minter's principal is excluded as a deposit owner because its default account is the ledger's minting account, not because of an address collision. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…xample with the automatic flow Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…er a concurrent scan Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n delay and the fee worst case Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…or to new registrations Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…vents the automatic flow adds Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2878ffd to
8494587
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Snapshot events currently prevent binary rollback and can make event-query pages exceed response limits.
2 open findings
What changed in this PR
Persists the automatic-deposit watchlist across canister upgrades through event-log snapshots.
Changes:
- Adds watchlist snapshot serialization and replay.
- Records snapshots during
pre_upgrade. - Adds unit, Candid, and PocketIC coverage.
| File | Description |
|---|---|
minter/src/test_fixtures/mod.rs |
Generates snapshot events for property tests. |
minter/src/state/watchlist/tests.rs |
Tests snapshot reconstruction. |
minter/src/state/watchlist/mod.rs |
Implements snapshot and restoration. |
minter/src/state/tests.rs |
Tests snapshot replay ordering. |
minter/src/state/mod.rs |
Restores snapshots into state. |
minter/src/state/event/candid/mod.rs |
Converts snapshot Candid types. |
minter/src/state/event.rs |
Defines the persisted snapshot event. |
minter/src/state/audit.rs |
Applies snapshot events during replay. |
minter/src/main.rs |
Registers the pre_upgrade hook. |
minter/src/lifecycle/tests.rs |
Tests snapshot recording. |
minter/src/lifecycle.rs |
Records snapshots before upgrades. |
minter/cksol_minter.did |
Exposes snapshot debug-event types. |
libs/types-internal/src/event.rs |
Adds public event representations. |
integration_tests/tests/tests.rs |
Tests restoration across an upgrade. |
integration_tests/src/fixtures.rs |
Adds watchlist test constants. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Pins the parameters, the response size estimate and the cycles the minter chooses for 1 and 100 addresses, which the tests of the response alone leave unchecked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e to the SOL RPC canister As for every other call, the SOL RPC canister prices the call with its own default estimate, which allows 192 bytes per account without data in its upcoming release, close to what the minter estimated, so the minter no longer mirrors the canister's internal sizes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recording it when the responses arrive skipped a slot that passed while the reads were in flight. The registration the reads were made for must still be live when they arrive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a failed read A tick that finds the previous one still waiting for its calls makes no call, and a failed read of a registration that was replaced or expired meanwhile leaves the current registration untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…balance read of deposit_sol Pins why deposit_sol compares the latest deposit id of the account rather than the one in flight: a deposit queued by a scan and minted before the balance read returns must not free the account for a second deposit from the stale balance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d one Since a balance scan of the automatic flow can queue the deposit an account's deposit_sol call returns, its mint may be reduced by the automatic deposit fee. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follows the minter, which no longer passes a response size estimate with getMultipleAccounts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d-sweeps Keeps the version of Watchlist::register of the base branch, without the Expired arm that the base removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the state transition The re-arm ran in the state transition of FailedTransaction and ExpiredTransaction, so post_upgrade replayed it against a watchlist without the transient registrations it was made against, and replayed its log line at every upgrade. The live caller now records the drop and then re-arms through process_dropped_sweep, which logs the outcome once, with a refused re-arm at Info. The state transition only drops the deposits again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docs of the swept deposits and of the Dropped status still said that only deposit_sol follows a dropped sweep. The re-arm no longer claims to keep an existing registration, since the account of a deposit in flight is never watched, and the test of that impossible case goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… event log Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e live one Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tchlist-snapshot The re-arm of a dropped sweep no longer runs in the replayed state transition, so the snapshot test now expects the replayed watchlist to stay empty after the replayed drop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he running version The snapshot recorded the window and the capacity of the watchlist it was taken from, which itself came from the previous snapshot, so a change of WATCH_WINDOW or MAX_WATCHED_ADDRESSES in a later version would never have taken effect. The snapshot now records only the entries, and the restore uses the constants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts schedule start The time was derived from the window end and the current window, which is off for a registration restored after a change of WATCH_WINDOW. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…minter Recording a snapshot on every upgrade made every upgrade to this version one-way: the previous version cannot decode the new event and traps on replay. Without registrations nothing is recorded, so a minter whose automatic flow was never used can still be rolled back. Replay may then restore an older snapshot, which brings back nothing but expired entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hlist tests The previous test inserted snapshots ahead of the recorded events, whose watchlist ended up the same with or without the restore, so a no-op restore passed every test. The tests now record the snapshot through pre_upgrade and compare the replayed watchlist with the one before the upgrade, including a re-armed and a scanned registration, and check that an upgrade without a snapshot brings back nothing but expired entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s trapping Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the pins to the dfinity/sol-rpc-canister#394 commit with the released sol_rpc_client 7.0.0 and sol_rpc_types 4.0.0, and run the integration tests against the SOL RPC canister v1.4.0 release wasm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de loses Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…be registered again Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The PR description needs to reflect that empty watchlists are not snapshotted and replay can retain expired entries from an older snapshot.
5 open issues
get_events pages can exceed the IC 2 MiB response limit First rollout drops existing registrations without a migration snapshot Event-count pagination can exceed reply limits and trap replay New event variant makes upgrades unrevertible due to decoder panic PR description misstates empty snapshot and replay behavior · New
Review effort: Balanced
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues block the changes that preserve the automatic-deposit watchlist across minter upgrades.
5 open issues
get_events pages can exceed the IC 2 MiB response limit First rollout drops existing registrations without a migration snapshot Event-count pagination can exceed reply limits and trap replay New event variant makes upgrades unrevertible due to decoder panic PR description misstates empty snapshot and replay behavior
Review effort: Balanced


The watchlist of the automatic deposit flow of #272 lives on the heap and registrations are not recorded as events, so a new
pre_upgradehook records a snapshot of it in the event log, and replay restores the latest one under the window and capacity of the running version. As in the ckETH minter, an empty watchlist records no snapshot, so upgrading a minter whose automatic flow was never used can still be rolled back; once a snapshot is recorded, a faulty version has to be fixed forward. When a later upgrade records no snapshot, replay restores the older one, which brings back only expired entries.🤖 Generated with Claude Code