Skip to content

feat(versioned-state): run a chained list of migrations in one upgrade (ENG-517)#522

Open
peer2f00l wants to merge 6 commits into
devfrom
feature/eng-517-migrate-methods-support-running-n-consecutive-migrations-in
Open

feat(versioned-state): run a chained list of migrations in one upgrade (ENG-517)#522
peer2f00l wants to merge 6 commits into
devfrom
feature/eng-517-migrate-methods-support-running-n-consecutive-migrations-in

Conversation

@peer2f00l

@peer2f00l peer2f00l commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to ENG-481. Generalizes the macro-generated migrate() from running a single state transform per upgrade to an ordered list [A, B, C] run consecutively in one atomic receipt. This lets a contract that has fallen 2+ state versions behind catch up in a single deploy+migrate, instead of needing multiple round-trips or a bespoke "skip N versions" transformer.

The chain is fully validated before any transform runs, so an invalid chain reverts without writing state:

  1. first migration's input version == stored state version,
  2. each step's output version == the next step's input version,
  3. last step's output version == target (<CurrentState>::VERSION).

Each step's StateTransformer::run still independently re-asserts stored == input_version() and bumps the stored version as it goes; the trailing require!(!needs_migration()) post-condition is kept as defense-in-depth.

Backwards compatibility

migrate_args still accepts the legacy single-object wire shape ({"from_version":"v0"}, parsed as a one-element chain) as well as a JSON array. No pending governance proposals or existing callers change. The opaque governance/manager --migrate-args-file passthrough already forwards raw bytes, so an operator can supply an array today with no code change.

Production-migration immutability

No already-deployed migration is modified. The Migrator trait gains only additive input_version/output_version accessors that delegate to the underlying StateTransformer — so universal-account's UnbrickV1 (which overrides input_version() to 0 while reading v1 state) flows through unchanged.

Changes

  • common/src/versioned_state/core.rs — extend trait Migrator; add parse_one_or_many (single-object-or-array), run_migration_chain (validate-then-run), MigrationChainError.
  • common/src/versioned_state/macros.rsmigrate() parses one-or-many and delegates to run_migration_chain, keeping the ENG-481 needs/empty-input gate and post-condition.
  • The 4 Migrator impls (universal-account, proxy-oracle common, governance-contract, pyth-lazer empty-enum) gain the two accessors.

Tests

  • templar-common: chain runner (multi-step, single-step, empty, wrong-start, broken-link, not-landing-on-target) + parse_one_or_many (object, array, leading-whitespace).
  • templar-universal-account: genuine v0→v1→v2 two-step catch-up in one call over real state structs; invalid-chain rejection before running; all pre-existing UnbrickV1/rejection tests stay green.

Verification

  • cargo fmt --all --check clean; clippy -D warnings clean on touched crates.
  • Lib unit tests green: templar-common, universal-account, proxy-oracle common, governance-contract, pyth-lazer.
  • Two independent adversarial review passes: zero findings.

Closes ENG-517.

🤖 Generated with Claude Code


This change is Reviewable

peer2f00l and others added 2 commits July 22, 2026 18:22
…e (ENG-481)

Generalize the macro-generated `migrate()` from a single state transform to an
ordered list `[A, B, C]` run consecutively in one atomic receipt. The chain is
fully validated before any transform runs — first input == stored version, each
output == the next input, last output == target — so an invalid chain reverts
without writing state. Each step's `StateTransformer::run` still re-asserts
`stored == input_version()` as it goes.

Backwards compatible: `migrate_args` still accepts the legacy single-object wire
shape (parsed as a one-element chain) as well as a JSON array. Production-deployed
migrations (incl. universal-account `UnbrickV1`) are untouched — the `Migrator`
trait gains only additive `input_version`/`output_version` accessors that delegate
to the underlying transformer.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Drop the per-iteration `index == 0` branch and `.enumerate()` from
`run_migration_chain`: split the first migration off with `split_first`
(which also yields the empty-chain check), validate its start against stored
version once, then loop the rest for link mismatches. Same behavior and same
distinct Start/Link/End errors, less branching.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 63793dce-e067-4faa-b1cb-45d7383478bc

📥 Commits

Reviewing files that changed from the base of the PR and between bb9f523 and 94fe8c0.

📒 Files selected for processing (1)
  • contract/universal-account/README.md

📝 Walkthrough

