Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion .claude/rules/delegate-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ migration entry, **users lose all room data**.
- Run `add-migration` BEFORE your changes alter the WASM (stash changes first if needed)
- **Single source of truth**: `legacy_delegates.toml` — never manually edit byte arrays
- **Both steps use BLAKE3**: `code_hash = BLAKE3(wasm)`, `delegate_key = BLAKE3(code_hash)` — NOT SHA256
- **Publish both UI and riverctl** when WASM changes: `cargo make publish-all`
- **Publish both UI and riverctl** when the ROOM-CONTRACT WASM changes:
`cargo make publish-all`. riverctl embeds only `room_contract.wasm`, so a
delegate-only re-key (e.g. freenet/river#757) publishes the UI alone.

## Single Source of Truth: `legacy_delegates.toml`

Expand Down Expand Up @@ -173,6 +175,37 @@ Two related things worth keeping straight:
`#[cfg(feature = "migration")]` (`common/src/lib.rs`), which the contract build
does not enable — not because non-code changes are free. Do not generalise it.

## river-core dependency and version changes re-key the room contract

**Measured, 2026-10-09 (freenet/river#757).** Same canonical co-build
(`scripts/sync-wasm.sh`), river-core's own source unchanged except as noted:

```
baseline room_contract.wasm = a3e63c8c…
+ `serde_bytes` dependency on river-core room_contract.wasm = 48d91e7b…
+ river-core version 0.1.21 -> 0.1.22 room_contract.wasm = c29522a5…
```

Same size, functions reordered, even though the contract never calls the new
code. (The mechanism is not established; river-core's crate metadata feeding
symbol order is a guess.) So a change meant for the delegate alone (a new wire
helper, a dependency only the delegate uses) can still re-key the room
contract, which then needs `add-room-contract-migration` and a riverctl
release. When the change does not need either, keep river-core's dependency
list and version untouched (#757 hand-rolled a ~55-line serde helper instead of
adding `serde_bytes`), and ALWAYS compare `room_contract.wasm` against `main`
after `sync-wasm`. The two `common/src/chat_delegate.rs` changes checked so far
(#345, #757) left it byte-identical, which is an observation, not a rule.

The exception is a release that must ship riverctl anyway: river-core is
published, riverctl pins it with `=`, and `release-riverctl.yml` skips a
river-core version crates.io already has, so a river-core source change that
riverctl needs requires the bump, and with it the room-contract re-key.

Note the measurement must use the co-build: building `-p room-contract` alone
resolves river-core with different features and gives a different hash even
on an unmodified tree.

## Technical Details

- **Delegate key formula**: `BLAKE3(BLAKE3(wasm) || params)` — both steps use BLAKE3
Expand Down
29 changes: 21 additions & 8 deletions .claude/rules/river-publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ cargo fmt
### Step 5: Commit, Build, and Publish

```bash
git add legacy_delegates.toml ui/public/contracts/ cli/contracts/
git add legacy_delegates.toml pointer-records.toml ui/public/contracts/ cli/contracts/
git commit -m "fix: <description> with delegate migration"
cargo make build
cargo make compress-webapp
Expand Down Expand Up @@ -248,12 +248,25 @@ where stale legacy data overwrites newer state on the current delegate
(non-destructive — the blob is left as a rollback fallback).
- **Nothing** → `fire_legacy_migration_request()`.

The **interrupted-migration flag** is a per-legacy-set localStorage marker
(`river_legacy_migration_in_progress:<fingerprint>`, parallel to the
`…_done:` flag): set BEFORE any migration re-save (`hydrate_loaded_rooms`'s
legacy branch and the current-blob explosion alike) and cleared ONLY on a
FULL successful re-save. A partial/aborted re-save therefore leaves it set,
which is what drives the recovery above.
The **interrupted-migration marker** is a key in the CURRENT delegate,
`__river_legacy_migration_in_progress__` (`LEGACY_MIGRATION_IN_PROGRESS_KEY`),
stored (once per session, acknowledged, retried once) BEFORE the first
migration re-save (`hydrate_loaded_rooms`'s legacy branch and the
current-blob explosion alike) and deleted ONLY by the legacy fan-out's
quiescence seal, once no re-save is running and none failed. Several
generations re-save concurrently and share it, and a recovery that finds
nothing to add also converges at that seal. A partial re-save therefore
leaves it in the delegate; the next load sees it in the `ListResponse` and
that drives the recovery above. A freenet-migrate walk wip marker
(`__migrate_pred_wip__:<hex>`) without its done marker counts the same way,
because the walk's flush also writes per-room keys. Session state is the
pure `MigrationMarkerState`; marker Store/Delete are serialised (they share
one correlation slot).
It was a localStorage flag until freenet/river#757, which never worked in
production: the gateway's iframe sandbox omits `allow-same-origin`, so
localStorage is unavailable. The `…_done:` seal is likewise inert there, so
an account with no rooms at all re-probes legacy on every load.
`is_migration_marker_key` keeps the marker from being copied forward.
4. `fire_legacy_migration_request` probes each legacy delegate TWO ways:
(a) fixed `GetRequest` for `[ROOMS_STORAGE_KEY, OUTBOUND_DMS_STORAGE_KEY]`
(the pre-#345 single-blob format + DM cache), and (b) a `ListRequest` to
Expand Down Expand Up @@ -347,7 +360,7 @@ call-site swap, the dual-running period and the parity test.
- **Wrong hash algorithm**: BLAKE3 not SHA256 for CodeHash
- **Forgetting migration**: Users lose all room data
- **Computing key AFTER changes**: Must run `add-migration` BEFORE changes alter the WASM
- **Not republishing riverctl**: Use `cargo make publish-all` when WASM changes
- **Not republishing riverctl**: Use `cargo make publish-all` when the room-contract WASM changes (riverctl does not embed the chat delegate)
- **Parameters file**: Always use `published-contract/webapp.parameters` (committed) — determines contract ID

## Contract ID
Expand Down
1 change: 1 addition & 0 deletions FREENET.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ This file enumerates the Freenet contracts and delegates published from this rep
## Notes for integrators

- Depend on `river-core` for the wire types; you almost never need to compile or execute the contract/delegate WASM yourself to read or construct River-compatible data.
- Chat-delegate message byte fields (stored values, signing payloads) are CBOR **byte strings** from the delegate generation after `V32` (freenet/river#757); earlier generations used CBOR arrays of integers. River encodes with ciborium, which decodes either form into a `Vec<u8>`; a decoder built on another CBOR library must accept both.
- Every contract/delegate here can re-key on any release (see the Migration notes above) — **a build-time-constant reference to a key will silently go stale.** Resolve a pointer instead; see below.

## Stable identity: resolve a pointer, do not pin a key
Expand Down
Loading
Loading