Skip to main content

Crate curvy_core

Crate curvy_core 

Source
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.

ProfileKey derivationPrimary API
Seed-backedHex seed bytes are processed with Curvy’s established BLAKE-512/prune derivationSeedNoteSigner, sign_hex, pub_from_private_key_hex
Direct-scalarA canonical non-zero subgroup scalar directly derives scalar * Base8ScalarSigningKey, 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 Go curvy-core.

§Module map

ModuleWhat it isMirrors
fieldBN254 scalar field Fr + decimal⇄Fr helpers (the boundary)-
encodinghex / little-endian / big-endian byte helpers-
poseidonPoseidon hash over Frposeidon-lite
babyjubjubBabyJubjub curve (point add + scalar mul)@zk-kit/baby-jubjub
blake512original BLAKE-512 (not BLAKE2)@zk-kit/eddsa-poseidon
eddsaEdDSA-Poseidon signing + key derivation@zk-kit/eddsa-poseidon
ciphernote-data AES-256-CTR additive field-OTPbalanceCipher.ts
notenote id / ownerHash / nullifier commitmentsnote.ts
hash_utilssha256BigIntproving/utils.ts
imtindexed IMT + stateful bounded sharded tree@zk-kit/imt / shardedNotesTree.ts
witnessaggregation / withdrawal / pending-commit witness builderswitnessFromNotes.ts
stealthDomain 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 elementsfield::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 elementsfield::Bn254Fr, which rejects values outside the field instead of reducing them.
  • Scalar-native BabyJubJub keysbabyjubjub::BabyJubSecretScalar and eddsa::ScalarSigningKey, which derive A = scalar·Base8 directly without the seed-backed profile’s hash/prune step.
  • Raw 256-bit integers (the cipher key material, hash_utils::sha256_bigint inputs, the EdDSA message) → num_bigint::BigUint, packed without field reduction. See encoding.
  • Endianness: big-endian for the cipher / sha256BigInt; little-endian for EdDSA. They are named explicitly in encoding so 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’s blake.ts (itself adapted from the blake-hash npm 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 as pub_from_private_key_hex, ephemeral_pub_key and sign_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’s IMT, 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 + the crate::imt tree. Pure assembly: no randomness, no IO.

Type Aliases§

Fr
The BN254 scalar field element.