Summary

  • Adds validated, atomic multi-step migration chains that run an ordered sequence in a single upgrade:
    • Pre-exec validation: reject empty chains, enforce stored version matches first step input, enforce step-to-step version linkage (each step’s output must equal the next step’s input), and enforce final output matches the target version.
    • Version accessors: extends Migrator with input_version() / output_version() and uses them for chain checks.
    • Ensures the post-condition (needs_migration() / stored-version alignment) is re-verified after the chain run.
  • Supports both payload formats for chained upgrades:
    • Legacy “single object” migration payloads
    • New JSON-array payloads (including acceptance of leading whitespace before an array)
    • Implemented via parse_one_or_many(...).
  • Updates the impl_versioned_state! macro and all affected Migrator implementations to produce correct chain metadata and to execute the full chain via run_migration_chain(...) (instead of stepwise migration execution).
  • Adds/updates tests covering:
    • Parsing for legacy vs array inputs and whitespace handling
    • Valid single-step and multi-step chain execution
    • Invalid-chain rejections with the expected MigrationChainError variant
    • Atomicity: on failure, stored state version remains unchanged
    • Universal-account upgrade behavior and the updated “unbrick” flow.

Human review focus

  • Atomicity & ordering: confirm that no migration step can mutate state unless the entire chain passes validation (stored/start mismatch, link mismatch, final target mismatch, empty chain).
  • Correct version metadata: verify every deployed Migrator reports accurate input_version() / output_version()—a wrong pair can bypass linkage checks or block legitimate upgrades.
  • Payload deserialization safety: ensure parse_one_or_many cannot misinterpret mixed/invalid payload shapes and that legacy-object compatibility can’t smuggle unintended chains.
  • Macro correctness: re-check the generated migrate() flow for:
    • using the correct target version when validating the chain
    • re-checking live stored version after execution (preventing “partially behind” state).
  • If applicable, cross-check against known migration/upgrade pitfalls cataloged in kadenzipfel/smart-contract-vulnerabilities (especially around upgrade/initialization/mutation order and rollback assumptions).

Walkthrough

The versioned-state migration framework now accepts legacy or ordered migration input, validates complete version chains before execution, and runs multiple migrations sequentially. Migration implementations report input and output versions, with tests covering successful chains and rejected invalid or deserialization-failing migrations.

Changes

Versioned migration chains

Layer / File(s) Summary
Migration chain contracts and execution
common/src/versioned_state/core.rs
Migrator exposes input and output versions; payloads support one object or arrays; validation checks stored alignment, step links, and target version before execution.
Generated migration entry point
common/src/versioned_state/macros.rs
Generated migrate() parses and runs complete chains, then verifies the target state version.
Migration version reporting and integration coverage
contract/proxy-oracle/..., contract/pyth-lazer/..., universal-account/src/state/migration.rs
Migration enums implement version reporting, and tests cover successful multi-step execution and link mismatch rejection.
Contract migration flow coverage
contract/universal-account/tests/migration.rs, contract/universal-account/README.md
Integration tests and migration documentation cover legacy payloads, single-call chained migration, payload formats, and atomic deserialization failure handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant GeneratedMigrate
  participant parse_one_or_many
  participant run_migration_chain
  participant MigrationSteps
  Caller->>GeneratedMigrate: Submit legacy object or migration array
  GeneratedMigrate->>parse_one_or_many: Deserialize migration input
  parse_one_or_many-->>GeneratedMigrate: Return ordered migrations
  GeneratedMigrate->>run_migration_chain: Validate chain against target version
  run_migration_chain->>MigrationSteps: Run validated steps sequentially
  MigrationSteps-->>GeneratedMigrate: Advance stored state version
  GeneratedMigrate-->>Caller: Return migration result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/eng-517-migrate-methods-support-running-n-consecutive-migrations-in

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • LINEAR integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@common/src/versioned_state/core.rs`:
- Around line 264-411: Parameterize the four rejection tests around
run_migration_chain with rstest, consolidating their shared context setup,
invocation, expected MigrationChainError, and unchanged-state assertion into one
readable test. Supply cases for empty, wrong-start, broken-link, and
end-mismatch scenarios while preserving each case’s inputs and exact error
expectations.
- Around line 171-215: Verify the runtime’s panic-rollback semantics support the
atomicity assumed by run_migration_chain when multiple migration.run() calls
execute in one receipt. Also confirm realistic migration chain lengths fit
available gas, especially alongside migrate() authorization and the updated
required Migrator methods; update implementation or validation only if these
checks expose an issue.

In `@common/src/versioned_state/macros.rs`:
- Around line 48-68: Document and enforce a bounded migration-chain policy
around the macro’s migrations parsing and run_migration_chain call: validate
that the number of steps does not exceed a defined sane maximum before executing
any transform, and panic with a clear error when exceeded. Expose or reuse a
named limit so callers can account for the expected gas per step while
preserving atomic validation and execution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7540f220-8ca8-4802-80e9-dafb0d143f0e

📥 Commits

Reviewing files that changed from the base of the PR and between 9386a6f and ed13d14.

📒 Files selected for processing (6)
  • common/src/versioned_state/core.rs
  • common/src/versioned_state/macros.rs
  • contract/proxy-oracle/near/common/src/state/migration/mod.rs
  • contract/proxy-oracle/near/governance-contract/src/state.rs
  • contract/pyth-lazer/contract/src/state/migration.rs
  • universal-account/src/state/migration.rs

Comment thread common/src/versioned_state/core.rs
Comment thread common/src/versioned_state/core.rs
Comment thread common/src/versioned_state/macros.rs
peer2f00l added a commit that referenced this pull request Jul 22, 2026
Consolidate the four chain_rejects_* tests into a single #[rstest] case table
(empty, wrong-start, broken-link, not-landing-on-target), each pinning the exact
MigrationChainError variant and asserting no state was written. Addresses PR #522
review feedback; aligns with the repo's rstest guideline.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b6df7b6e9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread script/sandbox-up.sh Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contract/universal-account/tests/migration.rs`:
- Around line 450-454: Extend the failed V0→V1 migration assertions around the
existing outcome checks to verify stored_state_version(...) remains 0 after
execution. Keep the current failure and deserialization-error assertions, and
use the existing stored-state lookup to confirm the failed receipt does not
persist a version update.

