Association providers
Supply the association root and membership proofs to the SDK — the bundled local set, a dynamic provider, and production wiring.
Every spend must prove its note's lineage label belongs to an approved
association set. The SDK gets that root and the membership
proofs from an AssociationProvider you pass in config. This page covers the interface and
three ways to implement it.
The interface
interface AssociationProvider {
root(): bigint | Promise<bigint>;
proof(label: bigint): MerkleProof | Promise<MerkleProof>;
}root()returns the association root the SDK puts into the proof and the on-chainTransactData. The contract checks it is currently approved by the registry.proof(label)returns the Merkle proof that the input note's label is a member of that root.
1 · The bundled local set
AssociationSet is a ready-made provider backed by a local Merkle set. Add approved labels,
read the root, and produce proofs:
import { AssociationSet } from "@hestia/sdk";
import { labelFromLeafIndex } from "@hestia/common";
const set = await AssociationSet.create(); // optional depth arg
set.add(await labelFromLeafIndex(0)); // approve a deposit's label
const root = set.root();
const proof = set.proof(await labelFromLeafIndex(0));
const hestia = await Hestia.create({ /* … */, association: set });Use this for tests, self-hosting, or when your agent screens its own deposits.
2 · A dynamic provider
The Labs console approves its own deposits and keeps the set in step with the pool's current
leaves. It refreshes the set to 0..leafCount on every sync:
import { AssociationSet, type AssociationProvider } from "@hestia/sdk";
import { labelFromLeafIndex } from "@hestia/common";
class DynamicAssociation implements AssociationProvider {
private set: AssociationSet | null = null;
async refresh(leafCount: number): Promise<bigint> {
const set = await AssociationSet.create();
for (let i = 0; i < leafCount; i++) set.add(await labelFromLeafIndex(i));
this.set = set;
return set.root();
}
root(): bigint {
if (!this.set) throw new Error("association set not synced");
return this.set.root();
}
proof(label: bigint) {
if (!this.set) throw new Error("association set not synced");
return this.set.proof(label);
}
}You call refresh(leafCount) after each hestia.sync(). This pattern is a stand-in: it treats
the pool's deposits as the approved set. A real ASP publishes that root on-chain instead.
3 · Production: an on-chain ASP
In production an Association Set Provider curates the approved set off-chain and publishes
its root to the AssociationSetRegistry. Your provider then:
- mirrors the ASP's published set (or fetches membership proofs from the ASP's service), and
- returns that published root from
root()— the same root the registry has approved.
You can confirm a root is live through the route before spending:
const res = await fetch(`/api/v1/association/status?root=${root}`).then((r) => r.json());
// → { root, valid: true, uri: "…" } // uri points to the set behind the rootIf root() returns a value the registry doesn't recognize as approved, the on-chain
transact will revert — the contract enforces the policy regardless of what the SDK supplies.
Choosing a provider
| Provider | When |
|---|---|
AssociationSet (local) | Tests, local dev, self-screening agents. |
| Dynamic (pool leaves) | A console/app that approves its own deposits, like Labs. |
| On-chain ASP mirror | Production, where a third party defines and publishes the approved set. |
Whichever you use, the privacy guarantee is unchanged: the SDK proves membership of a label in the root, never the label or the deposit itself.
