The Association Set Provider for the Opaque privacy pool, with a Stellar/Soroban chain
adapter. It is the off-chain curator that decides which deposits are "clean" and publishes
the association-tree root that the withdraw circuit proves against. For the testnet
demo it also publishes the mechanical pool state-tree root reconstructed from public
Deposit/Withdraw events, so the browser wallet has both roots required for withdrawal.
npm install
npm run typecheck
npm test # engine reconcile, set/tree, policy, store (offline)
ASP_SECRET=S... npm run indexer:once # one live reconcile pass against testnet
ASP_SECRET=S... npm run indexer # loop every ASP_INTERVAL_MS
runPoolTick (src/engine.ts):
- Read finalized
Depositevents from theprivacy-poolcontract (src/chains/stellar.ts, SorobangetEvents, respecting the exact-length topic-match rule). - Screen each via a pluggable
Policy(src/policy.ts). The shipped v1 policy isapproveAll— every testnet deposit is approved — with anallowliststub and a documentedscreeningPolicyhook for real sanctions/risk screening. - Maintain the ordered approved set and rebuild a depth-20 Poseidon(2) tree
(
src/set.ts+src/merkle.ts) byte-identical to theprivacy-poolcontract and the v3 circuit. - Reconcile ASP: compare the local association root to the on-chain root and, only on mismatch,
publish the manifest (
data/sets/<poolId>/<root>.json) and postupdate_asp_root. Reconcile-not-append makes it idempotent and self-healing — a crash mid-publish is resolved on the next tick. - Reconcile state: rebuild the pool state tree from
Depositcommitments andWithdrawremainder commitments, then postupdate_state_rootonly when it differs from the latest on-chain state root.
The ASP cannot mint, steal, or forge double-spends. It only gates withdrawal eligibility:
- The published label list is self-authenticating: a withdrawer recomputes the Merkle
root locally and checks it equals the on-chain
aspRoot, so a bad list simply fails proof generation. - State-tree (commitment) membership — which proves a deposit is real and backed — is rebuilt from public pool events and verified against the on-chain state root. The demo ASP process publishes that root, but it does not choose the leaves.
- The
privacy-poolcontract enforces a custody invariant (aggregate withdrawals ≤ aggregate deposits) and the SAC balance is the physical backstop, so even the state-root publisher cannot authorize unbacked withdrawals.
Under approveAll, clients can skip this service entirely and reconstruct the set straight
from on-chain Deposit events; the manifest/IPFS path only matters once a selective policy
is used.
| Var | Default | Meaning |
|---|---|---|
STELLAR_RPC_URL |
testnet RPC | Soroban RPC endpoint |
ASP_SECRET |
— (required) | ASP authority S… seed (the pool admin in the demo) |
ASP_INTERVAL_MS |
15000 |
loop interval |
ASP_CONFIRMATIONS |
1 |
confirmations before a deposit is treated as final |
IPFS_API_URL |
— | optional manifest pinning endpoint |
Pool id + scope are resolved from deployments/v1/testnet.json.