Skip to content

chore: merge v5-next into merge-train/spartan-v5 (raw, conflict markers) - #24221

Merged
PhilWindle merged 17 commits into
merge-train/spartan-v5from
cb/merge-train-spartan-v5-raw
Jun 22, 2026
Merged

chore: merge v5-next into merge-train/spartan-v5 (raw, conflict markers)#24221
PhilWindle merged 17 commits into
merge-train/spartan-v5from
cb/merge-train-spartan-v5-raw

Conversation

@AztecBot

Copy link
Copy Markdown
Collaborator

Raw merge — do not merge alone

This is PR 1 of a two-PR stack that catches merge-train/spartan-v5 up to its base v5-next so the long-lived train PR (merge-train/spartan-v5v5-next) becomes conflict-free again.

This PR is the raw merge of v5-next into merge-train/spartan-v5 with conflict markers committed — it is red by design. Review and merge it only together with the stacked resolution PR (PR 2, head cb/merge-train-spartan-v5-fix), which targets this branch.

Parents

  • train: merge-train/spartan-v5 @ bc2c924919fe8880ee9572df779b78f653ef2e15
  • base: v5-next @ 4df72438bf01f125e791af7e8bb11a613964457e

Conflicted files (1)

  • yarn-project/end-to-end/src/e2e_nested_utility_calls.test.ts

Resolution lives in the stacked fix PR.


Created by claudebox · group: slackbot

just-mitch and others added 16 commits June 18, 2026 20:18
Port of #24186 to the v5 release line.

Bind per-checkpoint fee recipient/value to the committee-attested checkpoint
header instead of prover-supplied calldata, so reward distribution and the epoch
proof's fee-region public inputs are derived from headers verified on L1 and no
longer depend on a sound verifier.

- L1: add ProposedHeader.accumulatedFees (hashed in ProposedHeaderLib); replace
  SubmitEpochRootProofArgs.fees[] with headers[]; submitEpochRootProof rehashes
  each supplied header against the stored hash; RewardLib and
  getEpochProofPublicInputs read recipient/value from the verified headers.
- Circuits: add accumulated_fees to CheckpointHeader (hash + serialize) and set
  it in the checkpoint-root composer; bump CHECKPOINT_HEADER_LENGTH.
- Off-chain: thread checkpoint headers from the prover node's epoch session
  through the proof-publishing service to the L1 submit call; archiver, sequencer
  and stdlib carry the new field.
- PXE: bump PXE_DATA_SCHEMA_VERSION (8 -> 9) for the checkpoint serialization
  change and regenerate the storage compatibility snapshots.
The committed pinned-build.tar.gz is used unconditionally in place of compiling
the protocol circuits, so it must track circuit source changes. Adding the
accumulated_fees field to the checkpoint header changed the checkpoint_root,
checkpoint_root_single_block and root rollup circuits; without re-pinning, the
prover ran stale 12-field checkpoint circuits and produced a checkpointHeaderHashes
value that disagreed with the freshly compiled 13-field Solidity, failing epoch
proof submission (e2e_simple: Timeout awaiting isProven).
Move verifyHeaders from submitEpochRootProof into getEpochProofPublicInputs.
The on-chain submit path still runs it via verifyEpochRootProof, and the
prover-publisher's off-chain getEpochProofPublicInputs preflight now validates
the headers before publishing instead of skipping the check. Since the fee
recipient/value public inputs are sourced from the supplied headers, this
catches a header mismatch off-chain rather than reverting on submit.
A malicious proposer could store header or archive field values >= the BN254
scalar field modulus on L1. Off-chain archivers decode those storage slots into
Fr elements, so an out-of-range value bricks honest archivers' L1 sync.

Reject such values at propose time:
- validateHeader: blockHeadersHash, outHash, feeRecipient, accumulatedFees
- propose: the new checkpoint archive root

Other header fields are already safe (equality-checked against field-reduced
on-chain values, or type-bounded), so they are left unchecked.

This is the L1 half of A-1254; the archiver-side defense-in-depth is a separate
follow-up PR.
#24199)

## Summary

Part of A-1254.

A malicious proposer can currently store checkpoint header or archive
field values that are `>=` the BN254 scalar field modulus
(`Constants.P`) on L1. Off-chain archivers decode those storage slots
into `Fr` elements, so an out-of-range value bricks honest archivers' L1
sync. This is the L1 half of the fix: reject such values at write time
so they can never reach L1 storage.

## Changes

A new helper `FieldLib.requireValidFieldElement(bytes32)` wraps the
`value < Constants.P` check and reverts with
`Errors.Rollup__FieldElementOutOfRange(bytes32 value)`. It is used to
range-check the attacker-controlled, `Fr`-decoded values that are not
otherwise constrained:

- `ProposeLib.validateHeader` — header fields `blockHeadersHash`,
`outHash`, `feeRecipient`, `accumulatedFees`.
- `ProposeLib.propose` — the new checkpoint archive root
(`_args.archive`), which is not part of the header, checked before the
digest/header validation/storage.
- `STFLib.initialize` — the genesis archive root, which is written to
`archives[0]` at deployment and propagates into the first header's
`lastArchiveRoot`.

## Why the other fields are already safe

- `lastArchiveRoot`, `inHash`, `blobsHash` — equality-constrained to
on-chain values that are already field-reduced (`tipArchive`,
`inbox.consume()`, `blobsHashesCommitment`).
- `totalManaUsed` — bounded by `<= FeeLib.getManaLimit()`, and the mana
limit is capped at `type(uint32).max`, far below `P`.
- `gasFees` (uint128), `coinbase` (address), `slotNumber` / `timestamp`
— type-bounded or equality-checked against slot-derived values.

## Tests

New `test/RollupFieldRange.t.sol`:

- Each of the five guarded propose fields reverts with
`Rollup__FieldElementOutOfRange` when set to `Constants.P` (boundary)
and to `type(uint256).max`.
- The genesis archive root reverts at deploy time when out of range.
- An otherwise-valid header with every guarded field at `Constants.P -
1` proposes successfully, confirming the boundary is exclusive and
legitimate field elements are not rejected.

Verified red/green for both the propose checks and the genesis check;
the broader `RollupTest`, escape-hatch, and fees suites remain green.

## Follow-up

The archiver-side defense-in-depth (decoding out-of-range values without
bricking sync) is a separate follow-up PR that will close A-1254.
…ield

