Expand description
§curvy-core
Production-compatible Curvy cryptography and circuit-input construction in Rust. This crate contains Poseidon, BabyJubjub EdDSA, note encryption and commitments, Merkle trees, witness builders, and stealth addressing. It does not evaluate compiled Circom graphs or generate Groth16 proofs.
§Install
[dependencies]
curvy-core = "=0.1.0-rc.3"Rust 1.94 or newer is required.
§Signing profiles
Seed-backed and direct-scalar BabyJubjub signing are both first-class supported profiles. Choose the profile that matches the account’s stored key material; neither profile is deprecated.
| Profile | Key derivation | Primary API |
|---|---|---|
| Seed-backed | Hex seed bytes are processed with Curvy’s established BLAKE-512/prune derivation | SeedNoteSigner, sign_hex, pub_from_private_key_hex |
| Direct-scalar | A canonical non-zero subgroup scalar directly derives scalar * Base8 | ScalarSigningKey, BabyJubSecretScalar, BabyJubPoint |
Both signer types implement NoteSigner and can be passed to
build_withdrawal_with_signer or build_aggregation_with_signer:
use curvy_core::eddsa::ScalarSigningKey;
use curvy_core::witness::{NoteSigner, SeedNoteSigner};
let seed_signer = SeedNoteSigner::new("000102030405060708090a0b0c0d0e0f");
let scalar_signer = ScalarSigningKey::from_decimal("1")?;
let _seed_public_key = seed_signer.public_key();
let _scalar_public_key = scalar_signer.public_key();Do not reinterpret key material from one profile as the other: the derivations produce different account keys.
§Parallel feature
The optional parallel feature uses Rayon for independent stealth scans and
bulk Merkle-tree construction:
[dependencies]
curvy-core = { version = "=0.1.0-rc.3", features = ["parallel"] }Native applications can select the global Rayon pool size with
RAYON_NUM_THREADS or configure rayon::ThreadPoolBuilder before the first
parallel call.
See the workspace guide for complete native and WASM build targets.
§Implementation notes
Every function here is pinned to a reference implementation by golden vectors (see “Verification” below), so behaviour must stay byte-for-byte identical - even where that means code that looks unusual.
§New here? Read this first
The crate is split into two cryptographic domains. They are very different; treat them separately.
- Domain B - the circuit/commitment layer. BabyJubjub + Poseidon over the BN254 scalar field, a note cipher, note commitments, and the Merkle trees and witness builders the zk-circuits consume. Start here - it is self-contained and where most code lives.
- Domain A - the stealth addressing core (
stealth). The hard part: dual-curve and pairing-based (secp256k1 spend keys + BN254 viewing keys), ported from the Gocurvy-core.
§Module map
| Module | What it is | Mirrors |
|---|---|---|
field | BN254 scalar field Fr + decimal⇄Fr helpers (the boundary) | - |
encoding | hex / little-endian / big-endian byte helpers | - |
poseidon | Poseidon hash over Fr | poseidon-lite |
babyjubjub | BabyJubjub curve (point add + scalar mul) | @zk-kit/baby-jubjub |
blake512 | original BLAKE-512 (not BLAKE2) | @zk-kit/eddsa-poseidon |
eddsa | EdDSA-Poseidon signing + key derivation | @zk-kit/eddsa-poseidon |
cipher | note-data AES-256-CTR additive field-OTP | balanceCipher.ts |
note | note id / ownerHash / nullifier commitments | note.ts |
hash_utils | sha256BigInt | proving/utils.ts |
imt | indexed IMT + stateful bounded sharded tree | @zk-kit/imt / shardedNotesTree.ts |
witness | aggregation / withdrawal / pending-commit witness builders | witnessFromNotes.ts |
stealth | Domain A stealth addressing (pairing) | Go curvy-core |
§The boundary: how values cross in and out
Scalar crypto boundaries speak decimal strings (and "X.Y" for points),
matching the existing TypeScript/Go wire shapes. Bulk tree boundaries use
canonical packed 32-byte field elements. Two conversions matter and are easy
to get wrong, so they live in exactly one place each:
- Trusted/internal field elements →
field::fr_from_dec/field::fr_to_dec, which reduce modulo the field. Use them for amounts, hashes, and commitments - anything that is a field element. - Untrusted canonical field elements →
field::Bn254Fr, which rejects values outside the field instead of reducing them. - Scalar-native BabyJubJub keys →
babyjubjub::BabyJubSecretScalarandeddsa::ScalarSigningKey, which deriveA = scalar·Base8directly without the seed-backed profile’s hash/prune step. - Raw 256-bit integers (the cipher key material,
hash_utils::sha256_bigintinputs, the EdDSA message) →num_bigint::BigUint, packed without field reduction. Seeencoding. - Endianness: big-endian for the cipher /
sha256BigInt; little-endian for EdDSA. They are named explicitly inencodingso the two never get mixed up.
§Error convention
Internal boundary parsers panic on malformed input (e.g. a non-numeric “decimal”), because callers pass already-validated values and a panic surfaces a programming error loudly. Untrusted input is validated at the wasm boundary before reaching here.
§Signing profiles
Seed-backed keys and direct-scalar keys are co-equal supported profiles. Use
witness::SeedNoteSigner for established seed-derived accounts and
eddsa::ScalarSigningKey when the account stores a canonical BabyJubjub
subgroup scalar. Both implement witness::NoteSigner and produce the same
Curvy circuit-input shapes; neither profile is deprecated.
§Verification
Committed compatibility vectors from the production TypeScript and Go implementations are asserted in the crate’s test suite. Primitive behavior must remain byte-for-byte compatible with those vectors.
§Example
use curvy_core::field::{fr_from_dec, fr_to_dec};
use curvy_core::poseidon::poseidon;
// The canonical circomlib test vector: Poseidon([1, 2]).
let h = poseidon(&[fr_from_dec("1"), fr_from_dec("2")]);
assert_eq!(
fr_to_dec(&h),
"7853200120776062878684798364095072458815029376092732009249414926327459813530",
);Re-exports§
pub use imt::NOTES_SHARD_HEIGHT;pub use imt::NOTES_SHARD_SIZE;pub use imt::NOTES_TREE_DEPTH;pub use imt::NOTES_TREE_VERSION;pub use poseidon::poseidon;
Modules§
- babyjubjub
- BabyJubjub twisted Edwards curve over BN254
Fr- a faithful port of@zk-kit/baby-jubjub@1.0.3(EIP-2494). Only the two operations EdDSA needs are ported: point addition and scalar multiplication. The curve lives over the same field as everything else (Fr), so no separate curve crate is required. - blake512
- Original BLAKE-512 (the SHA-3 finalist, not BLAKE2) - a faithful port of
@zk-kit/eddsa-poseidon’sblake.ts(itself adapted from theblake-hashnpm package). EdDSA-Poseidon’s default entry uses this to hash the private key, so the Rust core must reproduce it exactly. Validated by direct golden vectors. - cipher
- Note-data cipher - a faithful port of
balanceCipher.ts. - eddsa
- EdDSA-Poseidon over BabyJubjub - a faithful port of
@zk-kit/eddsa-poseidon’s default (BLAKE-1 / original BLAKE-512) entry, exposed here aspub_from_private_key_hex,ephemeral_pub_keyandsign_hex. - encoding
- Byte/integer encodings used at the EdDSA boundary.
- field
- BN254 scalar field (
Fr) - the SNARK scalar field shared by circom, snarkjs, poseidon-lite, and @zk-kit: - hash_
utils - Misc hash helpers - a faithful port of
proving/utils.ts. - imt
- Incremental Merkle Tree (arity 2, Poseidon hash) - a faithful port of
@zk-kit/imt’sIMT, plus indexed and stateful sharded engines. - note
- Note commitments - a faithful port of
note.ts’s Poseidon derivations. - poseidon
- Poseidon hash over BN254 Fr - a faithful port of
poseidon-lite@0.2.1(the canonical unoptimized HadesHash from the Poseidon whitepaper, as used by circomlib). Same round counts, same x^5 S-box, same[0, ...inputs]state init, same round constants (C) and MDS matrix (M) - so outputs match bit-for-bit. - stealth
- Domain A - stealth addressing core. Native Rust port of
curvy-core(Go/gnark). - witness
- Witness builders - native Rust port of
witnessFromNotes.ts/pendingNotesCommitmentInputs.ts. They produce the flat snarkjs input objects (circom field-declaration order) by composing the ported Domain-B primitives + thecrate::imttree. Pure assembly: no randomness, no IO.
Type Aliases§
- Fr
- The BN254 scalar field element.