Skip to content

chore: merge v5 into v5-next - #24700

Merged
nventuro merged 890 commits into
v5-nextfrom
cb/merge-v5-into-v5-next-20260714
Jul 15, 2026
Merged

chore: merge v5 into v5-next#24700
nventuro merged 890 commits into
v5-nextfrom
cb/merge-v5-into-v5-next-20260714

Conversation

@AztecBot

@AztecBot AztecBot commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Merges public v5 into public v5-next so ongoing development can continue on v5-next while v5 stays the release line. Requested in #engineering.

v5 was ~887 commits ahead of the merge base and v5-next ~31, and the merge is not clean — 4 conflicts. Structured as three commits for review:

Commit 1 — raw merge (conflict markers committed)

Merge remote-tracking branch 'origin/v5' — the merge commit with the conflict markers left in place, so you can see exactly what git couldn't auto-resolve:

  • yarn-project/txe/esbuild/plugins/size_guard.mjs
  • noir-projects/aztec-nr/aztec/src/standard_addresses.nr (generated)
  • yarn-project/standard-contracts/src/standard_contract_data.ts (generated)
  • noir-projects/noir-contracts/pinned-standard-contracts.tar.gz (generated, binary)

Commit 2 — hand resolution

  • size_guard.mjs — resolved by hand. Kept the higher totalLimitMiB = 15 (from v5): merging v5's larger bundle content into v5-next needs the bigger cap. Merged both bump-log lines chronologically.
  • The 3 generated standard-contract artifacts were left at the v5-next baseline as a placeholder (correctly flagged by CI: BBApiException: verification key has wrong size: expected 5216, got 4576 — the old pinned VKs don't match v5's bb).

Commit 3 — regenerated standard-contract artifacts

