What
RepoState.upgrade is documented as "Append-only successor pointer for breaking schema migrations" (crates/types/src/lib.rs:333). It is fully maintained by the CRDT machinery:
| site |
what it does |
crates/freenet-git/src/state_init.rs:63 |
initialises it to Some(None) at repo creation |
crates/types/src/lib.rs:484 |
validates the signed field |
crates/types/src/lib.rs:555 |
merges it via pick_signed_field |
crates/types/src/lib.rs:642,682-683 |
carries it in summaries and deltas |
crates/repo-contract/src/lib.rs:176 |
checks s.upgrade.is_none() |
But nothing reads it to act on it. I grepped every .upgrade reference across crates/freenet-git/src, crates/types/src and crates/repo-contract/src: there is no site that, on seeing Some(key), redirects to that RepoKey, follows it, or even warns. The pointer can be set and will replicate correctly — and no client will ever do anything with it.
So the forward-migration story is plumbed, not wired. The backward direction genuinely works (LEGACY_REPO_CONTRACT_WASM_HASHES + get_state_with_legacy_fallback in crates/freenet-git/src/main.rs:630-645), which is what makes this easy to miss: migration appears solved because half of it is.
Why it matters
This is the mechanism that would carry a repo across a breaking schema migration — the case the field's own doc comment names. Today, if that migration ever happened, an owner could publish the successor pointer exactly as designed and every client would ignore it.
It is also a case of existence is not reachability: a field that is declared, validated, merged and summarised looks covered from every angle except the one that matters. Nothing in CI would notice, because there is nothing to notice — the field is consistent, just inert.
Suggested shape
Whatever is done, the fix should pin the outcome rather than the presence of a call. A test asserting "a client that reads a repo whose upgrade is Some(k) ends up reading k" fails if the follow is deleted; a test asserting the field is set does not.
Worth deciding deliberately whether following it should be automatic, prompted, or bounded — River's equivalent (OptionalUpgradeV1) follows a chain with a per-hop timeout, a max-hops cycle guard, and a refusal to follow a pointer to an already-visited or not-newer contract (river/cli/src/api.rs:43-83). That prior art is probably worth copying rather than re-deriving, including its guards.
How this was found
While assessing freenet-git for the pointer-record rollout (freenet-core#5194). The conclusion there was that freenet-git should not get an app-identity pointer — it has no container contract and no curator, its repos are owned by their users, and RepoState.upgrade is already the right-shaped mechanism at the right granularity (owner-signed, per-repo). That conclusion stands; this issue is that the right-shaped mechanism is not connected yet.
[AI-assisted - Claude]
What
RepoState.upgradeis documented as "Append-only successor pointer for breaking schema migrations" (crates/types/src/lib.rs:333). It is fully maintained by the CRDT machinery:crates/freenet-git/src/state_init.rs:63Some(None)at repo creationcrates/types/src/lib.rs:484crates/types/src/lib.rs:555pick_signed_fieldcrates/types/src/lib.rs:642,682-683crates/repo-contract/src/lib.rs:176s.upgrade.is_none()But nothing reads it to act on it. I grepped every
.upgradereference acrosscrates/freenet-git/src,crates/types/srcandcrates/repo-contract/src: there is no site that, on seeingSome(key), redirects to thatRepoKey, follows it, or even warns. The pointer can be set and will replicate correctly — and no client will ever do anything with it.So the forward-migration story is plumbed, not wired. The backward direction genuinely works (
LEGACY_REPO_CONTRACT_WASM_HASHES+get_state_with_legacy_fallbackincrates/freenet-git/src/main.rs:630-645), which is what makes this easy to miss: migration appears solved because half of it is.Why it matters
This is the mechanism that would carry a repo across a breaking schema migration — the case the field's own doc comment names. Today, if that migration ever happened, an owner could publish the successor pointer exactly as designed and every client would ignore it.
It is also a case of existence is not reachability: a field that is declared, validated, merged and summarised looks covered from every angle except the one that matters. Nothing in CI would notice, because there is nothing to notice — the field is consistent, just inert.
Suggested shape
Whatever is done, the fix should pin the outcome rather than the presence of a call. A test asserting "a client that reads a repo whose
upgradeisSome(k)ends up readingk" fails if the follow is deleted; a test asserting the field is set does not.Worth deciding deliberately whether following it should be automatic, prompted, or bounded — River's equivalent (
OptionalUpgradeV1) follows a chain with a per-hop timeout, a max-hops cycle guard, and a refusal to follow a pointer to an already-visited or not-newer contract (river/cli/src/api.rs:43-83). That prior art is probably worth copying rather than re-deriving, including its guards.How this was found
While assessing freenet-git for the pointer-record rollout (freenet-core#5194). The conclusion there was that freenet-git should not get an app-identity pointer — it has no container contract and no curator, its repos are owned by their users, and
RepoState.upgradeis already the right-shaped mechanism at the right granularity (owner-signed, per-repo). That conclusion stands; this issue is that the right-shaped mechanism is not connected yet.[AI-assisted - Claude]