Repository navigation
fix(paid-service): persist init args across canister upgrades - #231
Merged
Merged
Conversation
The paid_service example stored its init args (including the payment ledger) only in a non-stable `thread_local` (`INIT_ARGS`), set solely in `#[init]`. With no `#[post_upgrade]` hook, the IC wiped this state on every upgrade, leaving it `None`. The `cost_1b` update method lazily builds `PAYMENT_GUARD`, whose initializer reads `payment_ledger()` and traps with "No init args provided" when the state is missing — a persistent failure of `cost_1b` after any upgrade until the canister is reinstalled. Add `#[pre_upgrade]`/`#[post_upgrade]` hooks that save the init args to stable memory and restore them, plus a regression test that upgrades the canister and asserts `cost_1b` still succeeds.
|
✅ No security or compliance issues detected. Reviewed everything up to c55edbb. Security Overview
Detected Code Changes
|
There was a problem hiding this comment.
Pull request overview
This PR updates the example_paid_service canister to persist its initialization arguments (notably the payment ledger principal) across canister upgrades, preventing cost_1b from trapping after an upgrade due to wiped heap/thread-local state.
Changes:
- Add
#[pre_upgrade]/#[post_upgrade]hooks to save/restore init args via stable memory. - Add
state::get_init_args()accessor to support persistence. - Add a PocketIC regression test that upgrades the canister and verifies
cost_1bstill succeeds.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/example/paid_service/src/lib.rs | Adds pre/post-upgrade stable persistence for init args to keep cost_1b usable after upgrades. |
| src/example/paid_service/src/state.rs | Adds get_init_args() to retrieve current init args for upgrade persistence. |
| src/example/paid_service/tests/it/util/test_environment.rs | Adds a helper to upgrade the canister in PocketIC tests. |
| src/example/paid_service/tests/it/upgrade.rs | Adds regression coverage for cost_1b behavior across upgrades. |
| src/example/paid_service/tests/it/main.rs | Registers the new upgrade integration test module. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Address review feedback: the previous post_upgrade called stable_restore().expect(...) unconditionally, so upgrading from a version without pre_upgrade (e.g. current main) would find no valid candid payload in stable memory and trap, aborting the upgrade. Make post_upgrade: - accept an optional InitArgs argument (matching init's signature, so the candid interface is unchanged), used in preference to stable memory so operators can supply the ledger config while upgrading from an un-persisted version; and - tolerate a missing/malformed stable payload by falling back to None instead of trapping. Add a regression test for the explicit-args upgrade path.
AntonioVentilii
enabled auto-merge (squash)
July 15, 2026 09:01
DenysKarmazynDFINITY
approved these changes
Jul 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The
paid_serviceexample canister stores its init args — including the paymentledger— in a non-stablethread_local(state::INIT_ARGS), populated only in#[init]. There was no#[post_upgrade]hook and nothing persisted to stable memory.On the IC, a canister upgrade re-instantiates the Wasm and wipes heap/
thread_localstate. So after any upgrade,INIT_ARGSbecomesNone. Thecost_1bupdate method lazily builds thePAYMENT_GUARDLazyLock, whose initializer callspayment_ledger()→init_element→.expect("No init args provided"). The result: everycost_1bcall traps after an upgrade, a persistent availability failure for that method until the canister is reinstalled.(Scope note: this is example code demonstrating the
ic_papi_guardlibrary, and the trap is specific tocost_1b— the sibling methods don't readINIT_ARGS. But the example should model the correct upgrade-safe pattern.)Fix
#[pre_upgrade]/#[post_upgrade]hooks that save the init args to stable memory (ic_cdk::storage::stable_save) and restore them on upgrade.state::get_init_args()helper.tests/it/upgrade.rs) that upgrades the canister and assertscost_1bstill succeeds. It fails onmain(trap) and passes with this fix.Verification
cargo build -p example_paid_service --target wasm32-unknown-unknown --release✓cargo test -p example_paid_service --no-run(integration tests compile) ✓cargo fmt -- --checkandcargo clippy --all-targetsclean ✓.didinterface unchanged (upgrade hooks are not candid methods).The full PocketIC integration run (
scripts/test.integration.sh) needsdfx deploy+ the PocketIC server binary, which weren't available in my environment — CI should exercise the new test end-to-end.