advanced

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.