Skip to main content
This page covers the Rust-specific shape of the primitives. The protocol-level concepts (commitments, Merkle tree, nullifiers, fees) are identical across both SDKs — see the shared core concepts page for those.

Flow classification

Every transact is one of three flows, discriminated by the sign of external_amount:
Preflight classifies automatically — you never construct FlowKind yourself. The orchestrator then routes:
  • Deposits → always direct submission (user must sign the SOL/SPL transfer into the pool).
  • Transfers / withdrawals → submitted via relay_url when set, otherwise direct.
  • Swaps → always submitted via relay_url (DEX routing happens server-side).

UTXOs

The keypair field is a field-element keypair (not an Ed25519 Solana keypair). It’s how you prove ownership of a note later — save it. The fixed-arity circuit consumes exactly 2 inputs and 2 outputs. You supply the real ones; preflight pads with zero-UTXOs (random blinding) up to 2 each.

TransactOptions as the single knob surface

Every flow is one struct. Common fields: Flow-specific extras: Chaining:

The retry loop

transact() runs up to max_root_retries iterations of:
  1. fetch merkle proofs — pull on-chain state + rebuild local tree
  2. build circuit inputs — 90 signals, decimal-encoded
  3. prove — Groth16 via ark-circom (heavy; cached artifacts + spawn_blocking)
  4. encrypt chain notes — one compact note bound to output_commitments[0]
  5. submit — direct (with blockhash + transport retries) or relay_url POST
  6. reconcile indices — best-effort poll to fill output_indices
Errors map to IterationStatus:

Commitments and the cached tree

After each successful transact, TransactResult.cached_merkle_tree holds a local MerkleTree with the two new output commitments appended. Thread it into the next TransactOptions.cached_merkle_tree and you skip the /commitments fetch on the next call — the orchestrator seeds LoopState.merkle_tree from it directly. sibling_commitments and pre_transaction_left_sibling on the result are populated from the post-tx tree so the next proof can use them without a fresh /commitments fetch.

Chain notes

Every non-zero-output transact emits exactly one compact v2 chain note:
  • Primary commitment: output_commitments[0] — same value bound into chainNoteHash.
  • Plaintext: 8-byte LE unix timestamp.
  • Encryption: HKDF-SHA256(ikm=nk, salt=commitment) → AES-256-GCM key + nonce.
  • Envelope: [0x02 | count | (len, ciphertext)*] appended at instruction byte 521.
nk resolution falls back: opts.chain_note_nk → first non-zero output’s UTXO key → first non-zero input’s.