Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 236 additions & 11 deletions noir-projects/fnd/noir-protocol-circuits/crates/blob/src/blob.nr
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ pub fn validate_factor(z_pow_d: BLS12_381_Fr, factor: BLS12_381_Fr) {
pub fn compute_factor(z: BLS12_381_Fr) -> BLS12_381_Fr {
let z_pow_d = z_pow_d(z);

// The barycentric formula only holds off the evaluation domain. At a d-th root of unity the
// factor is zero and p(z) collapses to zero for every blob, and `validate_fracs` has no
// solution there; asserting on `z^d` names that precondition in one place.
z_pow_d.assert_is_not_equal(BLS12_381_Fr::one());

// Safety: We immediately check that this result is correct in the following `evaluate_quadratic_expression` call.
let factor = unsafe { __compute_factor_helper(z_pow_d) };

Expand All @@ -195,18 +200,89 @@ pub fn compute_factor(z: BLS12_381_Fr) -> BLS12_381_Fr {
factor
}

// Compute the fracs: w^i / (z - w^i) for all i
/// Compute the fracs: w^i / (z - w^i) for all i.
///
/// The roots of unity let all `d` inverses be built from a single one, rather than from a
/// `d`-element batch inversion.
///
/// Write `z_k = z^(2^k)` and `w_k = w^(2^k)`, so `w_k` is a primitive `(d / 2^k)`-th root of
/// unity, and let
///
/// I_k[t] = 1 / (z_k - w_k^t), t = 0 .. d/2^k - 1
///
/// The top level `k = log2(d)` is a single entry, `1 / (z^d - 1)`, costing one inversion. Each
/// level below follows from the one above by a difference of two squares: with `m = d / 2^k`,
/// and using `w_{k-1}^(t+m) = -w_{k-1}^t`,
///
/// (z_{k-1} - w_{k-1}^t)(z_{k-1} + w_{k-1}^t) = z_k - w_k^t
///
/// so a single parent yields two children for two multiplications:
///
/// I_{k-1}[t] = (z_{k-1} + w_{k-1}^t) * I_k[t]
/// I_{k-1}[t + m] = (z_{k-1} - w_{k-1}^t) * I_k[t]
///
/// `ROOTS` is stored bit-reversed (the EIP-4844 blob layout), which makes the indexing fall out:
/// a parent at array index `u` has its two children at `2u` and `2u + 1`, and the root it needs,
/// `w_{k-1}^t`, is exactly `ROOTS[2u]` at every level. Entries are written in place, descending
/// through `u` so a child index never lands on a parent still to be read.
///
/// The last level folds the `w^i` numerator in as it descends. There `ROOTS[2u] = w^t` and
/// `ROOTS[u] = w^2t`, giving
///
/// fracs[2u] = (z*w^t + w^2t) * I_1[u]
/// fracs[2u + 1] = -(z*w^t - w^2t) * I_1[u]
///
/// which shares the `z*w^t` product across the pair: 3 multiplications per pair, against the 4 a
/// separate numerator pass would need.
///
/// Total: ~2.5d multiplications and one inversion, against ~4d and one inversion for the
/// batch-inversion form (Montgomery's trick alone costs 3 multiplications per element).
///
/// Requires `z^d != 1`, i.e. `z` must not itself be a d-th root of unity, and asserts it: at a
/// root the single inversion would be of zero, which `__invmod` maps to zero, and every entry
/// derived from it would be zero too. `validate_fracs` has no solution there whatever the hint
/// returns, because `fracs[i] * 0 = ROOTS[i]` is unsatisfiable at the index where
/// `z = ROOTS[i]`, and reaching a root needs a Poseidon2 preimage.
unconstrained fn __compute_fracs(z: BLS12_381_Fr) -> [BLS12_381_Fr; FIELDS_PER_BLOB] {
let mut denoms = [BLS12_381_Fr::zero(); FIELDS_PER_BLOB];
for i in 0..FIELDS_PER_BLOB {
denoms[i] = z.__sub(ROOTS[i]); // (z - w^i)
// z_pow[k] = z^(2^k)
let mut z_pow = [BLS12_381_Fr::zero(); LOG_FIELDS_PER_BLOB + 1];
z_pow[0] = z;
for k in 1..(LOG_FIELDS_PER_BLOB + 1) {
z_pow[k] = z_pow[k - 1].__mul(z_pow[k - 1]);
}
let inv_denoms: [BLS12_381_Fr; FIELDS_PER_BLOB] = bignum::bignum::batch_invert(denoms); // 1 / (z - w^i), for all i
// We're now done with `denoms` so we can reuse the allocated array to build `fracs`.
let mut fracs = [BLS12_381_Fr::zero(); FIELDS_PER_BLOB]; // aiming for: w^i / (z - w^i), for all i
for i in 0..FIELDS_PER_BLOB {
let inv_denom = inv_denoms[i]; // 1 / (z - w^i)
fracs[i] = ROOTS[i].__mul(inv_denom); // w^i * (1 / (z - w^i))

// The single inverse everything else is derived from. It is zero iff `z` is a d-th root of
// unity, which the precondition excludes; fail here rather than return an all-zero array.
assert(
z_pow[LOG_FIELDS_PER_BLOB] != BLS12_381_Fr::one(),
"blob challenge z is a d-th root of unity",
);
let mut inverses = [BLS12_381_Fr::zero(); FIELDS_PER_BLOB];
inverses[0] = z_pow[LOG_FIELDS_PER_BLOB].__sub(BLS12_381_Fr::one()).__invmod();

// Descend to level 1, doubling the number of live entries each time.
let mut num_entries: u32 = 1;
for step in 0..(LOG_FIELDS_PER_BLOB - 1) {
let k = LOG_FIELDS_PER_BLOB - step;
for i in 0..num_entries {
let u = num_entries - 1 - i;
let inverse = inverses[u];
let root = ROOTS[2 * u];
inverses[2 * u] = z_pow[k - 1].__add(root).__mul(inverse);
inverses[2 * u + 1] = z_pow[k - 1].__sub(root).__mul(inverse);
}
num_entries *= 2;
}

// Final level: turn I_1 into the fracs, folding in the w^i numerator.
let mut fracs = [BLS12_381_Fr::zero(); FIELDS_PER_BLOB];
for u in 0..num_entries {
let inverse = inverses[u];
let root = ROOTS[2 * u]; // w^t
let root_squared = ROOTS[u]; // w^2t
let z_root = z.__mul(root);
fracs[2 * u] = z_root.__add(root_squared).__mul(inverse);
fracs[2 * u + 1] = z_root.__sub(root_squared).__mul(inverse).__neg();
}

fracs
Expand Down Expand Up @@ -566,6 +642,107 @@ mod tests {
}
}

/// `__compute_fracs` derives every entry from a single inversion by halving down the roots
/// of unity, so an error in one level would corrupt a whole contiguous half of the output.
/// Spot-checking a few indices would miss that; check the defining relation everywhere.
/// For any `z` with `z^d != 1` each relation pins its `fracs[i]` uniquely.
unconstrained fn assert_fracs_satisfy_defining_relation_everywhere(z: BLS12_381_Fr) {
let fracs = __compute_fracs(z);

for i in 0..FIELDS_PER_BLOB {
// fracs[i] * (z - w^i) == w^i
assert_eq(fracs[i].__mul(z.__sub(ROOTS[i])), ROOTS[i]);
}
}

#[test]
unconstrained fn test_compute_fracs_satisfies_defining_relation_everywhere() {
let two = BLS12_381_Fr::from_limbs([2, 0, 0]);
let three = BLS12_381_Fr::from_limbs([3, 0, 0]);
let challenges = [
// A challenge of the size Poseidon2 produces in the rollup.
BLS12_381_Fr::from(
0x1f4a21e3a1ab23739c164d0df1596c870180aab5f810c548097dd6371ac3186d,
),
// Small values.
BLS12_381_Fr::zero(),
two,
BLS12_381_Fr::from_limbs([42, 0, 0]),
// Either side of the 120-bit limb boundaries.
BLS12_381_Fr::from_limbs([0xffffffffffffffffffffffffffffff, 0, 0]),
BLS12_381_Fr::from_limbs([0, 1, 0]),
BLS12_381_Fr::from_limbs([1, 1, 0]),
BLS12_381_Fr::from_limbs([
0xffffffffffffffffffffffffffffff,
0xffffffffffffffffffffffffffffff,
0,
]),
BLS12_381_Fr::from_limbs([0, 0, 1]),
// The top of the BLS12-381 scalar field. `-1` itself is `ROOTS[1]`, a d-th root of
// unity, and so outside the precondition; `-2` and `-3` are not roots.
two.__neg(),
three.__neg(),
];

for j in 0..challenges.len() {
assert_fracs_satisfy_defining_relation_everywhere(challenges[j]);
}
}

/// At `z = 0` every fraction is `w^i / (0 - w^i) = -1`, so the whole array is known in
/// closed form.
#[test]
unconstrained fn test_compute_fracs_at_zero_is_all_minus_one() {
let minus_one = BLS12_381_Fr::one().__neg();

let fracs = __compute_fracs(BLS12_381_Fr::zero());

for i in 0..FIELDS_PER_BLOB {
assert_eq(fracs[i], minus_one);
}
}

// `__compute_fracs` requires `z^d != 1` and asserts it: at a d-th root of unity the one
// inversion would be of zero, which `__invmod` maps to zero, and every derived entry with it.
// Checked at 1, -1 and a root with no special structure.

#[test(should_fail_with = "blob challenge z is a d-th root of unity")]
unconstrained fn test_compute_fracs_rejects_z_equal_to_one() {
let _ = compute_fracs(ROOTS[0]);
}

#[test(should_fail_with = "blob challenge z is a d-th root of unity")]
unconstrained fn test_compute_fracs_rejects_z_equal_to_minus_one() {
let _ = compute_fracs(ROOTS[1]);
}

#[test(should_fail_with = "blob challenge z is a d-th root of unity")]
unconstrained fn test_compute_fracs_rejects_z_equal_to_nontrivial_root_of_unity() {
let _ = compute_fracs(ROOTS[1000]);
}

/// The halving reads a parent's two children as `2u` / `2u + 1` and its root as `ROOTS[2u]`.
/// That only works because `ROOTS` is stored bit-reversed, which lands every root next to its
/// own negation. Assuming natural order instead silently corrupts half the output, so pin the
/// property the indexing rests on.
#[test]
unconstrained fn test_roots_are_stored_adjacent_to_their_negations() {
for u in 0..(FIELDS_PER_BLOB / 2) {
assert_eq(ROOTS[2 * u + 1], ROOTS[2 * u].__neg());
}
}

/// The other half of the indexing invariant: descending a level squares the root, and the
/// bit-reversed layout puts `ROOTS[2u]^2` at the parent's index `u`. The final level reads
/// `ROOTS[u]` as `w^2t` on the strength of this. Adjacent negation alone does not imply it:
/// reordering intact `(r, -r)` pairs keeps every pair adjacent but breaks the parent lookup.
#[test]
unconstrained fn test_roots_square_to_their_parent() {
for u in 0..(FIELDS_PER_BLOB / 2) {
assert_eq(ROOTS[2 * u].__mul(ROOTS[2 * u]), ROOTS[u]);
}
}

#[test]
unconstrained fn test_zero_blob() {
// Test that evaluating an all-zeros blob returns zero
Expand Down Expand Up @@ -733,7 +910,8 @@ mod tests {
// ==================== SOUNDNESS TESTS ====================
// These tests verify that invalid hints are rejected by the constraints.
// The BigNum library's evaluate_quadratic_expression asserts `remainder == [0; N]`
// when the constraint is not satisfied.
// when the constraint is not satisfied. That assertion carries no message, so the
// `validate_*` tests below can only say `should_fail`.

#[test(should_fail)]
fn test_validate_factor_rejects_invalid_factor() {
Expand Down Expand Up @@ -804,6 +982,53 @@ mod tests {
validate_fracs(z, fracs);
}

#[test(should_fail)]
fn test_validate_fracs_rejects_invalid_frac_at_last_index() {
// The last pair is the final one written by the halving's last level; corrupt its second
// entry rather than only ever exercising index 0.
let z = BLS12_381_Fr::from_limbs([42, 0, 0]);
let last = FIELDS_PER_BLOB - 1;

// Safety: computing correct values first
let mut fracs = unsafe { __compute_fracs(z) };
fracs[last] = unsafe { fracs[last].__add(BLS12_381_Fr::one()) };

validate_fracs(z, fracs);
}

/// At `z = ROOTS[j]` the constraint at index `j` reads `fracs[j] * 0 = ROOTS[j]`, so no array
/// satisfies `validate_fracs`. The hint array here is correct at every other index, so the
/// rejection is that index alone, independent of the hint's own assertion.
#[test(should_fail)]
fn test_validate_fracs_rejects_root_of_unity_for_any_fracs() {
let j: u32 = 1000;
let z = ROOTS[j];

// Safety: test hint. `batch_invert` leaves the one zero denominator (index `j`) as zero
// and inverts the rest, so every entry but `j` satisfies its constraint by construction.
let fracs = unsafe {
let mut denoms = [BLS12_381_Fr::zero(); FIELDS_PER_BLOB];
for i in 0..FIELDS_PER_BLOB {
denoms[i] = z.__sub(ROOTS[i]);
}
let inv_denoms = bignum::bignum::batch_invert(denoms);
let mut fracs = [BLS12_381_Fr::zero(); FIELDS_PER_BLOB];
for i in 0..FIELDS_PER_BLOB {
fracs[i] = ROOTS[i].__mul(inv_denoms[i]);
}
fracs
};

validate_fracs(z, fracs);
}

/// `compute_factor` asserts `z^d != 1` in-circuit: at a root the factor is zero and the
/// evaluation would collapse to zero for every blob.
#[test(should_fail_with = "assert_is_not_equal fail")]
fn test_compute_factor_rejects_root_of_unity() {
let _ = compute_factor(ROOTS[1]);
}

#[test(should_fail)]
fn test_validate_fracs_rejects_nonzero_frac_for_zero_y() {
// Test that when y[i] = 0, fracs[i] must also satisfy the constraint
Expand Down
Loading