Skip to main content

TransferCircuit

The core privacy circuit for Root14. Implements a 1-in-2-out UTXO transfer: consumes one note and creates two (recipient + change).

  • 7,638 R1CS constraints (BLS12-381 / Groth16)
  • 4 public inputs: old_root, nullifier, out_commitment_0, out_commitment_1
  • Private witnesses: secret key, consumed note fields, Merkle path (20 levels), two created notes

Struct​

#[derive(Clone)]
pub struct TransferCircuit {
pub secret_key: Option<Fr>,
pub consumed_note: Option<Note>,
pub merkle_path: Option<MerklePath>,
pub created_notes: Option<[Note; 2]>,
}

Each Note contains four fields:

pub struct Note {
pub value: u64,
pub app_tag: u32,
pub owner: Fr,
pub nonce: Fr,
}

The Merkle path has depth MERKLE_DEPTH = 20:

pub struct MerklePath {
pub siblings: Vec<Fr>,
pub indices: Vec<bool>,
}

Constraints​

The circuit enforces 7 groups of constraints inside generate_constraints:

1. Ownership​

poseidon(sk) == consumed_note.owner

Proves the sender knows the secret key that derives the consumed note's owner hash. Uses poseidon_hash_var(cs, &[sk_var]) and enforce_equal against the consumed owner witness.

2. Consumed note commitment​

poseidon(value, app_tag, owner, nonce) == consumed_commitment

Recomputes the commitment from the note's four fields. This value is then fed into the Merkle path verification.

3. Merkle inclusion​

verify_merkle_path(consumed_cm, path, old_root)

Walks the 20-level Merkle path from the consumed commitment up to the root. At each level, is_right selects the ordering: if true, hash (sibling, current); otherwise hash (current, sibling). The final value is enforced equal to the public input old_root.

4. Nullifier​

poseidon(sk, nonce) == nullifier

Binds the secret key to the consumed note's nonce, producing a deterministic nullifier. Enforced equal to the public input nullifier.

5. Output commitment 0​

poseidon(value_0, app_tag_0, owner_0, nonce_0) == out_commitment_0

Recomputes the first output note's commitment and enforces equality with the public input out_commitment_0.

6. Output commitment 1​

poseidon(value_1, app_tag_1, owner_1, nonce_1) == out_commitment_1

Same as above for the second output note, enforced against out_commitment_1.

7. Value conservation and app tag match​

consumed.value == created[0].value + created[1].value
consumed.app_tag == created[0].app_tag
consumed.app_tag == created[1].app_tag

Ensures no value is created or destroyed, and that all three notes share the same app_tag. The code uses three enforce_equal calls: one for value conservation and two for app tag matching.

Public Inputs​

pub struct PublicInputs {
pub old_root: Fr,
pub nullifier: Fr,
pub out_commitment_0: Fr,
pub out_commitment_1: Fr,
}

impl PublicInputs {
pub fn to_vec(&self) -> Vec<Fr> {
vec![self.old_root, self.nullifier, self.out_commitment_0, self.out_commitment_1]
}
}

The Groth16 verification key will have ic.len() == 5 (1 constant term + 4 public inputs).

API​

setup(rng) -> (ProvingKey, VerifyingKey)​

Runs Groth16 trusted setup using TransferCircuit::empty() (all witnesses None).

pub fn setup<R: RngCore + CryptoRng>(
rng: &mut R,
) -> (ProvingKey<Bls12_381>, VerifyingKey<Bls12_381>) {
let circuit = TransferCircuit::empty();
Groth16::<Bls12_381>::circuit_specific_setup(circuit, rng).expect("setup failed")
}

prove(pk, sk, consumed, path, created, rng) -> (Proof, PublicInputs)​

Computes public inputs natively (root from path, nullifier, output commitments), then generates the Groth16 proof.

pub fn prove<R: RngCore + CryptoRng>(
pk: &ProvingKey<Bls12_381>,
secret_key: Fr,
consumed_note: Note,
merkle_path: MerklePath,
created_notes: [Note; 2],
rng: &mut R,
) -> (ark_groth16::Proof<Bls12_381>, PublicInputs)

verify_offchain(vk, proof, pi) -> bool​

Verifies a proof off-chain using PreparedVerifyingKey.

pub fn verify_offchain(
vk: &VerifyingKey<Bls12_381>,
proof: &ark_groth16::Proof<Bls12_381>,
public_inputs: &PublicInputs,
) -> bool

constraint_count() -> usize​

Returns the R1CS constraint count by running generate_constraints in setup mode.

pub fn constraint_count() -> usize