Ran the real regeneration on the merged tree: noir-projects/noir-contracts/bootstrap.sh pin-standard-build + yarn workspace @aztec/standard-contracts run generate, iterated to a fixpoint (3 rounds — the standard contracts embed each other's address stamps via aztec-nr, so re-pinning shifts addresses until they stabilize). All four standard-contract addresses changed, as expected: the merged tree combines v5-next's handshake forgery-protection changes with v5's aztec-nr + protocol-constants + bb (VK format) changes, so the addresses match neither committed side.

Verified locally on the merged tree: full noir-contracts build and full yarn-project build (the CI job that failed) both pass, including the standard-contracts drift check.

Everything else auto-merged (including constants.gen.ts / constants.nr and the handshake contract/aztec-nr source). Opened as a draft for review per the usual conflict-PR flow.


Created by claudebox · group: slackbot

IlyasRidhuan and others added 30 commits May 29, 2026 08:21
fix(bb): keep lookup trace block early to fix ultra_honk +350MiB regression
* feat(bb): fake-GLV ECDSA verification for secp256r1

Implement secp256r1-specific ECDSA scalar multiplication using the
fake-GLV idea: instead of a single 256-bit 2-MSM, the verifier asks
the prover for half-size scalar decompositions (α_i, β_i) such that
α_i · P_i ≡ β_i · T_i (mod n) with |α_i|, |β_i| ≈ √n, then checks
each pair as an independent 2-MSM at 129 bits and sums the hints.

Soundness:
- T₁ and T₂ are computed natively from prover-supplied (α, β) and
  verified by asserting each 2-MSM equals the point at infinity.
- β_i ≠ 0 is asserted (rules out the trivial (0, 0) decomposition).
- u_i ∈ {0, 1, n−1} is substituted to 2 in-circuit before native
  multiplication to avoid degenerate point inputs to from_witness.
- Off-curve public keys are substituted to 2·G upstream before the
  native T₂ = u₂·Q multiplication, so from_witness only sees valid
  curve points. The original is_point_on_curve bit still flows into
  the validity output.

The two-2-MSM structure was chosen over a single 4-MSM because the
4-MSM variant admits a forgery: with T₁, T₂ as free witnesses and
only a single combined infinity assertion, an adversary can solve
a linear system for T₁ to make the equation hold without knowing a
discrete log. Splitting into two assertions binds each hint to its
own base independently.

Gate count is currently ~72k vs the ~68.5k baseline; the non-native
doubling cost dominates so two 129-bit chains roughly match one
256-bit chain. Further optimization (custom Montgomery ladder, joint
lattice decomposition) will follow.

* feat(bb): Pedersen-style fixed-base mul u·G for secp256r1 via plookup

Add `element::secp256r1_fixed_base_mul(u)` which computes u·G without any
in-circuit doublings of a non-native accumulator. The 256-bit scalar is
sliced into 32 little-endian 8-bit windows; for each window position w,
the precomputed entry k holds the point `k · 2^(8w) · G + 2^w · H`, where
H is the "biggroup table offset generator". Five basic tables per window
cover the non-native limb axes (xlo, xhi, ylo, yhi, xyprime), giving
32 × 5 = 160 BasicTables total. Each window contributes 5 plookup gates
plus one chain-add into a running 32-point sum; the constant aggregate
offset (2^32 − 1) · H is subtracted at the end.

Table backend lives in plookup_tables/secp256r1_fixed_base.{hpp,cpp}:
the 32 × 256 native entries are materialised once per process under
`std::call_once`, and each BasicTable is the preprocessed lookup-poly
slice for one (axis, window) pair. Five contiguous BasicTableId ranges
(SECP256R1_FIXED_BASE_{XLO,XHI,YLO,YHI,XYPRIME}_0, 32 IDs each) are
reserved in types.hpp and dispatched in plookup_tables.cpp.

Cost for a single u·G call: 8,296 gates (trace) on UltraCircuitBuilder.
Table contents (≈41k rows × 3 columns) land in the preprocessed lookup
polynomial — shared across every call in the circuit, so batch ECDSA
verification does not pay them per signature.

Soundness:
- Each byte witness is range-constrained to 8 bits, and the plookup
  membership argument implicitly constrains it to column 1 of the table
  (= 0..255).
- The 32 bytes are reconstructed and asserted equal to u in Fr.
- Table entries are circuit constants, so the looked-up point is pinned
  by the range-checked byte witness.

Caller responsibility: u = 0 produces the all-offsets result so the final
subtraction lands at (0, 0) (off-curve). The ECDSA path already substitutes
u_i ∈ {0, ±1} → 2 upstream, so this is not a real concern in practice.

* feat(bb): secp256r1 ECDSA hybrid mul (fixed-base u₁·G + 4-bit wNAF ladder for u₂·Q)

Rewrite the body of `element::secp256r1_ecdsa_mul` so that the two scalar
multiplications use specialised paths:

  - u₁·G uses the Pedersen-style fixed-base plookup mul (no in-circuit
    doublings). T₁ is pinned to u₁·G by construction, so no fake-GLV
    identity is needed on the u₁ side.

  - u₂·Q keeps the fake-GLV structure (half-GCD → α₂, β₂ with |α|,|β| < √n,
    identity α₂·Q − β₂·T₂ = O) but verifies it via a custom 2-MSM
    Montgomery ladder rather than the generic batch_mul. The scalars use
    4-bit signed wNAFs (32 entries each, α₂ stagger=1, β₂ stagger=0); odd
    multiples of Q and T₂_msm live in 16-entry signed-wNAF ROM tables
    (reusing `four_bit_table_plookup`). The loop is 32 iters × (2 explicit
    dbl + 2-input Montgomery ladder), then α₂ stagger fragment + skew
    corrections; offset-generator subtraction at the end flips the
    accumulator's is_infinity flag iff the identity holds.

Wider 8-bit ROM tables (256 entries) were measured at 148k gates — the
≈127 in-circuit point-additions needed to build each 256-entry table
dominate. 4-bit reduces table-build cost ≈30× while still cutting wNAF
loop iterations by half versus the 1-bit NAF used by the previous
generic 2-MSM.

Soundness (u₂ side, unchanged from prior sound fake-GLV):
- β₂ range-constrained to 129 bits and asserted nonzero.
- β₂_signed · u₂ ≡ α₂ (mod n) enforced.
- α₂, |β₂| wNAFs reconstructed in-circuit and asserted equal to the
  bigfield witnesses.
- ROM table indices implicitly range-checked by the lookup.
- T₂ pinned to u₂·Q by the 2-MSM identity (given β₂ ≠ 0).

Substitutions: u₁ → 2 iff u₁ = 0 (avoids the fixed-base (0, 0) output);
u₂ → 2 iff u₂ ∈ {0, ±1} (avoids degenerate fake-GLV decomposition and
ROM-table x-coordinate collisions between Q and ±Q).

Measured gate count for a single secp256r1 ECDSA verify:
  72,082 (prior sound fake-GLV baseline)  →  44,933  (-38%)

All 63 stdlib ECDSA tests pass.

* refactor(bb): use generic batch_mul for the u₂·Q identity check in secp256r1 ECDSA

Replace the custom 4-bit-wNAF Montgomery ladder for the fake-GLV identity
α₂·Q − β₂·T₂ = O with a single `batch_mul({pubkey, T₂_msm}, {α₂, |β₂|},
129, false)` call. `batch_mul` runs a 4-NAF-columns-per-iteration Strauss
MSM over the standard 1-bit signed NAF with a 4-entry combined batch
lookup table over {±Q ± T₂_msm}; the terminating
`subtract_internal(offset_generator_end)` flips the is_infinity flag iff
the identity holds.

Gate-count comparison for a single secp256r1 ECDSA verify (u₁·G is the
fixed-base plookup mul in all hybrid variants):

  prior sound fake-GLV baseline (no fixed-base):        72,082
  hybrid + 8-bit custom wNAF ladder (256-entry ROM):   148,211
  hybrid + 4-bit custom wNAF ladder (16-entry  ROM):    44,933
  hybrid + 1-bit NAF via batch_mul                      44,385   ← this commit

The custom 8-bit ladder is dominated by in-circuit ROM table
construction (~127 chain-adds per 256-entry table × 2 tables). 4-bit and
1-bit collapse the table cost almost completely (~7 chain-adds for the
16-entry tables; ~3 for the combined 4-entry batch table) and end up
within ~600 gates of each other. The fixed-base path for u₁·G is what
unlocks the ~38% saving versus the prior baseline; the choice of u₂·Q
strategy moves the total by < 1k gates around 44–45k.

batch_mul is strictly simpler code-wise (no custom wNAF helper, no
manual ROM table construction, no offset-generator math, no template
hack of passing a uint256_t through a secp256k1::fr container) and is
marginally cheaper, so it's the preferred variant going forward.

All 63 stdlib ECDSA tests pass.

* perf(bb): tighten secp256r1 fake-GLV scalars to 128 bits

The half-GCD bound gives |α₂|, |β₂_abs| strictly < √n < 2¹²⁸ for secp256r1,
so drop the 129-bit max_num_bits in batch_mul and the 129-bit range checks to
128. EcdsaMulGateCount: 44,385 → 44,131 gates.

* refactor(bb): drop redundant u₁=0 substitution in secp256r1 ECDSA mul

`secp256r1_fixed_base_mul(0)` returns canonical infinity (raw_result equals
total_offset, and `operator-` detects the P − P case and sets is_infinity).
`T₁ + T₂` with T₁ = ∞ correctly returns T₂, so the upstream verifier sees
the right answer when u₁ = 0. Drop the `u1_safe` conditional_assign and
add an EcdsaMulU1Zero test pinning the behavior.

EcdsaMulGateCount: 44,131 → 44,063 gates.

* refactor(bb): bundle secp256r1 fixed-base plookup tables into MultiTables

Group the 160 BasicTables (32 windows × 5 axes) into 10 MultiTables — one
LO/HI pair per axis — and drive each with the bigfield Fr's u_low / u_high
limb-pair via `plookup_read::get_lookup_accumulators`. C1 step = 256 slices
the input scalar into 8-bit windows; C2/C3 step = 0 yields per-window
(limb_a, limb_b) values without accumulation. Saves ~116 gates on the full
ECDSA mul (44,063 → 43,947) by retiring the manual 32-byte witness +
explicit per-window lookup-gate emission.

* perf(bb): drop secp256r1 fixed-base XYPRIME plookup tables

Remove the per-window XYPRIME basic/multi tables that held the precomputed
prime-basis limbs of each table-entry's (x, y). Recompute prime_basis_limb
in-circuit from the four binary-basis limbs via the 4-limb
`unsafe_construct_from_limbs`, adding ~2 add gates per coordinate per window
(+96 gates per ECDSA verification). This saves 32 BasicTables × 256 entries =
8192 preprocessed rows from the cumulative lookup-table footprint, which
dominates the circuit size in the ECDSA-r1 + SHA256 use case (tables_size
76952 → 68760, ~10.6% off table rows).

Also expand the gate-count diagnostic in the secp256r1 biggroup tests to
report `tables_size` and `max(num_gates, tables_size)` alongside `num_gates`,
since the real circuit size is the max of the two.

* perf(bb): switch secp256r1 fixed-base windows to 7-bit + short tails

Replace the uniform 8-bit window layout with a 7-bit-dominant layout that
keeps the bigfield's natural lo/hi split (136 / 120 bits):

  - lo half: 19 × 7-bit "big" windows + 1 × 3-bit tail (LO_TAIL_WINDOW)
  - hi half: 17 × 7-bit "big" windows + 1 × 1-bit tail (HI_TAIL_WINDOW)

Total: 36 × 7-bit windows (table size 128 each) + 2 small tail tables
(size 8 for the lo tail, size 2 for the hi tail). All inter-window steps
are 7-bit aligned so the MultiTable column-1 step stays uniform at 128;
only the per-window slice_sizes vector encodes the tails' bit-widths.

The change targets cumulative lookup-table footprint for the ECDSA-r1 +
SHA256 verification flow, where SHA256's tables (~36k rows) plus the old
8-bit secp256r1 tables (32,768) pushed total tables_size to 68,760 — over
the 2^16 row cap. With this layout the secp256r1 contribution drops to
18,472 rows and cumulative tables_size becomes 54,464 — comfortably under
2^16 with ~11k rows of margin.

Per-call gate count grows by ~900 (8 more windows than before, ~110 gates
each for the extra lookup + chain_add + prime_limb compute). On the dsl
ECDSA-r1 opcode the old gate-count constant (72611) was already stale
from the earlier XYPRIME drop; this commit also brings it in line with
the actual count (45942) and adjusts the test pin accordingly.

Verified against stdlib_primitives_tests, stdlib_ecdsa_tests (full suite
including Wycherproof + infinity regressions), stdlib_circuit_builders_tests,
dsl OpcodeGateCountTests + EcdsaConstraintsTest, and the chonk-standalone
VK invariance script.

* fix(bb.js): bump WASM initial memory pages 35 → 37

The secp256r1 fixed-base plookup tables added in this branch include a
static std::array<std::array<AffineElement, 128>, 38> native_table that
contributes ~304 KiB to the WASM data segment, pushing the linker-declared
initial memory size from 35 to 37 pages (64 KiB each). The hardcoded
default in BarretenbergWasmMain.init must match, otherwise
WebAssembly.instantiate fails with:

  LinkError: memory import has 35 pages which is smaller than the
  declared initial of 37

Caught by barretenberg_wasm/index.test.js on CI.

* declare constant instead of hardcoded 38.

* add a comment.

* test(bb): add boomerang static-analyzer tests for secp256r1

Covers `secp256r1_fixed_base_mul` and `secp256r1_ecdsa_mul`:

- `fixed_base_mul`: random witness scalar, asserts 0 flagged variables
  in one gate (after `finalize_circuit()`).
- `fixed_base_mul_u_zero`: exercises the offset-subtract-to-infinity
  edge case from the PR description.
- `ecdsa_mul`: random witness inputs; the 10 expected flagged variables
  are attributed in the source to the `T2_neg.conditional_select(T2,
  beta2_neg)` call (biggroup conditional_select expands to two bigfield
  conditional_assigns × 5 limbs each = 10), each constrained by a
  single arithmetic gate that fully pins its output.
- `two_ecdsa_muls_in_same_builder`: count must scale linearly; catches
  shared-state contamination across invocations.
- `ecdsa_mul_edge_cases`: u₂ ∈ {0, ±1} and u₁ = 0 substitution paths
  all produce the same 10, confirming the circuit's wire structure is
  input-independent.

* fix u2 bug: signature verification must fail when u2 is degen.

* refactor(bb): return u2_is_acceptable from secp256r1_ecdsa_mul

`secp256r1_ecdsa_mul` now returns `Secp256r1EcdsaMulResult { element result;
bool_ct u2_is_acceptable; }`. The caller in `ecdsa_verify_signature` consumes
the flag and ANDs it into the validity bit, dedup'ing the three bigfield
equality checks against {0, ±1} that previously lived in both functions.

Test call sites updated to access `.result`. ECDSA_SECP256R1 gate count
adjusted to 45945 (net +1 gate vs pre-bug-fix).

* test(bb): secp256r1 fixed-base layout and u2_is_acceptable wiring regressions

Adds two regression tests for the secp256r1 ECDSA path:

- PlookupTests.Secp256r1FixedBaseSliceSizeBound pins the soundness contract
  of the eight SECP256R1_FIXED_BASE multitables: the slicer rejects keys past
  the per-half bit budget, and each slot's slice_size matches its backing
  basic-table size. Constants are derived from Secp256r1FixedBaseParams so the
  test follows any future layout change.

- EcdsaTests.Secp256r1HonestU2EqualsOneRejected pins the is_u2_acceptable
  AND-term on the validity flag in ecdsa_impl.hpp. Constructs an honest
  signature with s = r so u2 = r * s^{-1} === 1 (mod n), confirms native ECDSA
  accepts, and asserts the in-circuit verifier rejects (regression for the
  fix in 3222906fd8).

* test(bb): add secp256r1 fuzzers

* chore(bb): refresh pinned Chonk IVC inputs to 44f45cc9b9f040ba

Generated by ci-refresh-chonk.

Only the pinned Chonk input hash is committed here; the immediate follow-up CI run is skipped intentionally.

--ci-skip

---------

Co-authored-by: AztecBot <tech@aztecprotocol.com>
Generated by ci-refresh-chonk.

Only the pinned Chonk input hash is committed here; the immediate follow-up CI run is skipped intentionally.

--ci-skip
fix(kernel): add fixed gas cost for updating fee payer balance to private-only txs (finding #1082)
fix(kernel): assert bit size when packing delayed public mutable values (finding #986)
… (finding #1073)

Address audit finding [#1073](https://app.audithub.dev/app/organizations/272/projects/821/project-viewer?issueId=1073&version=1726)

### The bug

`read_current_value_of_delayed_public_mutable_with_hash` returns an empty value when the stored hash is zero, but it never constrained the `delayed_public_mutable` hint in that branch, only the non-zero branch checks the hint against the hash. The same hint is also consumed by `get_expiration_timestamp_for_contract_updates` to compute the contract-update time horizon. So when the hash is zero, a malicious prover could pass an arbitrary (non-empty) `delayed_public_mutable` to manipulate the time horizon while the read itself still returned empty.

###  The fix

In the zero-hash branch, assert that the packed hint is all zeros (`assert_eq(packed, std::mem::zeroed(), ...)`), guaranteeing the hint is empty before it can be used elsewhere. The function now always returns `delayed_public_mutable.svc.get_current_at(...)`, since the emptiness check makes that equivalent to the zeroed value in the uninitialized case.
Added a comment in `get_expiration_timestamp_for_contract_updates` pointing to where the hint is constrained.

###  Tests

Added `read_uninitialized_value_with_non_empty_hint_fails`, which feeds a zero hash with a non-empty hint and expects the new assertion to fire. Updated the existing `read_uninitialized_value` test to pass an empty hint, matching the now-enforced precondition.
fix(avm): ECC - enforce canonical infinity inputs cannot fail with `!is_on_curve`
…_public_mutable

fix(kernel): Constrain empty delayed public mutable when hash is zero (finding #1073)
Address audit finding [#1071](https://app.audithub.dev/app/organizations/272/projects/821/project-viewer?issueId=1071&version=1726)

### The bug
`assert_sorted_padded_transformed_array` only checked `original_array_length <= CappedSize`, but the claimed output length is `result_array_length = original_array_length + num_padded_items_hint`, and that hint was unconstrained against `CappedSize`. A malicious prover could inflate the hint so `result_array_length > CappedSize`. Items in the range `[CappedSize, result_array_length)` only get the `counter == 0` check (from the `i >= CappedSize` branch). Their values are never validated, yet they sit within the claimed length of the output array, letting an attacker smuggle arbitrary values into the array under a `counter == 0` disguise.

### The fix
Replaced the `original_array_length <= CappedSize` assert with `result_array_length <= CappedSize` (which subsumes the `original check since result_array_length >= original_array_length`).

### Tests
Added `inflate_padded_items_hint_to_add_random_value_within_claimed_length`, which reproduces the attack (verified to pass on the buggy code before the fix, then flipped to `should_fail_with` after).
Updated four existing tests that expected the old assertion message to the new "...all items in result_array" message.
fix(kernel): Constrain CappedSize against result length (finding #1071)
AztecBot and others added 9 commits July 10, 2026 04:26
# Conflicts:
#	noir-projects/aztec-nr/aztec/src/standard_addresses.nr
#	noir-projects/noir-contracts/pinned-standard-contracts.tar.gz
#	yarn-project/standard-contracts/src/standard_contract_data.ts
#	yarn-project/txe/esbuild/plugins/size_guard.mjs
Resolves the 4 conflicts from merging origin/v5 into v5-next:

- yarn-project/txe/esbuild/plugins/size_guard.mjs: keep the higher
  totalLimitMiB = 15 (v5), since merging v5's larger bundle content in
  needs the bigger cap; merged both bump-log lines chronologically.

- Generated standard-contract artifacts (kept at the v5-next baseline,
  REGEN REQUIRED before merge):
    - noir-projects/aztec-nr/aztec/src/standard_addresses.nr
    - yarn-project/standard-contracts/src/standard_contract_data.ts
    - noir-projects/noir-contracts/pinned-standard-contracts.tar.gz
  These are generated by 'bootstrap.sh pin-standard-build' + 'yarn
  workspace @aztec/standard-contracts run generate'. The merged tree
  combines v5-next's standard-contract source changes (handshake
  forgery-protection) with v5's aztec-nr + protocol-constants changes,
  so the deterministic addresses match NEITHER committed side and must
  be re-pinned/regenerated from the merged tree. Left at v5-next's
  values as a consistent placeholder; bootstrap drift-check will flag
  this until regenerated.
@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 claudebox Owned by claudebox. it can push to this PR. labels Jul 14, 2026
@socket-security

socket-security Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​types/​node@​22.19.171001008196100

View full report

@nventuro nventuro added the ci-full Run all master checks. label Jul 15, 2026
@nventuro
nventuro marked this pull request as ready for review July 15, 2026 02:58
@nventuro
nventuro enabled auto-merge July 15, 2026 02:58
@nventuro
nventuro added this pull request to the merge queue Jul 15, 2026
Merged via the queue into v5-next with commit c11bc68 Jul 15, 2026
29 of 34 checks passed
@nventuro
nventuro deleted the cb/merge-v5-into-v5-next-20260714 branch July 15, 2026 04:24
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-full Run all master checks. ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure claudebox Owned by claudebox. it can push to this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.