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.

register_relation· apply· bump_ttl 9 read-only views · 0 admin methods
0
privileged methods
3
mutating methods
5
PLONK public inputs
8
FS transcript slots
0
contract-tracked nullifiers
1
immutable registration per tag

§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.

NO ADMIN · no init · no pause · no upgrade · no privileged caller

Nine read-only views

Per tag, plus one global. None of these mutates state. None requires authentication.

current_root(tag)Field · advances with each apply
current_epoch(tag)u64 · monotone counter
vk_digest(tag)BytesN<32> · immutable
vk_digest_fr(tag)Field · immutable · FS slot 1
bootstrap_root(tag)Field · immutable
pi_schema_hash(tag)BytesN<32> · immutable
extras_arity(tag)u32 · immutable
registrant(tag)Address · audit only
is_registered(tag)bool · existence probe
protocol_version()u32 · contract-wide

§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.

contract C…AGMK · soroban / wasm 4 sample tags · illustrative
caveat Tag presence on chain ≠ trust attestation. Registration is permissionless; anyone can register any tag. Users verify (vk_digest, pi_schema_hash, bootstrap_root) against the relation author's published artifacts out of band before trusting any tag. The contract is an interface, not a registry of approved governance.

§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.

step / 11 ·
cheap (steps 1, 3, 4) expensive · 1 pairing (step 8)

RelationState(tag)

before · current_root
after · current_root
before · epoch
after · epoch
public-input vector · assembled at step 7
[0]C_g
[1]C_g'
[2]e_Fr
[3]t_Fr
[4]r_ext

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.

left · extras vector · arity N
N =

max arity at the contract boundary: 32 (1024 bytes). Above realistic needs; below the gas-cost knee.

center · Poseidon2 sponge · r=2 · c=1
absorb → EXTRAS_TAG_FR extras[0] extras[1] extras[2] extras[3] extras[4] extras[5] extras[6] extras[7] Poseidon2 t=3 · α=5 · R_F=8 · R_P=56 s₀ (rate) s₁ (rate) s₂ (cap) IV = 0 · additive · pad-1 squeeze s₀ r_ext

absorb [EXTRAS_TAG_FR, N, extras…] → squeeze s₀ → r_ext

right · public-input vector · fixed 5 slots
[0]C_gcurrent_root
[1]C_g'new_root
[2]e_Frexpected_epoch
[3]t_FrH(TAG_TAG_FR, tag)
[4]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_1tx_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.

layer 1 · contract-enforced

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.

submission 1 proof π · epoch = 7 verify · advance · epoch→8 applied · state.epoch = 8 root advanced submission 2 · same bytes proof π · epoch = 7 stale · still binds epoch 7 REVERT · EpochMismatch step 4 · expected 7 ≠ state 8

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.

layer 2 · predicate-enforced

Witness-level replay → two patterns. Pick one inside R_Upd.

pattern (a) · preferred · in-tree spent-slot
before apply LIVE admitter empty slot apply after apply · single step SPENT admitter NEW new member second attempt: slot already SPENT · R_Upd rejects
pattern (b) · external accumulator

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.

D = 4 · 16 leaves (illustrative; spec is parametric) idx_a = idx_e = h_LCA =
root (h=4) h=3 h=2 h=1 leaves (h=0)
path_a · admitter (idx_a, LIVE→SPENT) path_e · empty slot (idx_e, ∅→NEW) must-be-equal · siblings at and above LCA LCA · lowest common ancestor

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.

timeline · single relation life
TTL · network_max 0

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.

onym_admit_anarchy_v1
▣ locked at registration
vk
VK_anarchy@v1
vk_digest
0x4a..cf
bootstrap_root
0xfe..00
current_root
0x9c..b1 · at epoch 4032
onym_admit_anarchy_v2
▣ locked at registration
vk
VK_anarchy@v2 · new circuit
vk_digest
0xc1..7d
bootstrap_root
0x9c..b1migrated
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.

PREDICATES THE CONTRACT ACCEPTS any R_Upd-shape · fixed 5-slot PI WHITEPAPER PROVES admission Theorem 1 · §4 removal e.g. onym_remove_quorum_v1 key rotation e.g. onym_rotate_key_v1 liveness-tick e.g. onym_liveness_v1
predicate frontier
admissionproven
removalrequires argument
key rotationrequires argument
liveness-tickrequires argument

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.

poseidon2

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)
byte → Fr / Fr serialisation

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
tag-Fr constants · prevent cross-context collisions

Five domain separators, pinned in v0.5.0 (Appendix A).

constantused byshapevalue
TRANSCRIPT_TAG_FRFS slot 0 · domain-separate the AGMK transcript1 Frv0.5.0 · 32B BE
VK_DIGEST_TAG_FRFS slot 1 · vk_digest_fr = H(VK_DIGEST_TAG_FR, vk_digest), cached in RelationRecord at registration1 Frv0.5.0 · 32B BE
TAG_TAG_FRPI slot 3 · t_Fr = H(TAG_TAG_FR, tag)1 Frv0.5.0 · 32B BE
EXTRAS_TAG_FRPI slot 4 · r_ext = H(EXTRAS_TAG_FR, extras)1 Frv0.5.0 · 32B BE
SCHEMA_TAG_FRpi_schema_hash construction · binds schema to vk1 Frv0.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.

v0.1.0 first sketch · single registry · admission only · contract-tracked nullifier set
v0.2.0 generalised to R_Upd · removed contract-side nullifiers · pushed witness replay into predicate
v0.3.0 fixed-5 PI vector · per-relation extras fold into r_ext via Poseidon2
v0.4.0 FS 8-slot transcript order normative · 8 transposition KATs added
v0.4.1 TTL discipline · unconditional bump-to-max on every apply · bump_ttl permissionless
v0.4.2 two-leaf-update LCA constraint made explicit · matches in-tree spent-slot pattern
v0.4.3 LCA range fixed to h_LCA ≤ ℓ ≤ D−1 · Soroban trait → struct/impl · vk_digest_fr cached in RelationRecord · transcript vectors split into 7 linear + tx_swap_wrap_7_0 · Appendix A non-implementability notice
v0.5.0 deferred · Appendix A tag-Fr constants pinned · KAT set frozen