Repository navigation
fix(migration): never return a partial delegate export as Ok - #103
Merged
Merged
Conversation
DeltaPredecessorIo::fetch_secrets treated an unanswered sub-request (GetKnownSites, GetSigningKey, GetSigningKeyForPrefix, GetSiteState) as "nothing there" and returned Ok with whatever else came back. The wasm channel reports a timeout as Ok(None), so a lost reply after a successful executability probe produced a smaller export; freenet-migrate imported it and sealed the predecessor Done, and the missing secrets were never asked for again. fetch_secrets now fails with FetchError on silence, on a reply of the wrong kind, and on an unrecognised in-band error, so the predecessor is reported Unresponsive, earns no marker, and is retried on the next load. The delegate's definite "nothing usable" answers (signing-key absence in both the V1-V5 and current wording, a corrupt stored key, the known-sites decode error, and the per-prefix site-state absence) stay answers. Silence was previously also the expected result of sending an old generation a request its WASM cannot deserialize (the node reports that as a client-API error the UI drops). Those requests are now never sent: PredecessorCapabilities gates them by generation index (GetKnownSites V3+, GetSiteState V5+, GetSigningKeyForPrefix V8+), with the thresholds measured against each registry entry's WASM blob and pinned by test. The differential FakeNode now models an unsupported request as silence rather than an in-band error, and gives fixtures realistic generations. Closes #100 Claude-Session: https://claude.ai/code/session_01GKBuAzByqMPegsXcUYJb7h
Review of the first commit found that GetSigningKey is NOT understood by every generation: it arrived in V4 (index 3). V1-V3 cannot deserialize it, so with silence now failing the fetch they would have been reported Unresponsive on every load. Added a signing_key capability (FIRST_WITH_SIGNING_KEY = 3), verified against the WASM blobs and the DelegateRequest enum history, and pinned in the registry test (now with index 0 and a GetSigningKey column). V1-V3 keys have no export request at all, so neither this adapter nor the hand-rolled sweep can recover them; documented. Also from review: - narrow the V1-V5 em-dash signing-key absence in the reply registry to PublicKey/SigningKey awaits, so a concurrent sweep probe's "no key" cannot resolve an export sub-request and fail a healthy fetch; - prune timed-out awaits from the registry, so a dead await no longer swallows a later reply meant for a live one; - fixtures can pin an explicit generation (V3/V4 band now testable), and the shipped-sweep oracle no longer reads V1-V3 legacy slots; - tests for real delegate-key capability lookup, a mid-loop failure, a corrupt stored key, and another site's backup-absence error; - document the all-or-nothing and mis-correlated-absence bounds. Claude-Session: https://claude.ai/code/session_01GKBuAzByqMPegsXcUYJb7h
- the mid-loop test now silences the SECOND prefix (extra prefixes are probed first), so a loop that stops early fails it; - pin the four remaining fail-closed arms (unrecognised error for GetKnownSites/GetSigningKey, wrong-kind reply for GetSigningKeyForPrefix/GetSiteState); - add a V4 fixture (GetSigningKey but no GetSiteState) to the old-generations test; - document that a V1-V3 predecessor holding an unexportable key is sealed and reads complete, and why; correct three stale comments. Claude-Session: https://claude.ai/code/session_01GKBuAzByqMPegsXcUYJb7h
Contributor
Author
Consolidated review (Full tier: data-migration surface)Round 1 (5e03197)
Round 2 (e4beb26)
Round 3 (34fd855, tests and comments only): Codex reports no regression. CI is green on 34fd855. The delegate and contract WASM are unchanged, so there is no re-key. [AI-assisted - Claude] |
sanity
added a commit
that referenced
this pull request
Oct 15, 2026
Published from main at 240565c (includes #101 and #103). Claude-Session: https://claude.ai/code/session_01GKBuAzByqMPegsXcUYJb7h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
DeltaPredecessorIo::fetch_secretstreated a sub-request that got no reply as "nothing there" and returnedOkwith whatever the other sub-requests produced. The wasm channel reports a timeout asOk(None), so ifGetKnownSites,GetSigningKey,GetSigningKeyForPrefixorGetSiteStatetimed out afterprobe_executablesucceeded, the export was silently partial. freenet-migrate imported it, recorded the predecessorDone, and never asked for the missing secrets again. WithlocalStorageavailable (a local node), that lasts for the life of the browser profile, and nothing reports an error. freenet-migrate's contract says a partial export must be anErr.Approach
fetch_secretsclassifies every sub-reply as the value, a definite "nothing usable" answer, or a failure. Failures are silence, a reply of the wrong kind, and an unrecognised in-band error, and they now returnFetchError. The predecessor is reportedUnresponsive, gets no marker, and is re-walked on the next load.--wording and the V1–V5 em-dash wording (confirmed in the V1–V5 WASM blobs), a stored key that isn't 32 bytes, the known-sites decode error, and site-state absence for this prefix. Treating a permanently corrupt known-sites list as a failure would have withheld that predecessor's signing keys forever, so it proceeds andextra_prefixesdrives the per-site probes instead.DelegateError, the node reports it as a client-API error with no delegate key, andconnection.rsdrops it. Just mapping silence toErrwould therefore have made every pre-V8 predecessorUnresponsiveon every load, because they all getGetSigningKeyForPrefix. Their legacy-slot keys would never have been imported. So those requests are now never sent.PredecessorCapabilities::of_generationgates them:GetKnownSitesV3+ (index 2),GetSigningKeyV4+ (3),GetSiteStateV5+ (4),GetSigningKeyForPrefixV8+ (7). TheGetSigningKeythreshold came out of review (Codex, plus three of the Claude lenses): the first commit assumed it was V1+. It isn't, so V1–V3 would have beenUnresponsiveforever. V1–V3 keys have no export request at all, so neither path can recover them (documented). I found each threshold by locating the git blob ofui/public/contracts/site_delegate.wasmwhose BLAKE3 matches each registry entry'scode_hash, then checking it for the variant name. A test pins the registry hashes on either side of each threshold. A side benefit: old generations no longer burn a 10 s timeout per unsupported request.FetchErrorholds no reply payload, because a mis-routedSigningKeyreply would carry key material into the report'sDebugstring.Testing
New tests in
delegate_migration.rs:a_silent_export_sub_request_is_retried_never_sealedsilences each of the four sub-requests in turn. It asserts the predecessor isUnresponsiveand has noDonemarker, then checks that a healthy retry with the same markers recovers everything.a_reply_that_does_not_answer_an_export_sub_request_is_retriedcovers wrong-kind replies and unrecognised errors.a_definite_nothing_usable_answer_is_not_a_failurecovers the V1–V5 em-dash absence and the known-sites decode error.old_generations_are_only_sent_requests_they_understanduses V1, V5 and modern fixtures. It checks the request log and that none of them isUnresponsive.generation_capabilities_match_the_registrypins the thresholds to the registry's code hashes.capabilities_are_keyed_by_the_real_delegate_keybuilds the lineage from the realLEGACY_DELEGATES, where delegate key and code hash differ.a_failure_part_way_through_the_per_site_loop_fails_the_fetch,a_timed_out_await_does_not_swallow_a_later_replyandthe_v1_v5_signing_key_absence_only_answers_signing_key_awaitscover the rest of the review fixes.I also changed the differential
FakeNodeto model an unsupported request as silence, which is what really happens, not as an in-band"unsupported request"error. Fixtures now get realistic generation numbers, and a fixture list no real lineage could produce now panics.Mutation checks:
fetch_secretsto skip silence and wrong-kind replies fails the first two tests.GetSigningKeygate, slot pruning, em-dash narrowing, and keying the map by code hash.cargo fmt,cargo clippy --all-targets -D warnings,cargo test(all green) andcargo check -p delta-ui --target wasm32-unknown-unknownall pass locally.Closes #100
[AI-assisted - Claude]
https://claude.ai/code/session_01GKBuAzByqMPegsXcUYJb7h