Files
amethyst/commons
Vitor PamplonaandClaude Opus 4.8 fac1bf5b5d feat(concord): refuse Control-Plane rollbacks with a version floor
`ConcordRefounding.compactControlPlane` re-wraps one edition per entity
when a community rotates epoch, and the ROTATOR chooses which one
survives. The receiving side had no memory: `refold()` folds only the
wraps at the current epoch's Control-Plane address and discards the prior
epoch's buffer, and `EditionFold` accepts whatever it is handed (its
no-genesis fallback anchors at the lowest version present).

So a rotator could publish only version 1 of a chain and omit version 2 —
restoring a revoked role, clearing a banlist, reverting metadata. Every
signature is genuine; this is rollback by omission, not forgery.

Adds a per-entity floor: the version AND hash last successfully folded.

- **No floor (fresh joiner)** — unchanged: genesis anchor, else the
  lowest-version edition as the legitimate compaction bootstrap.
- **With a floor** — the walk is anchored AT the floor: the offered set
  must contain the exact edition already folded (version and hash; a
  same-version sibling is a fork, not our chain), then walks up. A head
  below the floor is structurally unreachable.
- **Gap** (the floor edition is absent) — refuse, and keep the known
  head. Refusing by *retaining* matters here: this fold is recomputed
  from scratch each time, so letting an entity vanish would itself be a
  rollback — a dropped banlist is an unban.

The floor needs no new persistence. It is derived from `heldRoots`, the
rotated-out access roots already persisted in the NIP-44 self-encrypted
kind-13302 list: the session derives each prior epoch's Control-Plane
address from them, folds oldest-first, and takes the resulting heads as
the floor. That survives both a process restart and the session rebuild
`ConcordSessionRegistry.sync` performs at exactly the moment of a
Refounding — which would have destroyed any in-session floor. If the old
planes are not served, there is no floor and behaviour is as before.

Floors are built from AUTHORITY-GATED heads, not raw ones. Without that,
any ex-member still holding a rotated-out root could mint a high-version
edition on the old plane and freeze the entity for every honest client —
a denial of service this change would otherwise have introduced. Covered
by a test.

Verified by disabling both enforcement points: 7 of 12 quartz tests and
the end-to-end commons test fail, and the ones that still pass are
exactly the non-regression cases (fresh joiner, honest compaction,
pass-through without floors).

Known limit: `AuthorityResolver.resolve` folds authorized SUBSETS of the
edition pool and does not carry floors itself; gating happens at the pool
level before the resolver sees anything. Sound, but connectivity checked
on the full set is a weaker precondition than on each subset — passing
floors into the resolver's three folds is worth a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:36:25 -04:00
..