HESTIAdocs

Cryptographic primitives

The field, Poseidon, the note encryption scheme, and the conformance discipline that keeps all three layers in sync.


@hestia/common is the cryptographic spine of the protocol. Every value in a proof is an element of one field, every hash is the same Poseidon, and every note ciphertext uses one authenticated scheme. This page is the reference for those primitives.

The field

All snark values live in the BN254 (alt_bn128) scalar field — the prime field Groth16 over BN254 operates in:

ts
import { FIELD_MODULUS, MAX_VALUE } from "@hestia/common";

// r = 21888242871839275222246405745257275088548364400416034343698204186575808495617
// MAX_VALUE = 2^248 - 1  (the largest value a note may hold)

Helpers keep values canonical (in [0, r)):

ts
import {
  mod, isField, assertField,
  toField, randomFieldElement, addressToField,
  bytesToBigIntBE, bigIntToBytes32BE,
} from "@hestia/common";

isField(x);                 // x ∈ [0, r) ?
assertField(x, "value");    // throw if not
toField(bytes);             // reduce arbitrary bytes → canonical element
randomFieldElement();       // unbiased random element (rejection sampling)
addressToField("0x…");      // EVM address → field element (token / recipient encoding)

MAX_VALUE = 2^248 matters: capping note values well below the field modulus is what lets the circuit's balance and range checks be sound without field-wraparound tricks.

Poseidon

Hestia hashes with Poseidon over BN254 — a hash designed to be cheap inside a snark.

ts
import { getPoseidon, poseidon } from "@hestia/common";

const h = await poseidon([a, b]);     // one-shot
const hasher = await getPoseidon();   // cached instance
hasher.hash([a, b, c]);               // arity = inputs.length

The arities are fixed by use:

UseArityDefinition
Commitment5poseidon([value, token, owner, label, randomness])
Nullifier3poseidon([commitment, leafIndex, sk])
Merkle parent2poseidon([left, right])
Spending key1SK = poseidon([sk])

On-chain, Poseidon is circomlib-generated bytecode (T2/T3/T6) deployed via CREATE, so the contract reproduces these hashes bit-for-bit. See contracts.

Note encryption

Each note's secret fields are sealed to the recipient's public viewing key VK so only they can discover it. The scheme:

  • X25519 ephemeral key agreement (a fresh ephemeral key per note),
  • HKDF-SHA256 to derive a symmetric key from the shared secret, and
  • ChaCha20-Poly1305 authenticated encryption.
ts
import { encryptNote, decryptNote, type NotePlaintext } from "@hestia/common";

const blob = encryptNote(recipientVK, plaintext); // → ephemeralPub ‖ ciphertext
const pt = decryptNote(vk, blob);                  // → NotePlaintext | null

The blob layout is ephemeralPublicKey ‖ ciphertext. Decryption is a trial: the AEAD tag verifies only when the blob was sealed to the matching VK, which is exactly how the SDK discovers your notes (see viewing keys). Key derivation itself uses keccak256 for domain-separated seed and viewing-secret material (see keys).

The implementations are the audited @noble/* curve and cipher libraries — no bespoke cryptography.

Conformance is the contract

These primitives ship with golden fixtures that serve as the conformance suite for the whole protocol. The circuit (P2) and the contracts (P3) must reproduce the same outputs for the same inputs. If the SDK, the circuit, and Solidity didn't agree on Poseidon, the field encoding, or the commitment layout, a locally generated proof simply would not verify on-chain. The fixtures make that agreement testable rather than assumed.