Stellar Ecosystem Proposal · Anonymous Group-Membership Keystone
One contract. Any update relation. No admin.
A Soroban-hosted keystone that verifies one PLONK proof per state-transition step and stays neutral about what the transition means. Admission, removal, key rotation, liveness-tick — all share apply(tag, new_root, expected_epoch, proof, extras).
The SEP is normative for the contract interface only. Each tag points to an immutable (vk, vk_digest, vk_digest_fr, pi_schema_hash, extras_arity, bootstrap_root, registrant) and a mutable (current_root, epoch). Concrete governance predicates are deferred to downstream proposals — this is the host, not the policy.
§1 · the contract surface
Three mutating methods. Nine read-only views. Nothing else.
Click any method to expand it. The contract has no init, no revoke_admin, no set_paused. The set below is closed.
Nine read-only views
Per tag, plus one global. None of these mutates state. None requires authentication.
§2 · the relation registry
One contract address. Many tags. Each tag is its own state machine.
A relation is registered once, permissionlessly, under a Symbol tag. After that, only apply(tag, …) can advance its state. The immutable half of the record (vk, schemas, bootstrap) is locked at registration; the mutable half (root, epoch) advances on every apply.
- vk_digest
- vk_digest_fr
- pi_schema_hash
- extras_arity
- bootstrap_root
- registrant
- current_root
- epoch
§3 · the apply pipeline
Eleven ordered checks. Three of them are cheap. One is expensive.
The body of apply(tag, new_root, expected_epoch, proof, extras) in order. Cheap pre-verifier checks fail fast so that a bad transaction does not pay for the BLS12-381 pairing call at step 8. Click any step to toggle its fail mode and see the corresponding revert code.
RelationState(tag)
Order matters. Steps 1, 3, 4 are constant-time bound/lookup checks — Soroban refunds nothing, but a bad caller never pays for the pairing. Steps 9–11 are unconditional once step 8 returns true: the contract never decides whether to advance — it advances iff the proof is valid for (tag, expected_epoch, extras).
§4 · the fixed-5 public-input vector
Per-relation extras of any arity collapse into one field element.
The verifier sees the same five-element PI vector for every relation registered under this SEP. Only the verifying key differs. Per-call extras of arbitrary arity fold into r_ext through a Poseidon2 absorption tagged with EXTRAS_TAG_FR.
max arity at the contract boundary: 32 (1024 bytes). Above realistic needs; below the gas-cost knee.
absorb [EXTRAS_TAG_FR, N, extras…] → squeeze s₀ → r_ext
Every relation. Same 5 slots. Only the vk differs.
The fold has one job: keep the verifier's public-input shape constant across every relation registered under this SEP, so that the on-chain pairing is a single, schema-fixed call. Per-relation discipline (witness, predicate, replay) lives inside the circuit, not in the contract.
§5 · the Fiat–Shamir transcript
Eight slots. Order is normative.
The transcript pre-binds the verifying key, the schema, the tag, the public-input vector, and the per-call extras digest before the prover's first commitment is absorbed. Any permutation of slots breaks soundness; click swap → next to any slot to swap with its right neighbour and trip the corresponding conformance vector.
The eight transposition vectors — seven linear (tx_swap_0_1 … tx_swap_6_7) plus one cyclic wraparound (tx_swap_wrap_7_0) — are the SEP's transcript-discipline KAT set. A conformant verifier rejects all eight. A verifier that accepts any of them is, by definition, off-spec.
§6 · replay protection · two layers
Proof-bytes replay is the contract's problem. Witness-level replay is the predicate's.
Bisecting the replay surface is the architectural choice. The contract enforces the cheap, universal layer; the relation circuit enforces the expensive, relation-specific one. The contract does not track nullifiers.
Proof-bytes replay → fails at step 4 (EpochMismatch).
The same proof bytes resubmitted after a successful apply bind to a stale expected_epoch. State has already advanced; the pre-verifier check rejects before the pairing.
expected_epoch is in the PI vector. Fiat–Shamir binds the entire transcript, including the epoch field element, into every challenge. A proof for epoch 7 is cryptographically distinct from a proof for epoch 8 — the contract just has to read the epoch out and compare.
Witness-level replay → two patterns. Pick one inside R_Upd.
acc_root_new = insert(acc_root_old, nullifier(witness))
carry acc_root_old, acc_root_new in r_ext; circuit proves the insertion alongside the membership update.
The contract does NOT track nullifiers. Soroban archival semantics make a contract-side nullifier set unsound under operational drift — an archived nullifier entry would silently widen the spent-set during a restore window. Pushing witness-level replay into the predicate is the architectural choice.
§7 · two-leaf-update · the LCA constraint
Two Merkle paths must agree at and above their LCA.
For relations using the in-tree spent-slot pattern, a single step updates two leaves: the admitter's (LIVE → SPENT) and an empty slot (∅ → NEW). Each leaf carries its own sibling path. The two paths must be witness-compatible: identical above the lowest common ancestor (LCA), independent below. Drag either marker.
For two distinct leaves at idx_a, idx_e in a depth-D tree, let h_LCA be the height of their lowest common ancestor. The two-witness compatibility constraint is . Three regions: independent at levels 0 ≤ ℓ ≤ h_LCA − 2 (disjoint subtrees, siblings share nothing); cross-related at level ℓ = h_LCA − 1 (each path references the other leaf's level-(h_LCA−1) ancestor — recomputed subtree roots, not equal in general); equal at levels h_LCA ≤ ℓ ≤ D − 1 (shared ancestors, hence shared siblings; bit-identical).
§8 · TTL discipline · archival
Every apply bumps both entries to network-max. Unconditionally.
The contract does not decide whether a relation is "active enough" to stay alive. Each apply extends both persistent entries (Relation + RelationState) to network_max. For relations that go quiet, bump_ttl(tag) is permissionless: anyone pays gas to keep them alive.
Archived entries block all future applys until restoreFootprint is included in the calling transaction. The contract bumps TTL before emitting the applied event, so a successful apply also pays the rent for the next quiet stretch.
§9 · immutability · version-locality
There is no rotate_vk. Upgrades produce a new tag.
Once a tag is registered, its (vk, vk_digest, pi_schema_hash, bootstrap_root) is locked. The contract surface admits no method that mutates them. An upgrade — bug fix, predicate refinement, new field — is a new registration under a new tag. The whitepaper's soundness theorem is version-local across them.
- vk
- VK_anarchy@v1
- vk_digest
- 0x4a..cf
- bootstrap_root
- 0xfe..00
- current_root
- 0x9c..b1 · at epoch 4032
- vk
- VK_anarchy@v2 · new circuit
- vk_digest
- 0xc1..7d
- bootstrap_root
- 0x9c..b1→migrated
- current_root
- 0x9c..b1 · epoch 0
The prior current_root is migrated in as the new bootstrap_root. State is preserved; semantics are not — v2 is a distinct relation, with its own soundness argument. Old proofs do not verify against v2; new proofs do not verify against v1. Version-locality of the soundness theorem is made explicit at the contract surface.
§10 · scope of the soundness theorem
The SEP specifies a host. The whitepaper proves one predicate.
Every relation registered under this SEP is a member of the outer set — the contract surface accepts it. Exactly one relation, so far, sits in the inner set — its soundness is proven end-to-end in the companion whitepaper. Hover any outer-only predicate to see what stands between it and the inner set.
Every non-admission relation registered under this SEP requires its own soundness argument under the same compositional chain (whitepaper §§3–7) before being relied on in production.
Whitepaper coverage is admission only. The SEP says nothing about whether removal, key rotation, or liveness-tick are sound — only that if a relation circuit binds to the fixed PI vector and the contract verifies one proof per step, replay and proof-bytes discipline come for free. Predicate-level discipline does not.
§11 · cryptographic foundations
Pinned conventions. No room for interpretation.
A conformant implementation matches these byte for byte. Conformance vectors in the SEP's KAT set cover every parameter pin below.
Algebraic hash · over F_r of BLS12-381
- t
- 3
- α
- 5 (x↦x⁵)
- R_F
- 8 (full rounds)
- R_P
- 56 (partial rounds)
- rate r
- 2
- capacity c
- 1
- IV
- all-zero
- absorb
- additive
- padding
- zero-pad odd inputs
- squeeze
- s₀ (single field element)
Big-endian. No reduction.
- byte→Fr
- 31-byte BE chunks
- final chunk
- right-zero-pad to 31 bytes
- reduction
- none · chunks are < 2²⁴⁸ < r
- Fr serialise
- 32 bytes big-endian (CAP-0059)
- MAX_EXTRAS_ARITY
- 32 field elements (1024 bytes)
- extras encoding
- Vec<Fr> on the wire
Five domain separators, pinned in v0.5.0 (Appendix A).
| constant | used by | shape | value |
|---|---|---|---|
| TRANSCRIPT_TAG_FR | FS slot 0 · domain-separate the AGMK transcript | 1 Fr | v0.5.0 · 32B BE |
| VK_DIGEST_TAG_FR | FS slot 1 · vk_digest_fr = H(VK_DIGEST_TAG_FR, vk_digest), cached in RelationRecord at registration | 1 Fr | v0.5.0 · 32B BE |
| TAG_TAG_FR | PI slot 3 · t_Fr = H(TAG_TAG_FR, tag) | 1 Fr | v0.5.0 · 32B BE |
| EXTRAS_TAG_FR | PI slot 4 · r_ext = H(EXTRAS_TAG_FR, extras) | 1 Fr | v0.5.0 · 32B BE |
| SCHEMA_TAG_FR | pi_schema_hash construction · binds schema to vk | 1 Fr | v0.5.0 · 32B BE |
Until v0.5.0 the constants are deferred — Appendix A reserves the slots and the construction is normative; the byte-level pinning lands with the freeze.
§12 · revisions
Eight iterations. The interface has been pressure-tested.
Reviewers can see the design has moved. Each line summarises one revision; the contract surface is the same shape now as in v0.3.0, but the discipline around it (TTL, replay split, two-leaf LCA) has tightened.