@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)
- 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.
- 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).
- 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]
@iduartgomez @netsirius — as of 2026-07-21 the
freenet-migratecrate 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:What adoption looks like (smallest first)
[[entry]]TOMLs directly, hex or base58) and pointbuild.rsatfreenet-migrate-build0.2 — it emits byte-array consts matching hand-rolled shapes (canonical_consts(false)needs no runtime dep) and validates every hash + thedelegate_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.freenet-migrate0.3): the sans-IOProbeDriverowns probe order (newest-first by generation), hit criteria, advance/stop, and fold policy; your app pumps I/O and supplies decode / is-real / merge viaProbeStateOps.SelectionPolicy::NewestFirstWinsfor delete-by-absence states (River's case) orFoldAllfor tombstoned CRDT states (Delta's case — gated onpolicy_checkproperty 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).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
ReRunOldWasmera — 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]