Coins and commitments
A private coin is a secret record that represents an amount of a specific asset inside the pool. Only the coin owner holds the full coin data — the contract stores only a compact cryptographic commitment derived from it.| Field | Role |
|---|---|
value | The amount, in stroops |
asset[0], asset[1] | The Stellar asset contract ID split into two field-element limbs |
nullifier | A secret random value used to prevent double-spending |
secret | Derived from the deposit ephemeral scalar; binds the coin to the deposit flow |
sharedSecret[0], sharedSecret[1] | ECDH shared-secret coordinates binding the coin to a specific recipient |
commitment | The public Merkle leaf hash stored on-chain |
Commitment formula
The commitment for a coin is computed in three steps using Poseidon hashing:commitment value is what gets inserted into the on-chain Merkle tree as a leaf. The underlying nullifier, secret, and sharedSecret values remain entirely private — they never appear on-chain.
Nullifiers
A nullifier prevents a coin from being spent more than once. When you spend a coin, the circuit computesnullifierHash = Poseidon(nullifier) and includes it in the public signals. The contract stores this hash and rejects any future transaction that tries to use the same hash.
Because Poseidon is a one-way function, knowing the nullifierHash on-chain tells an observer nothing about the underlying nullifier or the coin it belongs to.
You can compute a coin’s nullifier hash off-chain using the SDK:
Stealth addresses
A stealth address lets you receive private funds without linking transactions to a fixed on-chain identity. Arcane stealth addresses use thestpl1 Bech32 address format, derived from the BabyJubJub elliptic curve.
Derivation flow
- Sign a message — Your Stellar wallet signs a deterministic message built by the SDK.
- Scalar derivation — The SDK hashes the signature with SHA-256 and interprets the result as a BabyJubJub scalar.
- Encode as
stpl1— The scalar maps to a BabyJubJub public point, which is Bech32-encoded with thestpl1human-readable prefix.
stpl1 address with depositors so they can direct funds to you privately.
Deposit and withdrawal flow
Private deposits and withdrawals use ECDH key agreement on the BabyJubJub curve so that only the intended recipient can recover a coin. Deposit (sender side):- The depositor samples a random ephemeral scalar.
- The ephemeral scalar maps to an ephemeral BabyJubJub public key.
- ECDH between the ephemeral scalar and the recipient’s
stpl1public key produces a shared secret. - The circuit sets
secret = Poseidon(ephemeralScalar)and incorporates the shared secret coordinates into the commitment. - The ephemeral public key is stored on-chain alongside the commitment leaf so the recipient can find it.
- The recipient holds the private BabyJubJub scalar that underlies their
stpl1address. - The recipient reads the depositor’s ephemeral public key from the on-chain leaf (via
get_leaf_ephemeral()). - ECDH between the recipient’s private scalar and the ephemeral public key reproduces the same shared secret.
- With the shared secret recovered, the recipient reconstructs the full coin, builds a Merkle witness, and generates the withdrawal proof.
The private scalar never leaves the client. Only the public
stpl1 address and the on-chain ephemeral public key are ever visible outside your application.Zero-knowledge proofs
Arcane uses Groth16 proofs to validate transactions on the Soroban contract without revealing any private inputs. Groth16 is a succinct, non-interactive zero-knowledge proof system: the proof is small, verification is fast, and the verifier learns nothing beyond what the public signals explicitly encode.What the circuit enforces
The circuit checks all of the following without revealing the underlying values:- The input coin’s commitment exists as a leaf in the Merkle tree (Merkle inclusion).
- The input coin’s commitment was constructed correctly from its private fields (commitment integrity).
- The nullifier hash matches
Poseidon(nullifier)for the spent input coin (nullifier binding). - Output commitments are correctly formed from the stated output coin fields.
- The value balance is conserved across inputs, outputs, public deposits, and public withdrawals.
Proof generation
Usesdk.proveWithdrawal() or sdk.proveTransaction() to generate a proof, then serialize it with sdk.proofToHex() and sdk.publicToHex() before passing the bytes to transact:
AuditEncodedDigest event. If verification fails, the transaction is rejected and no state changes occur.
See Client SDK for the full SDK API reference and installation instructions.