Skip to content

perf(blobs): derive the blob fracs by halving the roots of unity - #25444

Merged
AztecBot merged 1 commit into
nextfrom
cb/blob-fracs-halving
Sep 10, 2026
Merged

perf(blobs): derive the blob fracs by halving the roots of unity#25444
AztecBot merged 1 commit into
nextfrom
cb/blob-fracs-halving

Conversation

@AztecBot

@AztecBot AztecBot commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Refs #10323. Independent of the rest of that work — it lands on next on its own.

__compute_fracs builds w^i / (z - w^i) for all 4096 blob positions. It was a
4096-element Montgomery batch inversion (3 multiplications per element) plus a
pass to apply the numerator: ~4d multiplications and one inversion. The roots of
unity make almost all of that unnecessary.

The identity

Write z_k = z^(2^k) and w_k = w^(2^k), and let I_k[t] = 1 / (z_k - w_k^t)
over t = 0 .. d/2^k - 1. The top level is a single entry, 1 / (z^d - 1) — the
one inversion. Each level below follows by a difference of two squares, since
w_{k-1}^(t+m) = -w_{k-1}^t for m = d/2^k:

(z_{k-1} - w_{k-1}^t)(z_{k-1} + w_{k-1}^t) = z_k - w_k^t

so one 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]

Levels halve going up, so reaching I_1 costs about d multiplications in
total. The last level folds the w^i numerator in as it descends, sharing the
z*w^t product across each pair — 3 multiplications per pair rather than the 4 a
separate numerator pass needs.

Total: ~2.5d multiplications and one inversion, against ~4d and one inversion.

The indexing falls out of the EIP-4844 layout

ROOTS is stored bit-reversed. That turns out to make the recursion trivial to
index: a parent at array index u has its children at 2u and 2u + 1, and the
root it needs is ROOTS[2u] at every level. At the final level ROOTS[2u] is
w^t and ROOTS[u] is w^2t, so both terms are plain lookups.

I got this wrong first time by assuming natural order — the halves are i and
i + d/2 there, not 2u and 2u + 1 — and it produced fracs that failed the
defining relation. test_roots_are_stored_adjacent_to_their_negations pins the
property the indexing rests on, because the failure mode is silent corruption of
half the array.

Measurements

compute_fracs alone, native ACVM, harness overhead subtracted, min of 5 on a
loaded shared host:

time
batch inversion 0.915s
roots-of-unity halving 0.585s

1.56x, saving ~0.33s.

This is a hint used by both execution paths, so it should take roughly the same
~0.33s off constrained witness generation as well — __compute_fracs is ~0.86s
of the ~1.4s of Brillig hint work inside the ~11s constrained
rollup-checkpoint-root witness generation. I have not measured the constrained
side (it needs an 8-minute circuit compile per arm); worth confirming before
quoting that number.

It also matters for #10323: with the oracle removed (#25442) this takes a
checkpoint-root simulation from ~1.40s to ~1.05s.

Correctness

The ACIR is untouched — this only changes how the hint is produced, and
validate_fracs constrains the result exactly as before. The existing
test_validate_fracs_* tests pass, which is the real contract: the new hint
still satisfies the circuit's constraints.

New test_compute_fracs_satisfies_defining_relation_everywhere checks
fracs[i] * (z - ROOTS[i]) == ROOTS[i] for all 4096 entries rather than
spot-checking a few indices, because an error at one level of the recursion
corrupts a whole contiguous run of the output and a spot check would miss it.

Degenerate input behaves as before: if z is itself a d-th root of unity the
single inversion is zero and the array degenerates to zeros, exactly as
batch_invert mapped a zero denominator to zero. validate_fracs rejects that
either way, and reaching it requires a Poseidon2 preimage.

Ran: the blob crate's fracs and validate_fracs tests (6 passing) plus the
new invariant test. Not the full suite — CI is the complete check. The blob and
bignum crates are byte-identical between the commit those runs were made
against and current next, so the rebase did not move the code under the
measurements or the test results.


Created by claudebox · group: slackbot · requested by Tom (@TomAFrench) · Slack thread

@AztecBot AztecBot added ci-draft Run CI on draft PRs. ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure claude-review Triggers an automated Claude code review claudebox Owned by claudebox. it can push to this PR. labels Sep 10, 2026
@AztecBot

Copy link
Copy Markdown
Collaborator Author

Claude Review — starting review session...

@AztecBot

AztecBot commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Adversarial verdict: PASS

Claude Review — completed
live status
1m

VERDICT: PASS

070b810 has an identical repository tree to reviewed 8a155dc. Five focused tests and 192,512 arithmetic comparisons passed again; the prior 53-test full-suite result applies. No blocking findings.

Review and evidence

gist

@AztecBot

Copy link
Copy Markdown
Collaborator Author

Claude Review: Starting automated code review... workflow run

@AztecBot

Copy link
Copy Markdown
Collaborator Author

Claude Review: Starting automated code review... workflow run

@AztecBot AztecBot added claude-review-complete Claude code review has been completed claude-review-passed Adversarial ClaudeBox review passed and removed claude-review Triggers an automated Claude code review labels Sep 10, 2026
@AztecBot
AztecBot changed the base branch from cb/unconstrained-blob-eval-speedup to next September 10, 2026 07:47
@AztecBot
AztecBot force-pushed the cb/blob-fracs-halving branch from 442b9ee to c944189 Compare September 10, 2026 07:47
@AztecBot AztecBot added claude-review Triggers an automated Claude code review and removed claude-review-complete Claude code review has been completed claude-review-passed Adversarial ClaudeBox review passed labels Sep 10, 2026
@AztecBot

Copy link
Copy Markdown
Collaborator Author

Claude Review: Starting automated code review... workflow run

@AztecBot AztecBot added claude-review-complete Claude code review has been completed claude-review-passed Adversarial ClaudeBox review passed and removed claude-review Triggers an automated Claude code review claude-review-passed Adversarial ClaudeBox review passed labels Sep 10, 2026
@TomAFrench
TomAFrench marked this pull request as ready for review September 10, 2026 08:14

@iAmMichaelConnor iAmMichaelConnor left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved pending merge of #25450 into this branch

TomAFrench pushed a commit that referenced this pull request Sep 10, 2026
…s coverage (#25450)

Follow-up to #25444, targeting its branch `cb/blob-fracs-halving`. One
squashed commit; the diff touches only
`noir-projects/fnd/noir-protocol-circuits/crates/blob/src/blob.nr`. The
vendored bignum crate is untouched.

## Why

The halving in `__compute_fracs` derives all 4096 inverses from one
inversion of `z^d - 1`, so it requires `z^d != 1`. #25444's comment and
body say a root-of-unity challenge "behaves as before". It does not:
`batch_invert` skipped the single zero denominator and returned the
other 4095 fractions intact, whereas the halving returns all zeros. The
difference is unobservable (at a root `factor = 0` so `y_i = 0` either
way, and `validate_fracs` has no solution there), but the claim was
wrong and the precondition was implicit.

## What changes

- `compute_factor` already has `z^d` in-circuit, so it asserts `z^d !=
1` there with bignum's `assert_is_not_equal` (a few gates, sound against
the `z^d = 1 + MOD` alias). `validate_fracs` is unsatisfiable at a root
anyway, so this changes no accepted statement; it names the precondition
in one place. It is a small ACIR change, so #25444's "The ACIR is
untouched" no longer holds once this lands.
- The `__compute_fracs` hint asserts the same condition on its own `z^d`
with the message `blob challenge z is a d-th root of unity` rather than
returning an all-zero array. Its doc comment states the precondition and
drops the equivalence claim.

## Tests

- The all-entry defining-relation check now runs over a corpus of eleven
challenges (zero, small values, both sides of each 120-bit limb
boundary, `-2` and `-3` at the top of the field, and a Poseidon2-sized
value) instead of one. `-1` is deliberately absent: it is `ROOTS[1]`, a
root of unity.
- Every fraction is `-1` at `z = 0`.
- The hint rejects `z = 1`, `-1` and `ROOTS[1000]` with its own message
(`should_fail_with`), and `compute_factor` rejects a root in-circuit
with `assert_is_not_equal fail`.
- `validate_fracs` rejects a root even when handed an array that is
correct at every other index (built with `batch_invert`, exactly the old
hint), showing the constraint itself has no solution independent of the
hint.
- The squared-parent property `ROOTS[2u]^2 == ROOTS[u]`, which the final
level of the halving relies on, is pinned next to the existing
adjacent-negation check.
- A corrupted fraction at the last index is rejected, not only at index
0.

Rejections that fail inside bignum's `evaluate_quadratic_expression`
stay plain `should_fail`: that assertion carries no message, and giving
it one belongs in a bignum change rather than here.

Ran the full `blob` crate with the repository-pinned compiler (Noir
1.0.0-rc.0, `fae3abca7e51`): 53 tests pass, up from 45 at the #25444
head.

Analysis and derivation:
https://gist.github.com/AztecBot/bfe643f0f99557cf0f6266eefb433c55

---
*Created by
[claudebox](https://claudebox.work/v2/sessions/a85ec915f2c7003c/jobs/10)
· group: `slackbot` · requested by Mike (@iAmMichaelConnor) · [Slack
thread](https://aztecfoundation.slack.com/archives/D0B2N7W1WJD/p1789036805405059?thread_ts=1789036805.405059&cid=D0B2N7W1WJD)*
@AztecBot AztecBot added claude-review Triggers an automated Claude code review and removed claude-review-complete Claude code review has been completed claude-review-passed Adversarial ClaudeBox review passed labels Sep 10, 2026
@AztecBot

Copy link
Copy Markdown
Collaborator Author

Claude Review: Starting automated code review... workflow run

Refs #10323. Independent of the rest of that work — it lands on `next` on its own.

`__compute_fracs` builds `w^i / (z - w^i)` for all 4096 blob positions. It was a
4096-element Montgomery batch inversion (3 multiplications per element) plus a
pass to apply the numerator: ~4d multiplications and one inversion. The roots of
unity make almost all of that unnecessary.

## The identity

Write `z_k = z^(2^k)` and `w_k = w^(2^k)`, and let `I_k[t] = 1 / (z_k - w_k^t)`
over `t = 0 .. d/2^k - 1`. The top level is a single entry, `1 / (z^d - 1)` — the
one inversion. Each level below follows by a difference of two squares, since
`w_{k-1}^(t+m) = -w_{k-1}^t` for `m = d/2^k`:

```
(z_{k-1} - w_{k-1}^t)(z_{k-1} + w_{k-1}^t) = z_k - w_k^t
```

so one 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]
```

Levels halve going up, so reaching `I_1` costs about `d` multiplications in
total. The last level folds the `w^i` numerator in as it descends, sharing the
`z*w^t` product across each pair — 3 multiplications per pair rather than the 4 a
separate numerator pass needs.

Total: ~2.5d multiplications and one inversion, against ~4d and one inversion.

## The indexing falls out of the EIP-4844 layout

`ROOTS` is stored bit-reversed. That turns out to make the recursion trivial to
index: a parent at array index `u` has its children at `2u` and `2u + 1`, and the
root it needs is `ROOTS[2u]` at every level. At the final level `ROOTS[2u]` is
`w^t` and `ROOTS[u]` is `w^2t`, so both terms are plain lookups.

I got this wrong first time by assuming natural order — the halves are `i` and
`i + d/2` there, not `2u` and `2u + 1` — and it produced fracs that failed the
defining relation. `test_roots_are_stored_adjacent_to_their_negations` pins the
property the indexing rests on, because the failure mode is silent corruption of
half the array.

## Measurements

`compute_fracs` alone, native ACVM, harness overhead subtracted, min of 5 on a
loaded shared host:

| | time |
|---|---|
| batch inversion | 0.915s |
| roots-of-unity halving | 0.585s |

**1.56x**, saving ~0.33s.

This is a hint used by *both* execution paths, so it should take roughly the same
~0.33s off constrained witness generation as well — `__compute_fracs` is ~0.86s
of the ~1.4s of Brillig hint work inside the ~11s constrained
`rollup-checkpoint-root` witness generation. I have not measured the constrained
side (it needs an 8-minute circuit compile per arm); worth confirming before
quoting that number.

It also matters for #10323: with the oracle removed (#25442) this takes a
checkpoint-root simulation from ~1.40s to ~1.05s.

## Correctness

The ACIR is untouched — this only changes how the hint is produced, and
`validate_fracs` constrains the result exactly as before. The existing
`test_validate_fracs_*` tests pass, which is the real contract: the new hint
still satisfies the circuit's constraints.

New `test_compute_fracs_satisfies_defining_relation_everywhere` checks
`fracs[i] * (z - ROOTS[i]) == ROOTS[i]` for **all** 4096 entries rather than
spot-checking a few indices, because an error at one level of the recursion
corrupts a whole contiguous run of the output and a spot check would miss it.

Degenerate input behaves as before: if `z` is itself a d-th root of unity the
single inversion is zero and the array degenerates to zeros, exactly as
`batch_invert` mapped a zero denominator to zero. `validate_fracs` rejects that
either way, and reaching it requires a Poseidon2 preimage.

Ran: the `blob` crate's `fracs` and `validate_fracs` tests (6 passing) plus the
new invariant test. Not the full suite — CI is the complete check. The `blob` and
`bignum` crates are byte-identical between the commit those runs were made
against and current `next`, so the rebase did not move the code under the
measurements or the test results.

---
*Created by [claudebox](https://claudebox.work/v2/sessions/99aba4482349eaa5/jobs/9) · group: `slackbot` · requested by Tom (@TomAFrench) · [Slack thread](https://aztecfoundation.slack.com/archives/D0B586H14KG/p1788969921421879?thread_ts=1788969921.421879&cid=D0B586H14KG)*
@AztecBot
AztecBot force-pushed the cb/blob-fracs-halving branch from 8a155dc to 070b810 Compare September 10, 2026 14:26
@AztecBot
AztecBot enabled auto-merge September 10, 2026 14:26
@AztecBot
AztecBot added this pull request to the merge queue Sep 10, 2026
@AztecBot AztecBot added claude-review-complete Claude code review has been completed claude-review-passed Adversarial ClaudeBox review passed and removed claude-review Triggers an automated Claude code review labels Sep 10, 2026
Merged via the queue into next with commit 7cf6959 Sep 10, 2026
35 of 37 checks passed
@AztecBot
AztecBot deleted the cb/blob-fracs-halving branch September 10, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-draft Run CI on draft PRs. ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure claude-review-complete Claude code review has been completed claude-review-passed Adversarial ClaudeBox review passed claudebox Owned by claudebox. it can push to this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants