[SEP <TBD>]: Private Group Membership on Stellar: A Zero-Knowledge Approach #1903
Replies: 2 comments 2 replies
-
PreambleSimple SummaryA standard for anchoring cryptographic group membership state on Stellar using a commitment scheme and a zero-knowledge membership proof, such that no observer of the ledger can determine who is in a group or who is updating its state. Dependencies
MotivationOn-chain group registries can store membership lists in plaintext: After a contract implementing this standard is deployed:
Target use cases
Specification1. Definitions
1.1 Member Identity KeysMember identity keys are BLS12-381 G1 keypairs: the private key is a scalar Rationale. Ed25519 key ownership verification inside a BLS12-381 Groth16 circuit requires non-native field arithmetic emulation (Curve25519 operates over a different prime field), resulting in 500,000+ R1CS constraints for a single scalar multiplication. BLS12-381-native keys reduce key ownership to a single native-field scalar-mul — roughly 1,000 constraints. Stellar address binding. Applications that require a link between a member's BLS12-381 identity key and their Stellar Ed25519 address MUST use an off-chain key attestation: the member signs their BLS12-381 public key with their Ed25519 private key and distributes this attestation to the group via the application's encrypted channel. The attestation is verified by other members at registration time, not inside the ZK circuit. This keeps the circuit small without sacrificing the ability to trace a BLS12-381 key back to a Stellar account when the holder consents. The attestation format is: This attestation is a group-level artifact shared among members. It is never submitted on-chain. 2. Commitment Construction (Dual-Hash Scheme)The commitment scheme uses two hash functions for different roles:
This separation gives the circuit a small constraint count while preserving the ability to verify commitments on-chain using a native host function. 2.1 Member orderingBefore building the Merkle tree, members MUST be sorted in ascending lexicographic byte order of their compressed G1 representation (48 bytes). Membership is a set — the commitment must be deterministic regardless of insertion order. 2.2 Poseidon Merkle treeThe sorted member keys are placed as leaves of a binary Merkle tree of fixed depth The hash function for both leaf hashing and internal nodes is Poseidon over the BLS12-381 scalar field with the following parameters: Each leaf is computed as: where Internal nodes are: The tree root is 2.3 On-chain commitment valuewhere: Total preimage: 72 bytes. No padding, no separators, no length prefix (the structure is fixed-length). SHA-256 is chosen for the outer binding because it is available as a Soroban host function ( 2.4 Salt lifecycleThe salt is a group secret shared exclusively among current members via the application's encrypted channel. It is never published on-chain. A fresh 32-byte random salt MUST be generated by the Commit initiator for every epoch transition, ensuring that knowledge of salt 2.5 Salt recoveryIf a member loses the salt for the current epoch, they cannot generate proofs or verify the commitment locally. The recovery procedure is:
If no other member is reachable, the member cannot participate until the next epoch transition, at which point a new salt is distributed. Applications SHOULD design their salt distribution to be robust against single points of failure (e.g., every member stores the current salt and can re-share it). 3. Zero-Knowledge Membership Proof3.1 Proof statementThe prover demonstrates the following statement to the contract without revealing any witness:
Formal relation: 3.2 Proof systemGroth16 SNARK over BLS12-381. Rationale:
Groth16 requires a circuit-specific trusted setup. See Section 9. 3.3 Circuit structureThree constraints are encoded in the circuit: Constraint 1 — key ownership (BLS12-381 native) Compute Assert: Constraint 2 — Poseidon Merkle membership The prover supplies a Merkle opening proof consisting of Assert: At each level, Poseidon is applied to the pair Constraint 3 — commitment binding (SHA-256, fixed-length) The circuit recomputes Assert: Total circuit size estimates by tier:
The circuit scales logarithmically with group size. SHA-256 dominates the constraint count at all tiers, but the fixed 72-byte preimage keeps it bounded to a single compression call. 3.4 Public inputs and proof wire3.5 Verification in SorobanThe contract verifies the Groth16 proof using the BLS12-381 host functions introduced in Protocol 22: where The contract does NOT compute the SHA-256 of the member list during verification — that computation happens inside the ZK circuit. The contract only:
This means the contract never learns the member list, the salt, or the identity of the prover. 3.6 Proof size and cost
The constant verification cost is a core property. A 2,048-member group is indistinguishable from a 2-member group from the contract's perspective. 4. Contract InterfaceImplementations of this SEP MUST expose the following interface. The normative specification is the interface and its invariants; implementation language is non-normative.
|
| Name | Type | Description |
|---|---|---|
group_id |
Bytes |
Application-defined group identifier |
commitment |
BytesN<32> |
Commitment for epoch 0 |
proof |
Bytes |
ZK proof that the caller knows the preimage of the commitment (see Section 4.1) |
public_inputs |
PublicInputs |
{commitment, epoch: 0} |
tier |
u32 |
Circuit tier: 0 = Small (32), 1 = Medium (256), 2 = Large (2048) |
Invariants:
group_idMUST NOT already exist in storage- Proof MUST verify against the verification key for the specified tier
- Emits a
GroupCreatedevent
4.1 Semantics of the epoch-0 proof
At epoch 0, no prior group state exists. The ZK proof submitted with create_group proves the following: the caller knows a secret key sk and a salt s such that sk · G1 is a leaf in the Poseidon Merkle tree whose root, combined with epoch 0 and salt s, hashes to the submitted commitment. This is the same circuit and statement used for all subsequent epochs — the creator is proving they are a member of the set they are committing, not that the set existed before.
This means the creator cannot register a group containing only other people's keys without also including their own. Any party who knows the salt and a valid member key can create the group; the contract does not privilege the creator in any way after creation.
update_commitment
Transitions a group to a new epoch with a new commitment.
Parameters:
| Name | Type | Description |
|---|---|---|
group_id |
Bytes |
Identifies the group |
new_commitment |
BytesN<32> |
Commitment for the new epoch |
new_epoch |
u64 |
MUST equal stored_epoch + 1 |
proof |
Bytes |
ZK proof that the caller is a member of the current epoch's committed set |
public_inputs |
PublicInputs |
{commitment: current_commitment, epoch: current_epoch} |
Invariants:
group_idMUST existnew_epoch == stored_epoch + 1— strict monotonicity enforced- Proof is verified against the current commitment, not the new one. This proves the updater is a legitimate member before the transition.
- Emits a
CommitmentUpdatedevent
verify_membership
A read-only call that verifies a ZK proof of membership against the current state.
Parameters:
| Name | Type | Description |
|---|---|---|
group_id |
Bytes |
Identifies the group |
proof |
Bytes |
ZK proof |
public_inputs |
PublicInputs |
{commitment, epoch} |
Returns: bool
This function is a pure verification — no state change, no XLM cost when called via simulateTransaction.
deactivate_group
Marks a group as inactive, preventing further epoch transitions and allowing storage to be reclaimed.
Parameters:
| Name | Type | Description |
|---|---|---|
group_id |
Bytes |
Identifies the group |
proof |
Bytes |
ZK proof that the caller is a member of the current epoch's committed set |
public_inputs |
PublicInputs |
{commitment: current_commitment, epoch: current_epoch} |
Invariants:
group_idMUST exist and MUST NOT already be deactivated- Proof MUST verify against the current commitment
- After deactivation,
update_commitmentcalls for thisgroup_idMUST be rejected verify_membershipandget_stateremain functional (the last committed state is preserved)- Emits a
GroupDeactivatedevent - Persistent storage entries for epoch history MAY be reclaimed by the contract after deactivation
get_state
Returns the current CommitmentEntry for a group.
CommitmentEntry {
commitment: BytesN<32>
epoch: u64
timestamp: u64 -- ledger timestamp of last update
tier: u32 -- circuit tier
active: bool -- false if deactivated
}
Note: no committer address is stored. The ZK proof decouples the transaction signer from group identity.
get_history
Returns the most recent N CommitmentEntry records for a group, ordered by epoch, where N is capped by the history_window parameter.
Parameters:
| Name | Type | Description |
|---|---|---|
group_id |
Bytes |
Identifies the group |
max_entries |
u32 |
Maximum number of entries to return, capped at history_window |
The contract SHOULD maintain a rolling window of the last history_window entries (default: 64). Older entries are pruned from persistent storage on each update_commitment call. Applications that need full history MUST reconstruct it from CommitmentUpdated events emitted by the contract.
Rationale. An unbounded append-only history in contract storage accumulates rent indefinitely and exposes the complete temporal fingerprint of a group's activity. A rolling window bounds storage cost while preserving enough state for recent dispute resolution. Events provide the complete audit trail for applications that need it.
5. Transaction Submission and Fee Decoupling
The ZK proof ensures that the submitter is a valid group member without revealing which member. However, if the member submits the transaction from their own Stellar account, the fee payer address in the transaction envelope re-links a Stellar identity to group activity — partially defeating the purpose of the ZK proof.
Implementations MUST support and SHOULD encourage one of the following fee decoupling strategies:
5.1 Relayer pattern (RECOMMENDED)
A relayer is a public service that accepts pre-signed Soroban invocations and wraps them in a transaction envelope using the relayer's own Stellar account as the fee source.
The relayer workflow:
- The group member constructs a signed Soroban
InvokeHostFunctionoperation containing theupdate_commitment(orcreate_group) call with the ZK proof. - The member submits the signed operation to the relayer via an anonymous transport (e.g., Tor, a public HTTPS endpoint with no authentication).
- The relayer wraps the operation in a transaction, sets itself as
source_account, signs the transaction envelope, and submits to the Stellar network. - The relayer pays the transaction fee. Fee reimbursement, if any, happens out-of-band.
The relayer does not need to be trusted with any secret material. It sees the proof (which reveals nothing about the prover) and the group_id (which is opaque). It cannot determine which member is submitting the update.
A reference relayer specification is out of scope for this SEP but is expected as a companion document.
5.2 Shared fee account
The group maintains a Stellar account funded by members via anonymous or batched deposits. All update_commitment transactions are submitted from this shared account. Members share the signing key for this account via the encrypted channel.
This approach is simpler but has weaker privacy properties: the shared account is publicly associated with the group_id, and funding patterns may leak information.
5.3 Fee sponsorship via Soroban authorization
If future Soroban protocol versions introduce native fee sponsorship (where an invoker and fee payer can be distinct accounts in a single transaction), implementations SHOULD adopt that mechanism. This SEP will be updated to reference the relevant protocol version when available.
6. Events
Implementations MUST emit the following contract events for indexers.
GroupCreated
topics: ["GroupCreated", group_id]
data: { commitment: BytesN<32>, epoch: 0, tier: u32, timestamp: u64 }
CommitmentUpdated
topics: ["CommitmentUpdated", group_id]
data: { commitment: BytesN<32>, epoch: u64, timestamp: u64 }
GroupDeactivated
topics: ["GroupDeactivated", group_id]
data: { final_epoch: u64, timestamp: u64 }
No member identity appears in any event data.
7. Salt Distribution Protocol
The salt for each epoch must reach all current group members without appearing on-chain. This SEP is intentionally agnostic about the transport layer. Two compliant approaches are described:
Approach A — Encrypted group messaging layer (RECOMMENDED)
The salt is embedded as a custom extension in the group's application-layer key agreement protocol (e.g., MLS RFC 9420 GroupContext extension type 0xFF01). The extension is authenticated by the protocol's existing Commit signature, travels through the encrypted channel, and is decrypted only by current members.
Approach B — Direct encrypted delivery
The Commit initiator encrypts salt to each member's BLS12-381 public key (using ECIES over BLS12-381 G1) and delivers the ciphertext via any out-of-band channel. Members decrypt and store the salt locally.
In both cases, the receiving member MUST verify locally:
poseidon_root = MerklePoseidonTree(sorted_members).root
local_commitment = SHA-256(poseidon_root || epoch || salt)
assert local_commitment == contract.get_state(group_id).commitment
If the assertion fails, the member MUST NOT accept the epoch transition and SHOULD alert the user.
For salt recovery procedures when a member loses the current salt, see Section 2.5.
8. Circuit Tiers and Trusted Setup
8.1 Standard circuit tiers
This SEP defines three standard circuit tiers. Each tier corresponds to a fixed Groth16 circuit with a specific maximum group size, tree depth, and independent trusted setup.
| Tier | Identifier | MAX_MEMBERS | Tree depth d |
Approx. constraints |
|---|---|---|---|---|
| Small | 0 | 32 | 5 | ~27,500 |
| Medium | 1 | 256 | 8 | ~28,400 |
| Large | 2 | 2,048 | 11 | ~29,300 |
A group is bound to a single tier at creation time (the tier parameter in create_group). The contract stores the verification key for each supported tier and uses the appropriate key during proof verification.
Applications SHOULD choose the smallest tier that accommodates their expected group size. Unused leaf slots in the Merkle tree are filled with zero-valued leaves and do not affect proof validity or privacy.
If a group outgrows its tier, the application MUST create a new group at a higher tier and migrate members. The old group SHOULD be deactivated.
8.2 Trusted setup ceremony
Groth16 requires a circuit-specific trusted setup ceremony producing a proving key pk and verification key vk. The security guarantee is: the setup is sound if at least one participant honestly discards their toxic waste.
A separate MPC ceremony MUST be conducted for each circuit tier before any mainnet deployment of a contract implementing this standard. Each ceremony MUST:
- Use the Powers of Tau format compatible with
snarkjsorbellman - Include a minimum of 10 independent participants
- Publish all contribution hashes and attestations publicly
- Derive the final verification key from the last contribution
Each circuit is uniquely identified by:
circuit_id = SHA-256("SEP-XXXX" || tier || tree_depth || poseidon_params_hash)
The verification keys for all supported tiers are stored in the Soroban contract at deployment time and are immutable. A contract upgrade that changes any verification key constitutes a new deployment and requires a new ceremony.
The proving keys are distributed to client applications. They are public — possession of a proving key does not endanger security.
9. Security Analysis
9.1 What an on-chain observer learns
From the ledger, an observer can determine:
- That a group identified by
group_idexists - How many times the group's membership changed (epoch count)
- The timestamp of each change (within the rolling history window)
- The circuit tier, which reveals an upper bound on group size (32, 256, or 2,048)
- A 192-byte proof blob and 32-byte commitment per epoch — neither reveals membership or prover identity
An observer cannot determine:
- How many members are in the group (only the tier upper bound)
- The identity of any member
- Which member submitted any given update
- Whether membership grew or shrank in a given epoch
- The identity of the group creator (if
group_idis derived as specified in Section 1)
9.2 Commitment hiding
The commitment is SHA-256(poseidon_root || epoch || salt). Given a fresh 32-byte random salt and SHA-256's preimage resistance, an observer cannot recover the Poseidon root (or the underlying member set) without 2^256 operations. The salt prevents offline dictionary attacks even when the universe of possible members is small.
The Poseidon Merkle tree adds a second layer of hiding: even if an attacker somehow obtained the Poseidon root, recovering the individual leaves requires breaking Poseidon's preimage resistance.
9.3 ZK soundness
Groth16 is computationally sound under the knowledge-of-exponent assumption over BLS12-381. A party without a valid witness (i.e., a non-member) cannot produce an accepting proof except with negligible probability.
9.4 ZK zero-knowledge
Groth16 is zero-knowledge: the proof reveals nothing about the witness beyond the validity of the statement. In particular, the prover's leaf index in the Merkle tree and their public key pk are not recoverable from the proof or the public inputs.
9.5 Epoch monotonicity
The contract enforces new_epoch == stored_epoch + 1. This prevents replay attacks (resubmitting an old proof for a past epoch) and fork attacks (two conflicting epoch-N commitments).
9.6 Proof binds to current state
The ZK proof in update_commitment is verified against the current stored commitment, not the new one. This means the updater must prove membership in the group as it existed before the transition — they cannot unilaterally forge a new membership set without holding a valid current member key.
9.7 Fee payer correlation
If the transaction fee payer is the same Stellar account as a group member, an observer can correlate that address with group activity over time, partially defeating the ZK proof's privacy guarantee. Section 5 defines normative mitigations. The relayer pattern (Section 5.1) eliminates this correlation entirely. Implementations that do not implement fee decoupling MUST document this as a known privacy limitation.
9.8 Tier upper bound leakage
The circuit tier reveals an upper bound on group size (e.g., a tier-0 group has at most 32 members). Applications for which this is sensitive SHOULD use a higher tier than strictly necessary.
9.9 Residual leakage summary
| Observable | Severity | Mitigation |
|---|---|---|
group_id existence |
Low | Derive group_id as a hash (Section 1) |
| Epoch frequency and timing | Low | Reveals activity patterns, not identity |
| Transaction fee payer | Medium if unmitigated | Relayer pattern (Section 5.1) — normative |
| Circuit tier (group size upper bound) | Low | Use a higher tier than necessary |
| Proof size is constant | Positive | Group size is not inferrable |
| History window length | Low | Rolling window bounds exposure (Section 4) |
10. Test Vectors
Implementations MUST produce the following commitment values from the given inputs. These vectors allow independent validation of the Poseidon Merkle tree construction, the dual-hash commitment, and the canonical serialization.
Note: Poseidon hash outputs below use the BLS12-381 scalar field parameters specified in Section 2.2. All byte strings are hex-encoded.
Vector 1 — epoch 0, 2 members, tier Small
members (compressed G1, sorted):
member_0 = 0x010101...01 (48 bytes, all 0x01)
member_1 = 0x020202...02 (48 bytes, all 0x02)
Poseidon Merkle tree (depth 5, 32 leaves):
leaf[0] = Poseidon(member_0_x)
leaf[1] = Poseidon(member_1_x)
leaf[2..31] = 0x00...00 (zero leaves)
poseidon_root = [to be filled by reference implementation]
salt = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
epoch = 0x0000000000000000
commitment_preimage (72 bytes):
poseidon_root (32 bytes) || epoch (8 bytes) || salt (32 bytes)
commitment = SHA-256(commitment_preimage)
= [to be filled by reference implementation]
Vector 2 — epoch 1, 3 members, tier Small
members (compressed G1, sorted):
member_0 = 0x010101...01 (48 bytes)
member_1 = 0x020202...02 (48 bytes)
member_2 = 0x030303...03 (48 bytes)
Poseidon Merkle tree (depth 5, 32 leaves):
leaf[0..2] = Poseidon(member_i_x)
leaf[3..31] = 0x00...00
poseidon_root = [to be filled by reference implementation]
salt = 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
epoch = 0x0000000000000001
commitment = SHA-256(poseidon_root || epoch || salt)
= [to be filled by reference implementation]
Vector 3 — sort order enforcement
Identical inputs to Vector 2 but with member_0 and member_2 swapped in the input (wrong order). After sorting, the Poseidon Merkle tree and commitment MUST be identical to Vector 2. Any implementation that produces a different commitment has a sorting bug.
11. Rationale
Why a dual-hash scheme (Poseidon + SHA-256) rather than SHA-256 throughout?
SHA-256 inside a Groth16 R1CS circuit costs ~25,000 constraints per compression call. In the v0.1 draft, the circuit hashed the entire canonical member list under SHA-256, making circuit size grow linearly with group size — a 100-member group required multiple compression calls totaling hundreds of thousands of constraints just for the hash.
The dual-hash scheme confines SHA-256 to a single fixed-length (72-byte) compression call for the outer commitment binding, and uses Poseidon (~300 constraints per hash) for the Merkle tree. This makes the in-circuit cost logarithmic in group size (one Poseidon call per tree level) while preserving on-chain verifiability via the SHA-256 host function.
Why BLS12-381 identity keys rather than Ed25519?
Ed25519 key ownership verification inside a BLS12-381 Groth16 circuit requires emulating Curve25519 field arithmetic in a non-native field, costing 500,000+ constraints for a single scalar multiplication. BLS12-381 native keys reduce this to ~1,000 constraints. The Stellar Ed25519 address link is preserved via an off-chain attestation (Section 1.1) that is verified at group registration, not on every proof.
Why a Poseidon Merkle tree rather than hashing the flat member array?
The v0.1 draft included both a flat-array hash (for commitment binding) and a Merkle tree (for membership proof) — a redundancy, since the flat hash already required the full member array as witness. By making the Poseidon Merkle root the canonical representation of the member set, the circuit only needs a logarithmic-depth Merkle opening proof. The prover supplies only their leaf and d sibling hashes, not the full member list. This eliminates the redundancy and makes proving time sublinear in group size.
Why Groth16 rather than PLONK or STARKs?
Protocol 22 provides BLS12-381 pairing operations, which directly accelerate Groth16 verification. PLONK also uses pairings but requires polynomial commitment verification not yet available as host functions. STARKs are pairing-free but produce larger proofs and have no dedicated host function acceleration. Groth16 is the best fit for the current Soroban host environment.
Why define standard circuit tiers?
Groth16 circuits are fixed at setup time. Without standard tiers, every application would define its own MAX_MEMBERS and conduct its own trusted setup, fragmenting the ecosystem. Standard tiers allow shared trusted setup ceremonies, shared proving keys, and interoperable tooling.
Why is there no committer field in CommitmentEntry?
Storing the committer address would partially defeat the purpose of the ZK proof — an observer could still correlate Stellar addresses with group activity over time. The ZK proof is sufficient authorization. Fee decoupling (Section 5) ensures the fee payer is not linkable to the group member.
Why a rolling history window rather than unbounded append-only history?
Unbounded on-chain history accumulates Soroban storage rent indefinitely and exposes the complete temporal fingerprint of a group. A rolling window bounds both cost and leakage. The complete audit trail remains available via contract events, which are stored by Horizon indexers and do not incur persistent contract storage rent.
Why enforce strict epoch monotonicity rather than allowing out-of-order updates?
Strict monotonicity provides a simple, auditable invariant: the history is a linear sequence. It prevents replay, fork, and interleaving attacks without requiring the contract to maintain a nonce map per member. Applications requiring concurrent or parallel group state machines should use separate group_id values.
12. Changelog
| Version | Date | Notes |
|---|---|---|
| 0.0.1 | 2026-03-30 | Initial draft |
Beta Was this translation helpful? Give feedback.
-
|
reference impl https://github.com/rinat-enikeev/stellar-mls |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Preamble
1. Introduction
This document accompanies SEP-XXXX and provides a high-level overview of the proposal: a standard for private group membership registries on the Stellar network. The full specification defines a Soroban smart contract interface that allows groups to manage membership on-chain without ever revealing who is in the group or who is modifying it.
The core mechanism is a combination of a cryptographic commitment scheme and a zero-knowledge proof (Groth16 over BLS12-381), leveraging the pairing-friendly curve host functions introduced in Stellar Protocol 22.
2. Background
Group-based coordination is foundational to many decentralized applications — encrypted messaging, DAO governance, credential issuance, multi-party signing. All of these require some notion of "who is in this group right now," and that notion must be shared, verifiable, and resistant to forgery.
Blockchains are a natural place to anchor group state: they provide global ordering, immutability, and permissionless auditability. Stellar's Soroban smart contract platform makes this practical with low fees, fast finality, and — since Protocol 22 — native support for BLS12-381 elliptic curve operations, which are the building block for modern zero-knowledge proof verification.
The Messaging Layer Security protocol (MLS, RFC 9420) is a particularly relevant motivating case and the primary design driver for this SEP. MLS manages group encryption keys through a tree-based ratcheting structure, where each membership change (a "Commit") produces a new cryptographic group state. This state must be ordered and consistent across all members — exactly the guarantee a blockchain provides. Anchoring MLS group state on-chain creates a tamper-proof, globally-ordered log of group transitions that removes the need for a trusted central delivery service. But this only works if the on-chain record doesn't destroy the privacy that end-to-end encryption was designed to protect. The commitment-plus-ZK-proof scheme in this SEP is designed specifically to serve as that on-chain anchor without leaking the social graph that MLS keeps hidden.
3. Problem
Existing approaches to on-chain group state expose two categories of information that should remain private:
The membership roster. A plaintext member list (
group → [A, B, C]) permanently publishes the social graph of every group to any network observer. Even storing a hash of the list only helps partially — if the universe of possible members is known, the hash can be brute-forced or the list can be reconstructed from observed interactions.The updater's identity. Every Soroban invocation is wrapped in a transaction signed by a Stellar account. Even if the member list is hidden behind a hash, the address that submits each
updatecall is visible. Over time, an observer correlates these addresses with group activity, recovering a partial social graph through traffic analysis.These two leaks compound. Together, they make it possible to determine not just that a group exists, but who is in it, who is active, and how the group evolves — exactly the information that private communication and anonymous coordination systems exist to protect.
4. Scope
In scope
Out of scope
5. Solution
The SEP introduces a contract that stores, for each group, only three values: a 32-byte commitment, an epoch counter, and a circuit tier identifier. No member identity ever touches the ledger.
Dual-hash commitment. Member keys are organized into a Poseidon Merkle tree (a ZK-friendly hash requiring ~300 constraints per evaluation). The tree root is then bound to the epoch and a random salt via SHA-256, producing the on-chain commitment. Poseidon keeps the ZK circuit small; SHA-256 keeps on-chain verification native to Soroban.
Zero-knowledge membership proof. Any member can prove they belong to the committed set by presenting a Groth16 proof. The circuit verifies three things: that the prover owns a BLS12-381 private key, that the corresponding public key is a leaf in the Poseidon Merkle tree, and that the tree root hashes (with the epoch and salt) to the stored commitment. The proof is 192 bytes regardless of group size, and verification costs exactly 3 BLS12-381 pairings.
Fee decoupling. A relayer pattern ensures the Stellar account paying transaction fees is not the group member generating the proof. The relayer sees only an opaque proof and a group identifier — it cannot determine who submitted the update.
Lifecycle. Groups are created, updated through monotonically increasing epochs, and can be deactivated (with ZK-proof authorization) when no longer needed. A rolling history window bounds on-chain storage while contract events preserve the full audit trail.
6. Implementation Plan
Phase 1 — Circuit development and testing
circomorbellmanPhase 2 — Trusted setup ceremonies
Phase 3 — Soroban contract
Phase 4 — Client SDK and relayer
Phase 5 — Mainnet deployment and ecosystem adoption
Appendices
A. Constraint budget breakdown
SHA-256 dominates at all tiers. A future Poseidon-only variant (if Soroban adds a Poseidon host function) would reduce total constraints to ~3,000–5,000.
B. References
Beta Was this translation helpful? Give feedback.
All reactions