Skip to content

Continuity snapshots have no expiry, so a withdrawn passport can outlive its withdrawal #137

Description

@LKSNDRTMLKV

The static continuity tier serves a passport's last signed public view from an object store when the node is unreachable. The reconcile design is sound; the claim it makes has no time bound, and that is where withdrawal breaks.

What already works, so it is not re-litigated

snapshot_drain.rs is outbox-backed rather than best-effort. It reads the passport's current status and decides put-or-remove there, so a replayed row cannot apply a stale action; anything not Published — suspended, archived, deactivated, draft, deleted — takes store.remove(); both branches are idempotent and failures back off and retry. That is the right shape.

Gap 1 — withdrawal requires the node to be up

The outbox lives in the node's own database. If the node is down, a suspension cannot propagate to the object store. That is precisely the state the continuity tier exists to serve, so the failure mode is: operator suspends a passport, node is unreachable, and the snapshot keeps answering as though nothing happened — for as long as the outage lasts.

Continuity and withdrawal are in direct tension, and availability currently wins unconditionally.

Gap 2 — the signed payload makes an unbounded claim

store_published writes a signed public view with no expiry field. Nothing in the payload says when it was produced or how long it should be trusted. A consumer that obtains it — through the proxy, from a cache, from a mirror, or from a copy someone kept — cannot distinguish it from a live response, and neither can a verifier.

Staleness is currently communicated only by an HTTP header added by the reverse proxy. Headers do not survive being cached, copied, or fetched by any path other than the intended one, and they are not covered by the signature.

Serving a withdrawn passport under a still-valid signature is a correctness problem, not a caching one.

Proposal

Put the bound inside the signed payload:

  • Add a validUntil (and an explicit asOf) to the snapshot public view, covered by the signature.
  • The node refreshes published snapshots on a cadence comfortably shorter than that window, re-signing with a new validUntil.
  • Withdrawal then becomes stop refreshing — fail-safe, and it works while the node is down, because the default behaviour is expiry rather than persistence.
  • Verifiers reject an expired snapshot. Filed as a dependency on the core verifier.

This also gives the tier an honest self-description: "this was the signed public view as of T, and it vouches for itself until T+N" — which is a claim a DPP can actually stand behind, unlike an unbounded one.

Choosing N

It is a policy trade, not a technical one: N is the maximum time a withdrawn passport can keep answering, and simultaneously the maximum outage the tier can ride out. Both directions have a regulatory flavour — availability duty against accuracy duty — so the number deserves to be written down with its reasoning rather than picked.

Related, deployment-side rather than engine-side

The object store is currently world-readable, so the snapshot is reachable directly, bypassing the proxy and every header it adds. That is being fixed separately by making the bucket private — but note that it only removes the bypass. It does not bound the claim, which is why the expiry belongs in the payload rather than in the deployment.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions