Skip to content

Adopt freenet-migrate for contract/delegate upgrade migration (production-proven on River + Delta) #62

Description

@sanity

@iduartgomez @netsirius — as of 2026-07-21 the freenet-migrate crate is the production migration mechanism for both River and Delta, and every Freenet dApp should adopt it so a WASM re-key (any code/dependency/stdlib change) never strands user state. This issue is the adoption guide for this app.

Why

Contract/delegate identity is content-addressed (key = blake3(wasm ‖ params)), so every rebuild that changes bytes re-keys the app and orphans state under the old key. Hand-rolled migration is a minefield — Delta's 2026-07-13 incident (delta#33/#34: generation-blind selection rolled reads back months; prefix-blind key migration silently broke editing) is the canonical case. The crate makes those decision-bug classes structurally inexpressible and is now field-deployed:

  • River: registry codegen + build-time validation (river#434), UI recovery probe on the decision driver (river#436), riverctl recovery on the driver (river#437) — live since 2026-07-21 (web-container 30000349; riverctl 0.1.80 / river-core 0.1.16 on crates.io).
  • Delta: the contract-generation sweep on the driver with the FoldAll policy (delta#36) — live since 2026-07-21.

What adoption looks like (smallest first)

  1. Registry + codegen + validation (build-time only, no runtime change): commit a legacy-code-hash registry (the crate reads River-style [[entry]] TOMLs directly, hex or base58) and point build.rs at freenet-migrate-build 0.2 — it emits byte-array consts matching hand-rolled shapes (canonical_consts(false) needs no runtime dep) and validates every hash + the delegate_key == blake3(code_hash ‖ params) derivation on every build. Add the CI hash-guard (check_migration_guard): a WASM change whose predecessor hash isn't registered fails CI instead of stranding state. Worked example: river#434.
  2. Recovery/probe on the decision driver (freenet-migrate 0.3): the sans-IO ProbeDriver owns probe order (newest-first by generation), hit criteria, advance/stop, and fold policy; your app pumps I/O and supplies decode / is-real / merge via ProbeStateOps. SelectionPolicy::NewestFirstWins for delete-by-absence states (River's case) or FoldAll for tombstoned CRDT states (Delta's case — gated on policy_check property helpers verifying your merge). Worked examples: river#436 (browser event-driven pump), river#437 (CLI await pump), delta#36 (concurrent-fanout hybrid + batched forward PUT).
  3. Operational rule that makes it all work: register the OUTGOING code hash in the registry before any WASM-changing release ships.

The dapp-builder skill in freenet-agent-skills (v1.9.0) documents the full pattern with the same references.

Honest scope note

The crate's production-proven surface is the contract path. Delegate-secret migration still runs the app-side old-WASM round-trip (the ReRunOldWasm era — fragile across ABI bumps, see river#204); the crate-owned delegate entry points and the node-side secret copy-forward primitive are the next phase (freenet-core#2776). Design delegate storage with a committed key registry now and adoption later stays cheap.

Happy to help with the adoption PR here — the River/Delta swaps were each small once the registry existed.

Part of freenet-core#2776 (graceful upgrades for the whole ecosystem).

[AI-assisted - Claude]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions