Privacy protocol

Shielded pool on Stellar.

Hypertron's protocol is a permissionless shielded pool: deposit into a Poseidon Merkle tree, privately transfer notes, unshield to a public Stellar address. Proofs are Groth16 over BLS12-381 (CAP-0059). The pool is the cryptographic core. The API and workspace are consumers of it, not part of the trusted computing base.

Testnet. Single-coordinator Groth16 setup. No ceremony. No audit.

Keys were generated 15 Aug 2026 from OS CSPRNG (OsRng). No seed material exists on disk, so an arbitrary reader cannot replay the setup. The coordinator who ran it could have retained the toxic waste and could forge proofs. That is not a multi-party ceremony. Do not put assets of value in this pool.

Keys generated before 15 Aug 2026 used public seed 1. Those are retired. A seed-1 deposit proof verifies false against the live verifier.

Scope

A production deployment of a shielded-payment primitive.

The interesting object is the pool: notes, nullifiers, a recent-root window, and on-chain pairing checks. Application layers reuse this tree. They do not get a private anonymity set.

Stellar has an existing research primitive in this class — SDF commissioned shielded-payment protocol work (SPP) that showed a Groth16 pool is viable on this network. Hypertron is a CAP-0059 deployment of that construction: live testnet contracts, browser proving, an indexer for leaf availability, and merchant-facing layers on the same note set. Cite the primitive. This page describes Hypertron's circuits and contracts, not a comparison.

ComponentResponsibility
contracts/commitmentDepth-20 Poseidon Merkle tree, 32-root history, insert-only by the pool.
contracts/nullifierPersistent spent-nullifier set. Losing a spent nf is a double-spend.
contracts/verifierAdmin-registered Groth16 VKs. CAP-0059 pairing verification.
contracts/transferToken custody. Atomic deposit / transfer / transfer_n / unshield.
contracts/complianceOptional allow/deny at transparent exit only. Not on private transfer.
prover + prover-wasmCircuits, setup CLI, Merkle paths, note math, viewing encryption.
indexerOrdered leaves beyond RPC retention. Not a verifier. Cannot authorize a spend.

The transfer pool is the only authority that may insert commitments or mark nullifiers spent. Verifier-key administration can replace a registered VK with no timelock.

CAP-0059

Groth16 over BLS12-381, end to end.

Pairing in pure contract WASM is not feasible inside Soroban limits. Host functions are a hard dependency, not an optimization.

LayerChoice
Proof systemGroth16
Curve / scalar fieldBLS12-381
Off-chain proverark-bls12-381
On-chain verifiersoroban_sdk::crypto::bls12_381
Note / Merkle hashPoseidon over the BLS12-381 scalar field (soroban-poseidon on-chain)
Viewing encryptionX25519 ECDH + SHA-256 domain-separated KDF + ChaCha20-Poly1305
Merkle depth20 (2^20 leaves)
Accepted rootsMost recent 32
Value rangeExplicit 64-bit checks on outputs and amounts

Note construction

Commitment hides contents. Nullifier prevents double-spend.

A note is an owner public key, a fresh blinding factor, and a value. The spend key never appears on-chain.

Note algebra
owner_pk = Poseidon(spend_sk, 0)
cm       = Poseidon(Poseidon(owner_pk, k), v)
nf       = Poseidon(spend_sk, k)
  • spend_sk — secret required to spend. Never encrypted into viewing blobs.
  • owner_pk — public receiving material. Deposit does not require the spend key.
  • k — per-note blinding. Reused k under the same spend key collides nullifiers.
  • v — value. Spent input value is constrained by conservation, not a second bit decomposition.

Disclosure

Viewing cannot spend.

The viewing key is an independent X25519 pair. Decrypting a blob reveals owner_pk || k || v. It cannot compute Poseidon(spend_sk, k), so it cannot produce a valid nullifier or witness.

Blob length for a valid AEAD ciphertext is 144 bytes: 32-byte ephemeral public key, 96-byte plaintext, 16-byte Poly1305 tag. The ChaCha nonce is implicit all-zeros; uniqueness comes from a fresh ephemeral key per note.

Transfer blobs are not circuit public inputs. A submitter can replace, empty, or omit them without invalidating the proof. Scanners must recompute cm from a decrypted note and match the published leaf. Ownership still follows the proof-bound commitments.

Commitment contract

Incremental Poseidon tree, depth 20.

Only the configured pool may insert. Duplicate leaves are rejected. Insertions emit index, leaf, and root. The indexer preserves order after RPC events expire.

Circuits

Separate proving key and VK id per statement.

Any circuit change needs a new setup and a distinct registration. IDs are deployment configuration, not protocol constants.

CircuitPublic inputsStatement
Deposit · VK 1[cm, amount]cm opens to amount. Amount is 64-bit. No spend key required.
Unshield · VK 2[root, nf, recipient, amount, change_cm]Spend one note to a public address; change stays under the same spend key.
Transfer 1-in/2-out · VK 3[root, nf, out_cm1, out_cm2]Spend one note, emit recipient + change. Amounts not public.
TransferN 2-in/2-out · VK 4[root, nf_1, nf_2, out_cm1, out_cm2]Same owner spend key. No dummy padding. Conservation across two inputs.
TransferN 4-in/2-out · VK 5[root, nf_1…nf_4, out_cm1, out_cm2]Consolidation in-circuit. 4-in browser proving time is not published.

Unshield recipient_field is the BLS12-381 scalar reduction of SHA-256(XDR(ScVal::Address(recipient))). The contract derives it from the actual payout address, so a submitter cannot redirect funds. Unshield does not emit an encrypted change blob; the wallet must retain change material. Withdraw UI is still the 1-in unshield path.

TransferNCircuit

Live arity is 1, 2, and 4. Not 1-in only.

TransferNCircuit<N> spends N notes under one spend key and emits two outputs. N is a const generic. There is no dummy-input padding, so proving time scales with the payment.

TransferNCircuit
// N ∈ {2, 4}. No dummy-input padding.
Public:  [root, nf_1 … nf_N, out_cm1, out_cm2]
Private: [spend_sk,
          for each input i: k_i, v_i, Merkle path,
          owner_pk1, k1, v1, owner_pk2, k2, v2]
Checks:
  owner = Poseidon(spend_sk, 0)
  each input cm is in root under that owner
  nf_i = Poseidon(spend_sk, k_i)
  out_cm1, out_cm2 open correctly
  v1, v2, each input v are 64-bit
  Σ v_i = v1 + v2

Checkout selects the smallest confirmed-note set whose values sum to at least the payment, with size in {1, 2, 4}. A three-note cover takes a fourth owned note when one exists; otherwise the caller must top up. There is no 3-in circuit. That hole is intentional: a 3-in VK would be another setup, another proving key, another registration.

Contract entry points: transfer (VK 3) and transfer_n (VK 4 or 5, requiring 2 or 4 nullifiers). WASM exports transfer_2_proof and transfer_4_proof.

Testnet · 15 Aug 2026

The pool that is actually live.

Pool, commitment, and nullifier were redeployed for multi-input transfer (fresh Merkle tree). Verifier CCHSL7YS… was kept; VK ids 4 and 5 were registered on it.

RoleContract
PoolCB2SVTMGQKQVLUHWC5J7K5NOHPXULWEJL452B457NCRW7OKJ42XSVOLL
CommitmentCD7ZZPCQR7DDZHRNRDUFQ5PKSZK3KVPR3HXKO32NR5QNZWNH2ASVCMTQ
NullifierCCIZPBTVHFO6PCUB7APABIBSIJUUND2WVW6NSA2RBPCEOLUMASKF7KQD
VerifierCCHSL7YSPSCT62DBUSCG4CKBJ2I4U4JSBR4RE3YIEGNSEUYXYY7BDIEP
TokenCDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC (native XLM SAC)
hypertron-contracts
# Hashes local artifacts against deployments/testnet.json
# then checks the chain accepts a fresh proof from each PK.
./scripts/verify_deployment.sh

A Groth16 pairing check cannot pass against an unrelated verifying key, so the script confirms the on-chain keys without trusting deployments/testnet.json or its author. The VkRegistered event carries only the vk_id, not the key hash — observers must re-run the script after any admin action.

Open pool on Stellar Lab

Data availability

The chain is authority. The indexer is availability.

RPC event retention is not a permanent DA layer. The indexer records commitment events, serves ordered leaves, and parses nullifiers[]. It cannot authorize a spend.

  • The server already cross-checks commitment.root() on-chain before treating an indexer response as current.
  • The browser still builds Merkle paths from served leaves. It does not yet recompute the Poseidon root and compare it with the on-chain root. Until that check ships, clients depend on the indexer for correct path construction, not only availability.
  • Deposit and unshield require require_auth on the transparent Stellar account. Private transfer does not: the proof binds the state transition, so a relayer can submit. This repository does not operate a production relayer. Direct submission exposes the submitter (CAP-0015 fee-bump is the intended cover).

Roadmap

Production hardening.

Testnet is live. These are the remaining protocol and client items before the pool is treated as production infrastructure.

WorkNow
Transfer arityShipped on testnet. VK 3, 4, 5 registered. Checkout selects {1, 2, 4}.
3-in paymentsNo dedicated circuit. Selector pads to 4 owned notes or asks for a top-up.
Confidential checkoutIf notes do not cover, checkout still has a transparent deposit path. Remove that fallback.
Client Merkle checkServer already checks the contract root. Browser should recompute Poseidon from served leaves.
RelayerABI allows relayed transfer. No production relayer yet, so the submitter is public.
Proof-bound blobsCommitments are bound. Ciphertext is not. Bind or authenticate note blobs.
Ceremony and auditSingle-coordinator setup. Next: multi-party ceremony, published transcripts, independent audit.
MainnetAfter frozen circuits, ceremony, audit, and proving benchmarks.