Skip to content

Proof Generation

Achronyme generates zero-knowledge proofs natively — no external tools required. Both backends produce real, verifiable proofs using standard cryptographic protocols.

R1CS + Groth16Plonkish + KZG-PlonK
Libraryark-groth16PSE halo2
CurveBN254BN254
Proof size~128 bytes (constant)Larger (scales with circuit)
SetupPer-circuit trusted setupUniversal KZG params (reusable)
VerificationConstant timeLogarithmic

Whether using the CLI or inline prove {} blocks, the proof pipeline follows the same steps:

Source → Parse → AST → IR → Optimize → Compile → Witness → Verify → Prove
  1. Parse: source code to AST
  2. IR Lowering: AST to SSA intermediate representation, extracting public/witness declarations
  3. Optimize: const_fold, dce, bool_prop passes reduce constraint count
  4. Compile: IR instructions become R1CS constraints or Plonkish table rows
  5. Witness: concrete input values fill the witness vector/table
  6. Verify: constraints are checked against the witness (catches bugs before proving)
  7. Prove: cryptographic proof is generated

The circuit command compiles a standalone .ach file:

Terminal window
# R1CS (default): compile + witness + export .r1cs/.wtns
achronyme circuit multiply.ach --inputs "x=6,y=7,out=42"
# Plonkish: compile + verify
achronyme circuit multiply.ach --backend plonkish --inputs "x=6,y=7,out=42"
# Plonkish with proof generation
achronyme circuit multiply.ach --backend plonkish --inputs "x=6,y=7,out=42" --prove

With the R1CS backend, the CLI exports two binary files:

  • circuit.r1cs: the constraint system in iden3 format (snarkjs-compatible)
  • circuit.wtns: the witness vector in iden3 format

These can be used directly with snarkjs for Groth16 proof generation:

Terminal window
# Using snarkjs (external)
snarkjs r1cs info circuit.r1cs
snarkjs wtns check circuit.r1cs circuit.wtns
snarkjs groth16 setup circuit.r1cs pot_final.ptau circuit.zkey
snarkjs groth16 prove circuit.zkey circuit.wtns proof.json public.json
snarkjs groth16 verify vkey.json public.json proof.json

Or let Achronyme handle it natively — see the prove {} block section below.

With --prove, the Plonkish backend generates:

  • proof.json: the KZG-PlonK proof (hex-encoded bytes)
  • public.json: public input values
  • vkey.json: the verifying key (hex-encoded)

For R1CS/Groth16, you can generate an on-chain verifier:

Terminal window
achronyme circuit multiply.ach --inputs "x=6,y=7,out=42" --solidity Verifier.sol

This produces a Solidity contract that verifies Groth16 proofs for the specific circuit.

Prove blocks let you generate proofs inline within regular Achronyme programs:

let secret = 42
let hash = 0p18569430475105882...
let p = prove(hash: Public) {
assert_eq(poseidon(secret, 0), hash)
}
// p is a proof object
print(proof_json(p))
print(proof_public(p))
print(proof_vkey(p))
  1. The VM encounters the prove {} block
  2. Variables from the outer scope are captured — secret and hash become circuit inputs
  3. Int values are automatically converted to field elements (the only place this happens implicitly)
  4. The block is compiled as a circuit, a witness is generated from captured values, and a proof is produced
  5. The result is a ProofObject on the heap, accessible via native functions

A prove block returns one of:

  • Proof: contains proof_json, public_json, and vkey_json strings
  • VerifiedOnly: constraints were verified but no proof was generated (verify-only mode)
FunctionReturnsDescription
proof_json(p)StringThe proof data (Groth16 or PlonK format)
proof_public(p)StringPublic inputs as a JSON array of decimal strings
proof_vkey(p)StringThe verification key

Prove blocks support both backends:

  • R1CS + Groth16 (default): native via ark-groth16
  • Plonkish + KZG-PlonK: native via halo2

Proof generation requires cryptographic keys (proving key + verifying key). These are expensive to compute, so Achronyme caches them:

  • Location: ~/.achronyme/cache/
  • R1CS (Groth16): cached by a SHA256 hash of the constraint system structure. Same circuit = same keys, even across runs.
  • Plonkish (KZG): universal params cached by k (the log2 of table size). Reusable across different circuits of the same size.

On first run for a new circuit, key generation may take a few seconds. Subsequent runs with the same circuit structure are near-instant.

The witness is the complete assignment of values to all circuit wires — inputs, intermediates, and outputs. The compiler builds it in three passes:

  1. Evaluate: runs the IR with concrete inputs for early validation. Catches assertion failures, division by zero, and missing inputs before emitting any constraints.
  2. Compile: lowers IR to constraints, recording a trace of WitnessOp instructions as a side effect.
  3. Replay: fills the witness vector by replaying the ops trace with concrete values.

Each intermediate wire is computed by a recorded operation:

OpDescription
AssignLCEvaluate a linear combination
MultiplyMultiply two LCs
InverseCompute modular inverse
BitExtractExtract the n-th bit from a field element
IsZeroIsZero gadget: set result and inverse
PoseidonHashCompute Poseidon permutation (fills ~360 internal wires)

The pipeline reports errors at the earliest possible stage:

StageErrorExample
IR LoweringParse/declaration errorsvariable 'x' not declared
EvaluationAssertion/arithmetic errorsassert_eq failed: 5 != 6
CompilationConstraint errorsdivision by zero
VerificationUnsatisfied constraintsconstraint 3 failed
Proof GenerationCryptographic errorsGroth16 setup failed

The early evaluation pass is intentional: it catches logical errors before spending time on constraint generation and proving.

  • R1CS — how constraints work in the R1CS backend
  • Plonkish — how the Plonkish backend works
  • Circuit Overview — writing circuits in Achronyme
  • Builtins — built-in circuit functions and their costs