How it works
The moving parts — pool, notes, proofs, association sets, relayer — and the life of a private transfer end to end.
Hestia is a shielded UTXO (note) pool. Value lives in notes, not account balances. A note is a private record of "this much of this token belongs to this owner." The chain only ever stores a commitment — a hash of the note — never the note itself.
The three operations
text
shield send unshield
┌──────────────┐ ┌────────────────┐ ┌────────────────┐
│ public ERC20 │ ───► │ private balance │ ──► │ clean public │
│ or ETH │ │ (notes in pool)│ │ address │
└──────────────┘ └────────────────┘ └────────────────┘
gross amount is amounts & links amount & dest are
public; ownership fully hidden public; unlinkable
is hidden to the deposit- Shield moves a public balance into the pool. A new commitment is appended to the tree. The deposit amount is visible (it has to leave a public account), but who owns the resulting note is not.
- Send spends one note and creates two: one for the recipient, one for your change. A zero-knowledge proof shows the inputs and outputs balance — without revealing any amount.
- Unshield spends a note and pays part of it out to a public address. The withdrawal amount and destination are public, but they cannot be linked back to the original deposit.
The pieces
| Piece | Role | Package |
|---|---|---|
| Note | Private { value, token, owner, label, randomness } record | @hestia/common |
| Commitment tree | On-chain Poseidon Merkle tree of note commitments (depth 32) | contracts + @hestia/common |
| Nullifier set | On-chain set of spent-note markers; prevents double-spends | contracts |
| Circuit | Groth16 join-split that proves a spend is valid | @hestia/circuits |
| Association set | Merkle set of approved deposit labels (compliance) | registry + @hestia/sdk |
| Indexer | Rebuilds tree, nullifiers, and ASP roots from chain events | @hestia/route |
| Relayer | Submits your proof so the recipient address has no gas history | @hestia/route |
| SDK | Ties it together: keys, discovery, proving, submission | @hestia/sdk |
Life of a private transfer
When an agent calls hestia.send({ token, amount, to }):
- Sync. The SDK pulls the latest pool events through the indexer and reconstructs the commitment tree and nullifier set locally.
- Discover. It trial-decrypts the encrypted note blobs with the agent's viewing key to
find the notes it owns, and selects one note that covers
amount + fee. - Build outputs. It creates two output notes —
valueto the recipient's public spending key, the change back to itself — and encrypts each to the right viewing key. - Prove. Client-side, it generates a Groth16 proof that: the input note is in the tree, its nullifier is correct, inputs = outputs (+ withdrawal + fee), and the note's lineage label is in the approved association set.
- Submit. It sends the proof, the input's nullifier, the two output commitments, and the two ciphertexts on-chain (optionally via a relayer that pays the gas).
- Settle. The pool verifies the proof, records the nullifier as spent, and appends the two new commitments. The recipient discovers their note on their next sync.
Nothing in steps 3–6 reveals an amount, and nothing links the spent note to the new ones.
Where to go next
- The shielded pool & notes — the data model in detail.
- Commitments & nullifiers — how spends stay unlinkable yet unforgeable.
- SDK quickstart — make your first private transfer in code.
- The Labs console — do it in the browser, no code.
