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 capturedsecret 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