Association sets & compliance
How Hestia stays compliant without a backdoor — labels, the single-lineage rule, and the Privacy Pools association-set proof.
This is the page that makes Hestia compliant rather than just private. The mechanism is the Privacy Pools model: every spend proves, in zero knowledge, that its funds descend from a deposit in an approved association set — without revealing which deposit.
The problem it solves
A naive shielded pool gives everyone one giant anonymity set. That is great for privacy and terrible for legitimacy: honest users are mathematically pooled with whatever illicit funds also entered, and the only "fix" anyone offers is an operator with a master key. Hestia refuses both horns of that dilemma.
Labels: a note's lineage
Every note carries a label — a field element that records which original deposit it descends from. A shield deposit's label is derived from its leaf index:
import { labelFromLeafIndex } from "@hestia/common";
const label = await labelFromLeafIndex(leafIndex); // the deposit's lineage tagWhen a note is spent, its output notes inherit the same label. Lineage is preserved across every private transfer, so a label always points back to the deposit a chain of notes came from.
The single-lineage rule
Here is the rule that prevents laundering by merging:
A transaction's output notes carry the label of its input note, and the SDK spends one input note per transaction.
Because a spend takes a single input and stamps its label onto the outputs, you can never blend
a "clean" note and a "tainted" note into an output with an ambiguous origin. Lineage cannot be
mixed away. (The circuits support a two-input form, but the high-level
send/unshield operations use the single-input join-split precisely
to keep lineage unambiguous.)
The association-set proof
An association set is a Merkle set of approved labels — published by an Association Set Provider (ASP). On every spend, the circuit proves:
- the input note's label is a member of the association set, against a known association root, and
- that root is one the contract recognizes as approved.
So the spender demonstrates "my funds descend from a deposit this ASP approved" — revealing neither the deposit nor the label itself.
interface AssociationProvider {
root(): bigint | Promise<bigint>;
proof(label: bigint): MerkleProof | Promise<MerkleProof>;
}The SDK takes any AssociationProvider. The bundled AssociationSet builds one locally:
import { AssociationSet } from "@hestia/sdk";
const set = await AssociationSet.create();
set.add(approvedLabel); // approve a deposit's label
const root = set.root(); // publishable association root
const proof = set.proof(label); // membership proof for a spendSee association providers for production wiring.
How approval reaches the chain
An ASP publishes its association root to the AssociationSetRegistry contract. The
indexer tracks which roots are approved (and which were
revoked), and the pool only accepts spends whose association root is currently approved. You
can check a root's status through the route:
GET /api/v1/association/status?root=<root>
→ { root, valid: true, uri: "…" }What this gives each party
- Honest agents prove membership in a clean set and are never anonymized alongside illicit funds.
- ASPs / compliance define and update the approved set on-chain, transparently — no ability to deanonymize anyone, only to set the membership policy.
- Everyone keeps amounts, owners, and links private throughout.
Compliance here is not a disclosure of secrets; it is a proof of good provenance. The disclosure side — revealing your own history on request — is handled separately by viewing keys.
