The shielded pool & notes
Hestia stores value as encrypted notes, not balances. Here is the note model and how the pool tracks it.
Hestia is a note pool, not an account ledger. There is no row that says "agent X has X USDC." Instead, value is held in notes — and the chain only ever sees a hash of each note, called a commitment.
The note
A note is the atomic unit of shielded value. Every field is a canonical element of the BN254 scalar field (the field the proofs live in):
interface Note {
value: bigint; // amount, in the token's base units
token: bigint; // token identifier as a field element
owner: bigint; // recipient's public spending key, SK = poseidon([sk])
label: bigint; // lineage tag tying the note to its origin deposit
randomness: bigint; // blinding factor; makes the commitment hiding
}valueis the amount. It must be smaller thanMAX_VALUE(2^248), which leaves headroom for the circuit's range and balance checks.tokenis the asset. Native ETH is the field element0; an ERC-20 is its address mapped into the field (addressToField(token)). This lets one pool hold many assets while the circuit enforces that a spend never mixes them.owneris the recipient's public spending keySK. Only someone who knows the matching secretskcan ever spend the note. See Keys & identity.labelrecords the note's lineage — which approved deposit it descends from. It is the hook that makes association sets work.randomnessis a fresh field element that blinds the commitment, so two notes with the same value and owner still hash to different commitments.
You rarely build a note by hand; newNote fills randomness for you when omitted:
import { newNote } from "@hestia/common";
const note = newNote({ value: 1_000_000n, token: 0n, owner: SK, label });
// → { value, token, owner, label, randomness: <fresh field element> }What the pool stores
For each note that has ever existed, the pool holds exactly two public things:
- its commitment (a leaf in the Merkle tree), and
- an encrypted note blob — the note's secret fields, sealed to the owner's viewing key.
When a note is spent, its nullifier is published into a public set. That's it. Amounts, owners, labels, and randomness are never on-chain in the clear.
on-chain, public off-chain, private (in your SDK)
───────────────── ────────────────────────────────
commitment (leaf) ◄─── Note { value, token, owner, label, randomness }
encrypted blob ────► decrypt with vk → recover the note
nullifier (on spend) sk → derive nullifier to spendHow balances are computed
Because there is no balance on-chain, the SDK computes it. On sync() it pulls every
encrypted blob, trial-decrypts each with your viewing key, keeps the ones that are yours,
discards any whose nullifier is already spent, and sums the survivors per token:
await hestia.sync();
const usdc = await hestia.balance(USDC_ADDRESS.baseSepolia); // bigint, base unitsA "balance" is therefore just the sum of your unspent notes for a token — reconstructed locally, from public data only.
Why notes instead of accounts
Accounts leak. A single mutable balance, updated in place, links every deposit and spend to one identity. Notes don't: each spend consumes notes and creates new ones with fresh randomness and no on-chain link to their parents. That unlinkability is the entire point, and it's what the next pages — commitments & nullifiers and the commitment tree — make precise.