In `@script/sandbox-up.sh`:
- Line 27: Restore the prebuild step in sandbox startup by re-enabling the
prebuild-test-contracts.sh invocation in script/sandbox-up.sh, ensuring it
builds the test Wasms and exports TEST_CONTRACTS_PREBUILT=1 before node-backed
tests run. If startup logic was moved elsewhere, place both the prebuild
invocation and environment export in that replacement path instead.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c60e1203-26b9-4a08-8d6c-460ce10234a3

📥 Commits

Reviewing files that changed from the base of the PR and between ed13d14 and 1b6df7b.

📒 Files selected for processing (3)
  • common/src/versioned_state/core.rs
  • contract/universal-account/tests/migration.rs
  • script/sandbox-up.sh

Comment thread contract/universal-account/tests/migration.rs
Comment thread script/sandbox-up.sh Outdated
peer2f00l and others added 2 commits July 22, 2026 20:05
Consolidate the four chain_rejects_* tests into a single #[rstest] case table
(empty, wrong-start, broken-link, not-landing-on-target), each pinning the exact
MigrationChainError variant and asserting no state was written. Addresses PR #522
review feedback; aligns with the repo's rstest guideline.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…-517)

Fixes a red-on-dev regression: ENG-481's all-or-nothing migrate guard forbids
leaving state stranded between versions, so the universal-account integration
tests that migrated a two-versions-behind contract one step at a time began
reverting (masked in CI because these sandbox tests are gated out of the default
run). Convert them to the chained API this branch introduces:

- from_0_2_0: migrate [V0, V1] in one call to reach v2.
- migrate_accepts_legacy_direct_payload: send a bare `{"from_version":"unbrick_v1"}`
  object against 0.4.0 (one step to target), still exercising the legacy
  non-array wire shape.
- from_0_4_0_v0_then_v1_fails_to_deserialize: single chained [V0, V1] call that
  passes version validation but fails to borsh-read 0.4.0's V1-shaped state.

Test-only change; the V0/V1/UnbrickV1 state transformers are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@peer2f00l
peer2f00l force-pushed the feature/eng-517-migrate-methods-support-running-n-consecutive-migrations-in branch from 1b6df7b to 530b04a Compare July 22, 2026 20:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contract/universal-account/tests/migration.rs`:
- Around line 438-458: Verify the migration integration test covering the
legacy-object V0→V1 failure and atomic rollback using the narrowest documented
Rust sandbox test command. Confirm asynchronous receipt execution and sandbox
deployment are exercised, and explicitly document the verification step if it
cannot be run in the current environment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7113dd1d-3190-451c-8422-d8a27eae8ad8

📥 Commits

Reviewing files that changed from the base of the PR and between 1b6df7b and 530b04a.

📒 Files selected for processing (2)
  • common/src/versioned_state/core.rs
  • contract/universal-account/tests/migration.rs

Comment thread contract/universal-account/tests/migration.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 530b04ab27

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread common/src/versioned_state/macros.rs
Post-review cleanup (no behavior change):
- Drop the inline comment in `run_migration_chain` that restated the function's
  doc comment verbatim.
- Fix the pyth-lazer empty-`Migration` doc: a new variant now needs arms in the
  whole `Migrator` impl (input_version/output_version/run), not just `run`; and
  with the contract at its launch version `migrate` is a no-op rather than
  "fails to deserialize".

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
migrate now validates and runs the whole migration chain in one call,
landing on the target state version. The stepwise recipe (migrate V0,
then migrate V1) no longer works — it fails on the first legacy object
with EndMismatch. Document the JSON-array chain form, the end-to-end
validation rules, the still-supported bare-object form, and that
unbrick_v1 lands directly on V2.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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.

1 participant