Skip to content

fix(paid-service): persist init args across canister upgrades - #231

Merged
AntonioVentilii merged 3 commits into
mainfrom
av/paid-service-post-upgrade-state-79d062
Jul 15, 2026
Merged

AntonioVentilii merged 3 commits into
mainfrom
av/paid-service-post-upgrade-state-79d062

Conversation

@AntonioVentilii

Copy link
Copy Markdown
Contributor

Problem

The paid_service example canister stores its init args — including the payment ledger — in a non-stable thread_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_local state. So after any upgrade, INIT_ARGS becomes None. The cost_1b update method lazily builds the PAYMENT_GUARD LazyLock, whose initializer calls payment_ledger() → init_element → .expect("No init args provided"). The result: every cost_1b call 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_guard library, and the trap is specific to cost_1b — the sibling methods don't read INIT_ARGS. But the example should model the correct upgrade-safe pattern.)

Fix

  • Add #[pre_upgrade] / #[post_upgrade] hooks that save the init args to stable memory (ic_cdk::storage::stable_save) and restore them on upgrade.
  • Add state::get_init_args() helper.
  • Add a regression test (tests/it/upgrade.rs) that upgrades the canister and asserts cost_1b still succeeds. It fails on main (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 -- --check and cargo clippy --all-targets clean ✓
  • Candid .did interface unchanged (upgrade hooks are not candid methods).

The full PocketIC integration run (scripts/test.integration.sh) needs dfx deploy + the PocketIC server binary, which weren't available in my environment — CI should exercise the new test end-to-end.

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.
@AntonioVentilii
AntonioVentilii requested a review from a team as a code owner July 15, 2026 07:30
@zeropath-ai

zeropath-ai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

✅ No security or compliance issues detected. Reviewed everything up to c55edbb.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► src/example/paid_service/src/lib.rs
    Add pre_upgrade and post_upgrade hooks to persist and restore init args across upgrades
    Modify init import to include post_upgrade, pre_upgrade, and adjust to use get_init_args
► src/example/paid_service/src/state.rs
    Add get_init_args() to return current init args if any
► src/example/paid_service/tests/it/main.rs
    Add upgrade module to integration tests
► src/example/paid_service/tests/it/upgrade.rs
    New file: regression tests ensuring paid service remains usable after upgrade with/without explicit init args
► src/example/paid_service/tests/it/util/test_environment.rs
    Add upgrade_paid_service helper to perform in-place upgrade with optional init args

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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_1b still 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.

Comment thread src/example/paid_service/src/lib.rs
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 AntonioVentilii changed the title fix(paid_service): persist init args across canister upgrades fix(paid-service): persist init args across canister upgrades Jul 15, 2026
@AntonioVentilii
AntonioVentilii enabled auto-merge (squash) July 15, 2026 09:01
@AntonioVentilii
AntonioVentilii merged commit 9e82470 into main Jul 15, 2026
13 checks passed
@AntonioVentilii
AntonioVentilii deleted the av/paid-service-post-upgrade-state-79d062 branch July 15, 2026 11:13
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