HESTIAdocs

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):

ts
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
}
  • value is the amount. It must be smaller than MAX_VALUE (2^248), which leaves headroom for the circuit's range and balance checks.
  • token is the asset. Native ETH is the field element 0; 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.
  • owner is the recipient's public spending key SK. Only someone who knows the matching secret sk can ever spend the note. See Keys & identity.
  • label records the note's lineage — which approved deposit it descends from. It is the hook that makes association sets work.
  • randomness is 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:

ts
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:

  1. its commitment (a leaf in the Merkle tree), and
  2. 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.

text
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 spend

How 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:

ts
await hestia.sync();
const usdc = await hestia.balance(USDC_ADDRESS.baseSepolia); // bigint, base units

A "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.