run_rollup_upgrade.sh deploys with a random GENESIS_ARCHIVE_ROOT. The rollup now
rejects a genesis archive root >= the BN254 scalar field modulus
(Rollup__FieldElementOutOfRange), so a full 32-byte random value fails ~81% of the
time. Generate 31 random bytes with a zero top byte so the value is always a valid
field element.
The previous re-pin generated VKs via the build cache, leaving a verification
key that did not match its recompiled circuit after the CHECKPOINT_HEADER_LENGTH
bump. Real epoch proofs then failed at the root-rollup pairing check
(bb_prover_full_rollup), which only runs under full CI. Rebuild with NO_CACHE so
every VK is derived locally from its own ACIR; validated by running
bb_prover_full_rollup with real proofs (root-rollup proof now verifies).
The prior re-pin was generated with a stale local bb (predating the ECCVM
fixed-VK / chonk-verifier / IPA changes now in the tree), so the rollup VKs
embedded the old ECCVM VK. Locally that bb proves and verifies self-consistently,
but full CI builds bb from current source and proves the new ECCVM against the
old baked VK, failing the root-rollup pairing check (bb_prover_full_rollup:
'Failed to verify proof from key'). Rebuilt bb from source and regenerated both
the noir and mock protocol-circuit pins so their VKs match the current bb.
Validated locally with real proofs.
BEGIN_COMMIT_OVERRIDE
feat(txe): auto-generate oracle serialization roundtrip tests (#24138)
END_COMMIT_OVERRIDE
This is a defense-in-depth change: it protects the fee juice portal in
the event of a catastrophic soundness bug.

## Problem

The epoch-proof `_fees` array (per-checkpoint `recipient`/`value`) was
prover-supplied calldata, bound only by the epoch proof. If the verifier
were unsound (e.g. returned `true` unconditionally), a prover could
forge fee recipients/values and steal fees. In an extreme case, this
could result in the complete drain of the canonical fee juice portal.

## Fix

Bind the fee value to the committee, not the proof. The committee
already attests to the checkpoint `headerHash` (which commits
`coinbase`) at propose time. We add the fee **value** to the checkpoint
header as `accumulatedFees`, so both halves of a fee entry are committed
when the checkpoint is proposed and stored on L1. At proof submission
the prover supplies the full checkpoint headers; L1 rehashes each with
the existing `ProposedHeaderLib.hash` and requires equality with the
stored header hash. Rewards and the proof's fee-region public inputs are
then derived from these verified headers so fees and checkpoint rewards
no longer depends on the verifier.

## Design note

The `fees[FeeRecipient]` array in the rollup circuit/merge/root public
inputs are not strictly needed anymore. They are left there to minimize
the changeset. Removing the array can be a follow-up. Further, there are
cheaper options where we could break up what the proposer stores when
they publish a checkpoint, and have the checkpoint header hash be a
nested computation like H(fee_recipient, amount, H(...rest)). We can
investigate those if needed.
## Why we are doing this

Utility functions had no notion of `msg_sender`, so a utility that
wanted to know its caller had to accept it as a parameter.
`HandshakeRegistry::get_app_siloed_secret` took an explicit `caller`
argument and siloed the returned secret by it, so a hostile contract
could pass another address and read an app's siloed secret it shouldn't
see.

## Our fix

- `UtilityContext` gains a `msg_sender`, exposed as `self.msg_sender()`
and `self.context.maybe_msg_sender()`, mirroring the private and public
contexts.
- It is set only for cross-contract calls (utility-to-utility or
private-to-utility), where it is the calling contract's address taken
from the call graph and so cannot be forged. A top-level call (invoked
directly by a wallet or dapp) has no caller: `maybe_msg_sender()` is
`none` and `msg_sender()` panics. The `from` supplied when simulating
still only controls note/key visibility; it is never exposed as a
sender.
- `HandshakeRegistry::get_app_siloed_secret` drops the forgeable
`caller` parameter and silos by `self.msg_sender()`, so a contract can
only retrieve secrets siloed to its own address.
- `TestEnvironment` gains `ExecuteUtilityOptions::with_from` to simulate
a cross-contract caller in tests without routing through an actual
nested call.

Fixes F-671

---------

Co-authored-by: Nicolás Venturo <nicolas.venturo@gmail.com>
BEGIN_COMMIT_OVERRIDE
feat(aztec-nr)!: add msg_sender to the utility context (#24062)
fix(cli): transpile pre-existing artifacts on aztec compile (#24188)
END_COMMIT_OVERRIDE
@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 ci-no-squash claudebox Owned by claudebox. it can push to this PR. labels Jun 22, 2026
## Conflict resolution (PR 2 of 2)

Stacked on top of the raw merge #24221. This is the **reviewable diff**
— it resolves the single conflict from merging `v5-next` into
`merge-train/spartan-v5`. Once both are green, merge this into #24221,
then #24221 into `merge-train/spartan-v5`, which catches the train up to
its base and makes the long-lived `merge-train/spartan-v5` → `v5-next`
PR conflict-free.

### Conflicts resolved (1)
**`yarn-project/end-to-end/src/e2e_nested_utility_calls.test.ts`** —
both sides added content at the same point in the `authorizeUtilityCall
hook` describe block:
- **train (`ours`)** added a doc comment above the existing `denies
cross-contract utility call from private function when hook returns
false` test (from the e2e annotation pass on the train).
- **base `v5-next` (`theirs`)** added a new `nested utility call sees
the calling contract as its msg_sender` test (from `feat(aztec-nr)!: add
msg_sender to the utility context` #24062).

**Resolution:** keep both — the new `msg_sender` test followed by the
doc-commented `denies…private function` test. No code was dropped from
either side. The `delegate_get_msg_sender` contract method the new test
exercises is brought in by the merge (it lives in
`noir-projects/noir-contracts/contracts/test/nested_utility_contract`,
non-conflicting).

No fixtures or generated files were involved. PR 1's CI is red by design
(conflict markers committed there); this PR's CI is the one to watch.

---
*Created by
[claudebox](https://claudebox.work/v2/sessions/51d528a3438f2849) ·
group: `slackbot`*
@PhilWindle
PhilWindle marked this pull request as ready for review June 22, 2026 11:48
@PhilWindle
PhilWindle enabled auto-merge (squash) June 22, 2026 11:49
@PhilWindle
PhilWindle merged commit 0440c0d into merge-train/spartan-v5 Jun 22, 2026
14 of 18 checks passed
@PhilWindle
PhilWindle deleted the cb/merge-train-spartan-v5-raw branch June 22, 2026 12:06
PhilWindle added a commit that referenced this pull request Jun 22, 2026
## Merge `v5-next` into `merge-train/spartan-v5` — MUST merge as a merge
commit (do not squash)

### Why the conflicts came back
The previous stack (#24221/#24222) resolved the conflict correctly, but
#24221 was **squash-merged** onto the train. A squash creates a
brand-new single commit and does **not** record `v5-next` as a parent,
so git still sees the train and `v5-next` as diverged (`git merge-base
--is-ancestor origin/v5-next origin/merge-train/spartan-v5` → false).
The merge-train auto-pull then re-attempts `v5-next → train` and hits
the same conflict again.

### What this PR does
This is a real **merge commit** of `v5-next` into the train (two
parents: train tip `eb7d64d` + `v5-next` `4df7243`), with the one
conflict resolved. The resulting tree is **identical to the current
train** (0 file changes) because the content already landed via the
earlier squash — this PR exists purely to record `v5-next` as an
ancestor and advance the merge base.

After this lands, `v5-next` is an ancestor of the train and the
long-lived `merge-train/spartan-v5` → `v5-next` PR is conflict-free.

### Conflict resolved (1)
`yarn-project/end-to-end/src/e2e_nested_utility_calls.test.ts` — kept
the train's doc comment above the `denies…private function` test; the
`msg_sender` test from `v5-next` is already present.

### ⚠️ Merge method
**Merge this with "Create a merge commit"** (or `merge_pr
merge_method=merge`). A squash or rebase merge will recreate the same
divergence and the conflicts will return.

---
*Created by
[claudebox](https://claudebox.work/v2/sessions/51d528a3438f2849) ·
group: `slackbot`*
rangozd pushed a commit to rangozd/aztec-packages that referenced this pull request Aug 5, 2026
BEGIN_COMMIT_OVERRIDE
fix(p2p): re-seed discovery from persisted peer ENRs after restart
(AztecProtocol#24169)
docs(e2e): annotate e2e tests with setup/category notes (AztecProtocol#24191)
chore: merge v5-next into merge-train/spartan-v5 (raw, conflict markers)
(AztecProtocol#24221)
fix(archiver): index zero-field logs under empty tag instead of throwing
(A-1253) (AztecProtocol#24212)
fix(prover-node): report awaiting-root and publishing-proof phases in
EpochSession (A-1212) (AztecProtocol#24216)
fix(p2p): bound declared contract-class bytecode length before
allocating (A-1258) (AztecProtocol#24213)
fix(p2p): frame gossipsub msgId and restrict allowedTopics (A-1256)
(AztecProtocol#24214)
chore: merge v5-next into merge-train/spartan-v5 (AztecProtocol#24226)
fix(p2p): guard ENR address parsing against malformed TCP fields
(A-1255) (AztecProtocol#24215)
END_COMMIT_OVERRIDE
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 ci-no-squash claudebox Owned by claudebox. it can push to this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants