Skip to content

feat: merge-train/spartan-v5 - #23936

Merged
AztecBot merged 8 commits into
v5-nextfrom
merge-train/spartan-v5
Jun 9, 2026
Merged

feat: merge-train/spartan-v5#23936
AztecBot merged 8 commits into
v5-nextfrom
merge-train/spartan-v5

Conversation

@AztecBot

@AztecBot AztecBot commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

BEGIN_COMMIT_OVERRIDE
chore: add tmp folder in yarn-project (#23932)
fix(p2p): cap inbound tx DA gas limit by max tx blob size (A-1162) (#23933)
fix: ingest L1 checkpoints up to the blob block capacity (A-1156) (#23934)
fix: sync proposed checkpoint on HA peers (A-1165) (#23940)
END_COMMIT_OVERRIDE

So we can dump in there tmp files like plans, analysis, etc.
AztecBot and others added 3 commits June 8, 2026 17:23
…23933)

## Motivation

`GasLimitsValidator` caps a tx's declared DA gas limit at
`min(MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, maxBlockDAGas ?? Infinity)`.
With no per-block DA cap configured (the default), this allows a tx to
declare up to the full checkpoint DA limit of 786,432 DA gas.

However, a single tx's effects cannot encode more than
`MAX_TX_BLOB_DATA_SIZE_IN_FIELDS` (8475) fields in a blob, and DA gas is
charged per field at `DA_GAS_PER_FIELD` (32), so the most DA gas any tx
can actually consume is 271,200 — roughly 1/3 of what inbound validation
currently permits.

The sequencer uses declared tx gas limits pessimistically during
proposal building, before processing txs. A tx that declares an
unnecessarily high DA gas limit can reserve a disproportionate share of
the block/checkpoint DA budget in the pre-processing admission check
even though it can never use that much DA, hurting block packing and
creating an avoidable griefing vector where valid txs get skipped during
block construction.

## Approach

Cap the declared DA gas limit at the maximum a tx can physically
consume:

```
min(MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, maxBlockDAGas, MAX_TX_BLOB_DATA_SIZE_IN_FIELDS * DA_GAS_PER_FIELD)
```

`MAX_TX_BLOB_DATA_SIZE_IN_FIELDS` lives in `tx_blob_data.nr` (alongside
circuit code), not in `constants.nr`, so it was not exported to the TS
constants. Rather than duplicate the value, the constants generator now
supports pulling specific constants from additional Noir files via an
explicit allowlist, so `MAX_TX_BLOB_DATA_SIZE_IN_FIELDS` is generated
from its single Noir source of truth with no risk of drift.

- **Generator**: `ADDITIONAL_NOIR_CONSTANT_FILES` lists extra files and
the exact constant names to extract from each. `parseNoirFile` now
returns raw (unevaluated) expressions so cross-file references resolve
in a single evaluation pass; an opt-in line-comment stripper handles
inline `//` comments in the blob-size definition. The generator throws
if a listed constant is missing, guarding against silent drift on
rename.
- The new constant binds below the checkpoint cap but above the default
`fallback()` gas settings (196,608 DA), and estimation settings use
`skipTxValidation`, so normal tx flows are unaffected.

This is sound because DA gas is uniformly charged as `(fields written to
the tx blob) × DA_GAS_PER_FIELD` at every layer (private kernel
`gas_meter.nr`, AVM per-opcode, and the settled on-chain block DA in
`l2_block.ts`), and the tx blob is a fixed-size `[Field;
MAX_TX_BLOB_DATA_SIZE_IN_FIELDS]` array — a hard ceiling. The cap only
rejects over-provisioned declarations a tx could never use.

### Defense-in-depth: per-tx blob field check in the public processor

The public processor already skips a processed tx whose blob fields
would overflow the remaining *checkpoint* budget, but that does not
enforce the *per-tx* `MAX_TX_BLOB_DATA_SIZE_IN_FIELDS` ceiling. The
per-category side-effect limits (note hashes, nullifiers, L2→L1, public
data writes, private/public/contract-class logs) already guarantee a
single tx cannot exceed the per-tx blob size, so this is unreachable in
practice. As defense-in-depth — and because an oversized tx effect would
otherwise poison proving (the rollup circuit cannot encode it) rather
than fail gracefully — the processor now rejects any tx whose resulting
effects exceed `MAX_TX_BLOB_DATA_SIZE_IN_FIELDS` as invalid (not a
silent skip), since no valid tx should ever produce that many fields.

### Wallet: bound gas-estimate padding to the cap

Wallets pad simulated gas by a buffer (default 10%) before sending.
Since the unpadded DA estimate can be at most
`MAX_TX_BLOB_DATA_SIZE_IN_FIELDS × DA_GAS_PER_FIELD`, the padding could
otherwise push the declared DA limit over the new inbound cap and get
the tx rejected. `getGasLimits` now clamps the padded DA gas (total and
teardown) to the per-tx max, relying on the generated constants. The
unpadded estimate never exceeds the cap, so clamping never
under-provisions a tx.

## Changes

- **constants**: generator (`constants.in.ts`) extended to extract
constants from additional Noir files; regenerated `constants.gen.ts` now
exports `MAX_TX_BLOB_DATA_SIZE_IN_FIELDS`. No changes to the generated
Solidity/C++/PIL constants.
- **p2p**: `GasLimitsValidator` caps the effective DA gas limit by the
per-tx blob size.
- **simulator**: `PublicProcessor` rejects (as invalid) any tx whose
resulting effects exceed `MAX_TX_BLOB_DATA_SIZE_IN_FIELDS`.
- **aztec.js**: `getGasLimits` clamps padded DA gas to the per-tx max so
the estimation buffer can't push a tx over the inbound cap.
- **tests**: `gas_validator.test.ts`, `tx_pool_v2.test.ts`,
`public_processor.test.ts`, `get_gas_limits.test.ts` updated/extended;
`e2e_bot` and `e2e_sequencer_config` set `daGasLimit` to the new per-tx
max so their txs remain admissible.

Fixes A-1162
spalladino and others added 4 commits June 8, 2026 21:48
…3934)

## Motivation

A single constant, `MAX_BLOCKS_PER_CHECKPOINT = 72`, was doing two
unrelated jobs: it bounded checkpoint **deserialization**
(`Checkpoint.fromBuffer`) and **L1-sync ingest** validation, *and* it
served as the build/attest policy limit.

The protocol and L1 impose no per-checkpoint block-count cap — the real
ceiling is the blob-field budget (`BLOBS_PER_CHECKPOINT *
FIELDS_PER_BLOB = 24,576` fields, ~2457 minimally-sized blocks). So a
checkpoint that L1 accepts with more than 72 small blocks could not be
deserialized or validated on ingest: the archiver throws, never advances
past that L1 block, and wedges permanently — a liveness failure for any
node that hits such a checkpoint.

## Approach

Split the conflated constant into two, each with one job:

- `MAX_CAPACITY_BLOCKS_PER_CHECKPOINT = 2457` — the exact blob-capacity
ceiling. Used for deserialization and when ingesting checkpoints already
accepted by L1.
- `MAX_ATTESTABLE_BLOCKS_PER_CHECKPOINT = 72` — the conservative policy
for what we build or attest to, and the **default** block-count limit
for `validateCheckpoint`.

`validateCheckpoint` / `validateCheckpointStructure` gain a
`maxBlocksPerCheckpoint?` option that defaults to the attestable limit
(fail-closed). The L1-sync ingest path explicitly opts into the capacity
ceiling; build and proposal-validation use the default and set nothing.

The p2p block-proposal validator now enforces the attestable ceiling as
a hard, always-on bound (a lower configured `maxBlocksPerCheckpoint`
still tightens it). `indexWithinCheckpoint` is 0-based, so a proposal
for the 73rd block (index 72) is rejected at gossip ingress with a peer
penalty — previously the check was skipped entirely when the config was
unset.

The 2457 value is the exact blob ceiling, pinned by a unit test that
re-derives it from the blob constants so any change to those forces a
re-derivation.

## API changes

- `stdlib` (`@aztec/stdlib/deserialization`): removed
`MAX_BLOCKS_PER_CHECKPOINT`; added `MAX_CAPACITY_BLOCKS_PER_CHECKPOINT`
and `MAX_ATTESTABLE_BLOCKS_PER_CHECKPOINT`.
- `validateCheckpoint` and `validateCheckpointStructure` accept an
optional `maxBlocksPerCheckpoint` (defaults to the attestable limit).

## Changes

- **stdlib**: split the constant; `Checkpoint.fromBuffer` deserializes
up to the capacity ceiling;
`validateCheckpoint`/`validateCheckpointStructure` parameterize the
block-count limit, defaulting to the attestable limit.
- **archiver**: the L1-sync ingest path (`data_store_updater`) validates
checkpoints against the capacity ceiling.
- **p2p**: the proposal validator enforces the attestable ceiling on
block proposals unconditionally at gossip ingress.
- **stdlib (tests)**: new constant-invariant test pinning 2457 to the
blob budget; new >72-block serialization round-trip; `validate` tests
for default-rejects / capacity-accepts / explicit-limit.
- **p2p (tests)**: 73rd-block rejection holds even when
`maxBlocksPerCheckpoint` is unset.

Fixes A-1156
Validators were skipping processing of `CheckpointProposal`s coming from
their own keys. In an HA setup, this means the checkpoint proposal
produced by another node in their same setup was never synced, so the
archiver ended up pruning blocks without corresponding checkpoint
proposal.

## Issue

In an HA deployment, peer nodes share validator signing keys for
redundancy. When the active proposer broadcasts a checkpoint proposal,
its HA peer receives the proposal over gossip and classifies it as "own"
— it owns the signing key — so the all-nodes checkpoint handler returned
early without recording the proposed-checkpoint metadata in its
archiver.

That early return was written on the assumption that an own checkpoint
proposal can only originate from this node's own sequencer, which
already pushed the proposed checkpoint locally before broadcasting. That
assumption breaks for HA peers: the peer never built the checkpoint, so
it lacked the proposed-checkpoint metadata needed to build the next slot
on top of the still-proposed checkpoint. When that peer had to take over
for the following slot, it **pruned the proposed block as an orphan**,
rebuilt the same checkpoint, and clashed instead of producing the next
checkpoint.

## Approach

For own checkpoint proposals, the all-nodes handler now inspects the
proposed checkpoint already stored for that slot:

- **Matching archive** — idempotent no-op. This is the true local
proposer (or an already-hydrated peer); nothing to validate or write.
- **Conflicting archive** — log a structured warning and do not
overwrite. Duplicate/equivocation handling remains the responsibility of
the existing detection paths.
- **Missing** — the HA peer that received the proposal only via gossip.
Wait for the last block to sync (using the same publish-deadline-bounded
wait as the foreign-validation path) and hydrate the proposed-checkpoint
metadata from local block data.

The block-sync wait (`syncImmediate` + deadline-bounded retry) is
extracted into a shared helper so the own/HA hydration path has the same
reliability as foreign validation, rather than a single no-wait fetch
that could silently miss a not-yet-synced block.

Checkpoint attestation behavior is unchanged: a validator still ignores
proposals from its own keys and does not produce duplicate HA
attestations. Block proposal handling is also unchanged.

## Changes

- **validator-client**: Rework the `isOwnProposal` branch of the
all-nodes checkpoint proposal handler to hydrate missing
proposed-checkpoint metadata for HA peers while keeping the
local-proposer fast path idempotent. Widen the handler's archiver
dependency to include `getProposedCheckpointData`. Extract a shared
`waitForLastBlockData` helper reused by the foreign-validation path;
`setProposedCheckpointFromValidation` now takes the already-synced
block.
- **validator-client (tests)**: Replace the unit test that asserted own
proposals never touch the archiver. Add coverage for own-proposal
matching/missing/conflicting cases, the block-sync wait
(missing-then-appears), and the never-syncs degraded case.
- **end-to-end (tests)**: Add `e2e_epochs/epochs_ha_checkpoint_handoff`
reproducing the handoff: it routes two consecutive slots to one HA pair,
has the builder propose the first slot, and asserts the HA peer records
the proposed checkpoint and produces the next slot's checkpoint. The
primary assertion (peer records the proposed-checkpoint metadata) is
timing-independent.

Fixes A-1165

@ludamad ludamad left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Auto-approved

@AztecBot
AztecBot enabled auto-merge June 9, 2026 01:35
@AztecBot

AztecBot commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Auto-merge enabled after 4 hours of inactivity. This PR will be merged automatically once all checks pass.

@AztecBot
AztecBot added this pull request to the merge queue Jun 9, 2026
@AztecBot

AztecBot commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author

Flakey Tests

🤖 says: This CI run detected 2 tests that failed, but were tolerated due to a .test_patterns.yml entry.

\033FLAKED\033 (8;;http://ci.aztec-labs.com/dc1e8b29ec33ee57�dc1e8b29ec33ee578;;�):  yarn-project/end-to-end/scripts/run_test.sh simple src/e2e_epochs/epochs_invalidate_block.parallel.test.ts "proposer invalidates multiple checkpoints" (519s) (code: 0) group:e2e-p2p-epoch-flakes
\033FLAKED\033 (8;;http://ci.aztec-labs.com/d730d0e6e23ed35e�d730d0e6e23ed35e8;;�): yarn-project/end-to-end/scripts/run_test.sh ha src/composed/ha/e2e_ha_full.test.ts (260s) (code: 0)

@AztecBot

AztecBot commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Auto-merge enabled after 4 hours of inactivity. This PR will be merged automatically once all checks pass.

Merged via the queue into v5-next with commit b4d2e44 Jun 9, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants