Skip to content

RepoState.upgrade is plumbed but never followed — the forward-migration path is scaffolding #67

Description

@sanity

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]

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions