Architecture
Architecture
A Soroban contract on testnet, a snarkjs circuit, three Node services, and this Next.js frontend. The private path runs entirely in the browser; the services hold only issuer-side state.
Contracts
| Role | Contract ID |
|---|---|
| PoolPass | CBVARDGO…M7K2FV |
| Testnet USDC (SAC) | CAX5YVXG…I4KP77 |
| Pool token | CBUXSNRS…VY2KWC |
| Poseidon parity gate | CDBKFLR5…VIFUWD |
| Groth16 parity gate | CALDSMCD…EI6UFX |
PoolPass methods
contract interfaceinitialize(issuer, usdc_sac, pool_token, groth16_vk: Bytes, merkle_depth: u32, pool_name: String) -> Result<(), Error>
update_accredited_set(issuer, leaf_hashes: Vec<BytesN<32>>) -> Result<BytesN<32>, Error>
subscribe(investor, amount: i128, proof: Bytes, public_inputs: Vec<BytesN<32>>) -> Result<BytesN<32>, Error>
get_pool_info() -> Result<PoolInfo, Error>
advance_epoch(issuer) -> Result<u32, Error>The issuer must authorize set updates and epoch advances. The investor must authorize subscribe and its nested Testnet USDC transfer.
Events
PoolPass uses the SDK 26 #[contractevent] API. The generated publish prepends the snake-case event-name symbol to the topic vector; non-topic fields are a canonical ScMap, named rather than positional. Reproduced verbatim from the integration contract:
event shapesRootUpdated { root, leaf_count, epoch(topic), timestamp }
topics: Symbol("root_updated"), epoch: u32
data: { leaf_count: u32, root: BytesN<32>, timestamp: u64 } // key order: leaf_count, root, timestamp
Subscribed { investor(topic), amount, nullifier, commitment, epoch(topic), timestamp }
topics: Symbol("subscribed"), investor: Address, epoch: u32
data: { amount: i128, commitment: BytesN<32>, nullifier: BytesN<32>, timestamp: u64 } // key order: amount, commitment, nullifier, timestamp
EpochAdvanced { epoch(topic), timestamp }
topics: Symbol("epoch_advanced"), epoch: u32
data: { timestamp: u64 }For RPC getEvents, filter by type: "contract", the PoolPass contract ID, and the encoded first topic via nativeToScVal(name, { type: "symbol" }).toXDR("base64").
Services
| Service | Role |
|---|---|
| api | Fastify HTTP: /accredit, /faucet, /verify, /prove (fallback), /pools, /pool/:id. |
| indexer | Reads RPC getEvents, dedupes by event ID, persists to services/data/events.json. |
| demo-issuer | Holds the issuer key; commits leaves and mints Testnet USDC. |
On-chain Merkle recomputation
The Groth16 proof already outputs the merkle_root as a public input, so in principle the contract could skip on-chain recomputation and just compare the proof’s root to the stored root. PoolPass deliberately recomputes the root on-chain as well, using Stellar’s native Poseidon host function, for two reasons:
Why recompute?
(1) It demonstrates that Stellar’s Poseidon implementation agrees with the off-chain and circuit implementations. This is the Gate A parity evidence.
(2) It provides an independent integrity check that the Merkle tree the contract stores matches the one the proof was generated against.
In a production deployment optimized for gas, the recomputation could be removed and the contract would rely solely on the proof’s public root input. The current design prioritizes parity verification over gas efficiency.
Repository
layoutcontracts/ Soroban contracts (poolpass, mock token, gates)
circuits/ Circom circuit, artifacts, fixtures, VK
src/ shared crypto: poseidon, field, merkle, groth16 serializer
services/ api, indexer, demo-issuer
merkle-tools/ standalone tree + proof helpers
web/ this Next.js frontend
deployments.json machine-readable source of truth