Cryptography
Poseidon, Groth16, Ed25519, X25519 primitives
Overview#
Mini Veil uses four cryptographic primitives: Poseidon hashing for the Merkle tree and nullifier derivation, Groth16 for zero-knowledge proofs over BN254, Ed25519 for stealth address key pairs, and X25519 for key exchange.
Poseidon Hash#
Poseidon is a zk-friendly hash function designed for use inside SNARK circuits. Mini Veil uses it at three arities:
| Arity | Usage | JavaScript API |
|---|---|---|
| 1 | Nullifier hash: poseidon1([nullifier]) | poseidonHash([value]) |
| 2 | Merkle tree: poseidon2([left, right]), Leaf: poseidon2([nullifier, secret]) | poseidonHash([a, b]) |
| 3 | Nullifier: poseidon3([spendingKey, DOMAIN_SEPARATOR, counter]) | poseidonHash([a, b, c]) |
Domain Separator#
DOMAIN_SEPARATOR = Poseidon(["mixer_nullifier"])
= 19652034335053287043662554569486675073262743043436276517630902671951488085701n
This ensures nullifier derivation is domain-separated from other protocols using the same circuit or keys.
BN254 Scalar Field#
All Poseidon operations are modulo the BN254 scalar field:
BN254_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617n
Groth16#
Proving System#
Groth16 is a pairing-based zero-knowledge proving system. Mini Veil uses it with the BN254 curve (Solana's alt_bn128).
Verification Key Structure#
pub struct Groth16Vk {
pub alpha_g1: [u8; 64], // G1 point (x, y)
pub beta_g2: [u8; 128], // G2 point (x_re, x_im, y_re, y_im)
pub gamma_g2: [u8; 128], // G2 point
pub delta_g2: [u8; 128], // G2 point
pub ic: &[[u8; 64]], // IC[0..n] for public inputs
}
On-chain Verification#
Groth16 verification uses three Solana syscalls:
| Syscall | Operation |
|---|---|
| alt_bn128_addition | G1 point addition |
| alt_bn128_multiplication | G1 scalar multiplication |
| alt_bn128_pairing | Pairing check: e(-A, B) * e(α, β) * e(vk_x, γ) * e(C, δ) == 1 |
G2 Point Packing#
snarkjs outputs G2 Fp2 elements as [im, re]. Solana's alt_bn128_pairing expects [re, im] (EIP-197 convention). The packG2 function handles this swap:
// snarkjs output: [[im_0, re_0], [im_1, re_1]]
// Solana input: [re_0, im_0, re_1, im_1]
for (let idx = 0; idx < 2; idx++) {
for (let i = 0; i < 2; i++) {
buf[offset++] = point[idx][1 - i]; // [re, im] instead of [im, re]
}
}
Ed25519#
Stealth Address Keys#
Stealth addresses use Ed25519 for signing claim transactions. The key derivation:
stealthPrivScalar = (spendingScalar + tweak) % L
stealthPubKey = stealthPrivScalar * G
Where L is the Ed25519 group order:
L = 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed
ScalarSigner#
Since the stealth private key is a scalar (not an Ed25519 seed), a custom ScalarSigner performs signing:
r = SHA-512(scalar || message) % L
R = r * G
h = SHA-512(R || pubKey || message) % L
s = (r + h * scalar) % L
signature = (R, s) // 64 bytes
X25519#
Key Exchange#
X25519 is used for shared secret derivation between sender and recipient:
// Sender:
sharedSecret = X25519(ephemeralPriv, recipientViewingPub)
// Recipient:
sharedSecret = X25519(recipientViewingPriv, ephemeralPub)
Tweak Derivation#
The shared secret is hashed to produce a tweak for the stealth address:
tweak = reduceScalar(SHA-256(sharedSecret))
The tweak is the first 32 bytes of the SHA-256 hash, reduced modulo Ed25519 order L.
AES-256-GCM#
The ephemeral private key r is encrypted for the recipient:
encryptionKey = SHA-256(sharedSecret)
encryptedKey = AES-256-GCM(encryptionKey, r)
The encrypted key is published in the StealthAnnouncement event. The recipient decrypts using the same shared secret.