Skip to content

chore(deps): upgrade River to freenet-stdlib 0.12.1 and freenet-migrate 0.8 (re-keys room contract and chat delegate) - #834

Closed
sanity wants to merge 3 commits into
mainfrom
chore/stdlib-0.12
Closed

sanity wants to merge 3 commits into
mainfrom
chore/stdlib-0.12

Conversation

@sanity

@sanity sanity commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Problem

River was built against freenet-stdlib 0.8.5 while freenet-core links 0.12.1, and its UI and riverctl used freenet-migrate 0.7 / 0.6, which require stdlib 0.8. Moving River forward needed the new freenet-migrate 0.8 line (freenet/freenet-migrate#51, published as 0.8.0).

Approach

One upgrade, every River crate:

  • freenet-stdlib 0.8.5 -> 0.12.1 (workspace), freenet-migrate -> 0.8 in both the UI (was 0.7) and riverctl (was 0.6; riverctl uses only the pointer/probe half, unchanged since 0.6). freenet-macros 0.2.0 -> 0.3.1 follows stdlib. freenet-scaffold 0.2.2 and freenet-migrate-build 0.2.0 were already current.
  • This re-keys both WASMs, handled per .claude/rules/delegate-migration.md:
    • First commit: the outgoing generations, recorded before anything rebuilt, as V38 in legacy_delegates.toml (chat delegate da47cbd9…, key 5e2084d5…) and common/legacy_room_contracts.toml (room contract fa9e59e8…). Both equal the committed artifacts on main.
    • Then sync-wasm: room contract fa9e59e8… -> abd7eb67…, chat delegate da47cbd9… -> b703ec55…, ui/public/contracts/ and cli/contracts/ in sync.
    • Pointer records re-signed (room contract v8, chat delegate v9). check-pointer-freshness passes.
    • river-core 0.3.1 -> 0.4.0 (its public API carries stdlib types, so a semver-breaking bump; riverctl pins it with =), riverctl 0.2.24 -> 0.2.25 (it embeds the new room contract).
    • Pins updated: the room-contract registry test (38 generations + digest) and chat_delegate_wasm_is_byte_identical.
  • The only source change the bump forced: OutboundDelegateMsg gained UnsubscribeContractRequest (tag 8), so the exhaustive log-kind match names it.

Compatibility with nodes on older freenet-core. The new delegate and room contract import and export exactly what the old ones did (wasm-objdump: delegate imports the four freenet_delegate_secrets calls plus the logger; contract imports only __frnt__fill_buffer), and neither carries a freenet-manifest section. Between stdlib 0.8.5 and 0.12.1 the contract interface code is unchanged, the delegate message enums only gained appended variants (River emits none of them), and the client-request variant 0.9.0 removed was the last one. The rehearsal below ran on freenet 0.2.151, and a second one on 0.2.134, whose host links stdlib 0.8.5. Nodes 0.2.122-0.2.133 (also stdlib 0.8.x hosts) were not rehearsed; the wire analysis says they behave like 0.2.134.

The River URL does not move. publish-river publishes from the committed frozen published-contract/web_container_contract.wasm (7c9d7dbe…), which this PR does not touch; with the committed params it still derives raAqMh…. (Rebuilding that crate does not reproduce 7c9d7dbe…, on main or here; publishing never uses a rebuild.)

Dev-dependencies not moved. freenet stays on the 0.1 line (0.1.188) for room-contract's integration tests. They are feature-gated and #[ignore]d, and porting them to freenet 0.2.x is #836. freenet-test-network 0.1.24 is already the latest release.

Only test-only old stdlibs remain. cargo tree --workspace --all-features -e normal,build --target all -d shows no duplicate stdlib. The 0.1.40 and 0.3.5 copies in Cargo.lock come only from dev-dependencies (freenet-test-network 0.1.24, already the latest, for riverctl; freenet 0.1.188 for room-contract's integration tests). They don't ship and weren't changed here.

Rehearsal (isolated network-mode node, judged by arithmetic)

A freenet network gateway (0.2.151) on its own ports with no peers and telemetry off. The UI was published under a throwaway container key, using the committed frozen container WASM so both builds share one origin. The node registered only V38: its secrets dir 7LS5qk… is bs58 of V38's registry key, which I derived myself. The successor's dir (3vggzN…) was absent before the upgrade.

Baseline, built from main (V38) through the real old UI and old riverctl. Alice owns Rehearsal Public (3 messages) and Rehearsal Private (2 messages). Bob and Carol joined Public and posted. Bob owns Bob Room (2 messages) and invited Alice, who joined and posted. Alice DM'd Bob.

Upgrade. Published the new UI (version 2) over the same container and reloaded Alice's existing browser profile.

check baseline (old build, V38 keys) after upgrade (new build, new keys)
rooms in Alice's rail 3 3
UI messages: Bob Room / Private / Public 3 / 2 / 5 (identical bodies) 3 / 2 / 5 (identical bodies)
UI members: Bob Room / Private / Public 2 / 1 / 3 2 / 1 / 3
contract members: Public / Private / Bob Room (old key -> new key) 2 / 0 / 1 2 / 0 / 1
contract messages: Public / Private / Bob Room 7 / 2 / 4 7 / 2 / 4
room name, privacy, config version, bans, deputies — identical for all three
Alice's sent DM (outbound plaintext cache) shown shown
Bob decrypts Alice's DM (new riverctl, new key) yes yes
new delegate store absent 3 room slots, 3 signing keys, rooms_meta, outbound_dms, journal, V38 DONE marker
V38 store 20 files 20 files, untouched

Each room logged "Re-key merge … finished: every older key it owed returned the room". Console: 0 errors, 0 warnings, 0 panics.

Identities survive (the contract rejects a post from a non-member key):

  • Alice posted on the new build in all three rooms: messages 7->8, 2->3, 4->5.
  • Bob (as member, and as owner of his room) and Carol posted on the new riverctl: Public 8->10, Bob Room 5->6.

Second load (populated successor). 3 rooms; 8 / 3 / 5 messages; no duplicates, no errors. The journal reports V38 resolved, no interrupted import, 0 rejected items. The 34 older generations were never installed on this node, so they stay pending and are retried on the #761 cadence, the same as on the old build.

Emptied successor. I moved the new delegate's store aside and reloaded. Everything recovered from V38: 3 rooms, all 16 messages including the post-upgrade ones, the DM plaintext, 3 room slots, 3 signing keys, rooms_meta. A post after recovery was accepted (Private 3->4).

Caveat: the rehearsed UI linked freenet-migrate from its source checkout at the PR head, before the crates.io publish. It is the same source as the published 0.8.0, and it is UI-only (it enters neither key-bearing WASM).

Rehearsal: skipped release (V37 user, V38 never installed)

A fresh isolated 0.2.151 node, populated through the V37-era UI and riverctl (2b79676d2; delegate 0fc3625e…, room contract 8bfd9bcd…). The node's only delegate was V37 (7uJ65G…, bs58 of V37's registry key). Alice owned Public (Bob a member) and Private, and DM'd Bob. I then published the new UI directly, skipping V38.

V37 baseline after upgrade
UI rooms / messages Public / Private / members 2 / 4 / 2 / 2,1 2 / 4 / 2 / 2,1
contract messages Public / Private (V37 key -> new key) 5 / 2 5 / 2 (then 3 after Alice's post)
DM plaintext shown shown

The walk recorded V38 (gen 34) as Unresponsive (not installed) and went on to import V37 (gen 33). Each room's re-key merge asked both older keys (V38 empty, V37 holding the room) and finished. Posts were accepted afterwards from Alice (new UI) and Bob (new riverctl). Console: 0 errors, 0 panics.

Rehearsal: old riverctl and the pointer records

On the main fixture after the upgrade:

  • Before the pointer existed: an OLD (V38) riverctl's write landed only on the V38 key (old key 11 messages, new key 10). The upgraded UI never saw it.
  • After publishing this PR's re-signed records to the node: the old riverctl refused to write ("Refusing to write with a room-contract generation this riverctl does not know"). The new riverctl wrote to the new key.

That is River's existing re-key window, not something this PR adds. It is why the deploy order below matters.

Testing

All of CI's test steps were run locally on this branch:

  • river-core: lib 292, with features 335, integration 762
  • room-contract: 3
  • riverctl: 442
  • river-ui: 1396, and 1402 with example data
  • chat-delegate: 59
  • web-container: 12
  • the migration, registry, delegate-key and publish-guard tests

check-migration, check-room-contract-migration and check-pointer-freshness pass.

Rehearsal on an older node (freenet 0.2.134, host stdlib 0.8.5)

The same upgrade, run again on a fresh isolated 0.2.134 node. It is the last release whose host links stdlib 0.8.5. Alice owned one room, Bob was a member, 4 messages. After the new UI loaded:

  • the delegate slot migrated;
  • the re-key merge finished;
  • the UI showed 4 / 2 (messages / members);
  • console: 0 errors.

Alice's post on the new build and Bob's on the new riverctl were both accepted: 7 messages at the new key. So the stdlib 0.12 delegate and contract run on a stdlib 0.8.5 host.

Deploy (not done here; held for a supervised run)

Do not merge yet. Ian asked for the merge and deploy to happen together, supervised. Do not use cargo make publish-all: it bumps riverctl a second time (0.2.25 -> 0.2.26), commits on local main, and cargo publishes riverctl without river-core 0.4.0, which crates.io does not have, so it fails with the UI already live. This PR corrects River's rules to say so. The sequence, from main after the merge:

  1. Re-confirm CI green on the merge commit.
  2. export PATH=$HOME/.cargo/bin:$PATH RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER=, check dx --version = 0.7.10, then cargo make compress-webapp && cargo make publish-river. Grep the log for incompatible (must be empty) and read the advisory read-back.
  3. Push the tag riverctl-v0.2.25. release-riverctl.yml publishes river-core 0.4.0, then riverctl 0.2.25, and cuts the GitHub release.
  4. Open the counter-bump PR for published-contract/contract-version.txt (river-publish.md step 6). It must merge before step 5's pre-flight (CI green on main's SHA) can pass.
  5. About 2 minutes after the UI is live: POINTER_NODE_PORT=<a network node of yours, not 7509> POINTER_WASM=~/code/freenet/freenet-migrate/contracts/pointer-contract/pointer-v1.wasm cargo make publish-pointer-records.
  6. Once crates.io shows riverctl 0.2.25: ~/bin/riverctl-autoupdate.sh on nova, then confirm riverctl debug contract-key 4uNUKFzZQCnzo4K2ecZ16cMsYEEfoaRS35z6exEsbvm4 gives the new key.
  7. Verify live: the River URL serves the new build, an existing room loads, and delegate migration completes.

Keep the gap between the UI publish (2) and the pointer publish (5) short, and get nova's riverctl updated promptly. I measured this on the rehearsal node. After the upgrade and before any pointer, an old riverctl's write landed only on the V38 key (old key 11 messages, new key 10), and the upgraded UI never saw it. Once the re-signed pointer records were published, the old riverctl refused to write ("Refusing to write with a room-contract generation this riverctl does not know"), and the new riverctl wrote to the new key. Nova's moderator and announcer run riverctl, so anything they write in that gap is stranded on the old key.

[AI-assisted - Claude]
https://claude.ai/code/session_01Xxpwzvt4ucaGfbeCr77Jns

…ib 0.12 upgrade

Registers the committed chat_delegate.wasm (code hash da47cbd9...) and
room_contract.wasm (fa9e59e8...), both built against freenet-stdlib 0.8.5,
before the upgrade rebuilds them.

Claude-Session: https://claude.ai/code/session_01Xxpwzvt4ucaGfbeCr77Jns
…grate 0.8

Re-keys the room contract (fa9e59e8 -> abd7eb67) and the chat delegate
(da47cbd9 -> b703ec55); both outgoing generations were registered as V38 in
the previous commit. river-core 0.3.1 -> 0.4.0 (its public API carries stdlib
types) and riverctl 0.2.24 -> 0.2.25 (it embeds the new room contract).

- OutboundDelegateMsg gained UnsubscribeContractRequest; name it in the
  exhaustive log-kind match.
- Pointer records re-signed for both artifacts (room contract v8, chat
  delegate v9).
- Registry pin (38 generations) and the chat-delegate byte-identity pin
  updated.

The new artifacts import and export exactly what the old ones did, and carry
no manifest section, so nodes on older freenet-core still instantiate them.
The web container is published from the committed frozen WASM, so the River
URL does not move.

Claude-Session: https://claude.ai/code/session_01Xxpwzvt4ucaGfbeCr77Jns
- Cargo comments: freenet-migrate unifies with the workspace's stdlib 0.12,
  and 0.8 is the stdlib 0.12 line.
- The RegisterDelegate layout note now covers stdlib 0.12.1.
- River's rules no longer send a room-contract re-key to `cargo make
  publish-all`: it bumps riverctl a second time and never publishes
  river-core, so it fails whenever riverctl pins a river-core version
  crates.io does not have (true for this PR's river-core 0.4.0). Release
  riverctl by tag instead, which publishes river-core first.

Comment and doc changes only; neither key-bearing WASM is affected.

Claude-Session: https://claude.ai/code/session_01Xxpwzvt4ucaGfbeCr77Jns

@sanity sanity left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comprehensive PR Review: #834

Summary

  • PR: upgrade River to freenet-stdlib 0.12.1 and freenet-migrate 0.8 (re-keys the room contract and chat delegate)
  • Review tier: Full (contract and delegate WASM re-key, migration, wire format)
  • Reviewers run: freenet:code-first-reviewer, freenet:testing-reviewer, freenet:skeptical-reviewer and freenet:big-picture-reviewer, plus a data-loss lens, each blind, on f7206f8. The fixes after that (c8355b4) are comment and doc changes only; I re-checked them diff-of-diff.
  • External model: codex review was unavailable tonight ("You've hit your usage limit … try again at 12:55 AM", on two attempts, on freenet/freenet-migrate#51 in the same session). The data-loss lens ran in its place, alongside the four. Per the multi-model-review rule I did not wait.
  • CI: green on c8355b4 (build, playwright, check-wasm-sync, check-delegate-migration, check-room-contract-migration, check-pointer-freshness).

Verified independently by reviewers

  • V38 equals what is live. The registry entries equal b3sum of origin/main's committed WASMs (da47cbd9…, fa9e59e8…), and delegate_key = BLAKE3(code_hash) = 5e2084d5…. The live bundle fetched through nova's node embeds main's delegate and room contract. No generation from V24 to V38 is missing.
  • The committed WASMs are the build of this tree. Two reviewers rebuilt both (--locked, a different checkout path and target dir) and got byte-identical abd7eb67… / b703ec55….
  • The UI and riverctl derive the same room key. ui/public/contracts and cli/contracts are identical. Pointer records are fresh and signed (v8/v9). The pins match.
  • Old nodes are unaffected. Import and export sets are identical to the previous generation's, with no custom sections. Comparing stdlib 0.8.5 with 0.12.1:
    • the client-API enums are unchanged apart from the removed last DelegateRequest variant;
    • the delegate enums gained only appended variants;
    • the contract interface code is unchanged;
    • get_context no longer returning None for UserResponse is irrelevant here, since River never calls it.
  • No behaviour change from the crate bump. freenet-migrate 0.6/0.7 → 0.8 changes only doc comments, and published 0.8.0 is byte-identical to git d5152e2.
  • The River URL does not move. publish-river uses the frozen committed published-contract/web_container_contract.wasm.

Findings and resolution

Must Fix (deploy plan, PR body): fixed. The body said to run cargo make publish-all. That bumps riverctl a second time (0.2.26), commits on local main, and cargo publishes riverctl without river-core 0.4.0. crates.io doesn't have 0.4.0, so the publish fails (reproduced with cargo package -p riverctl) after the UI is already live. The deploy section now gives the correct sequence:

  1. UI via publish-river;
  2. the counter-bump PR;
  3. the riverctl-v0.2.25 tag (the workflow publishes river-core first);
  4. the pointer records, with their environment variables;
  5. nova autoupdate, once crates.io shows 0.2.25;
  6. the live checks.

River's own rules (river-publish.md, delegate-migration.md) sent re-keys to publish-all too. They are corrected in this PR, as are the user-level river-publish skill and memory note.

Should Fix: fixed.

  • Stale Cargo.toml comments (stdlib "0.8.x"; the 0.7.0 rehearsal note).
  • The RegisterDelegate layout note now covers 0.12.1.
  • Untested node range: the body says 0.2.122-0.2.133 were not rehearsed.
  • Dev-dependency wording: freenet 0.1.x is not the latest, porting it is #836.

Should Fix (rehearsal gaps, data-loss lens): run and added to the body.

  • A skipped release (V37 data, V38 never installed): every room, message, member and the DM recovered. The walk recorded V38 as Unresponsive and imported V37.

  • Old riverctl after the upgrade:

    • before any pointer, its write lands only on the V38 key and is not seen;
    • after this PR's re-signed records were published to the node, it refuses to write.

    That is the existing re-key window. The deploy section now says to keep the gap between the UI and pointer publishes short, and to update nova's riverctl promptly.

Should Fix (CI gaps, testing): filed, predate this PR. No CI rebuild-and-compare of the committed WASMs (the existing issue #678, with this PR's evidence added). No pin on the WASM import and export surface (#835). Both were verified by hand for this PR.

Consider: noted, not changed.

  • The lockfile re-resolves windows-sys for Windows-only native dependencies. Windows builds only in the release workflow, which builds before it publishes.
  • An open tab on the old build after the journal resolves is covered by the existing top-up window. This PR doesn't change it, and I did not rehearse it.

Verdict

State: Ready to Merge on review and CI grounds. Held on purpose: Ian asked for the merge and deploy to happen together, supervised, in the morning.
HEAD SHA reviewed: c8355b4 (full review on f7206f8, light re-check of the comment- and doc-only delta).

[AI-assisted - Claude]

@sanity

sanity commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Closing without merging. The freenet-stdlib 0.12.1 / freenet-migrate 0.8 upgrade will land together with the room-state v2 work instead, so users go through one migration rather than two. The branch is kept so that work can reuse this PR's rehearsal and legacy-registry changes.

[AI-assisted - Claude]

@sanity sanity closed this Oct 29, 2026
sanity added a commit that referenced this pull request Oct 29, 2026
Brings in #837 (webpki-roots 1.0.11) and #838 (rustls-webpki 0.103.16)
ahead of folding in #834's stdlib 0.12.1 upgrade. No conflicts.

Claude-Session: https://claude.ai/code/session_01F9yCAkah1PdW8SVGV2q5oa
sanity added a commit that referenced this pull request Oct 29, 2026
river-core now builds against stdlib 0.12.1 (#834's upgrade, folded in),
and two stdlib versions cannot link. The contract interface needed no
change; its tests pass. The measurement tool's lock follows.

Claude-Session: https://claude.ai/code/session_01F9yCAkah1PdW8SVGV2q5oa
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