Skip to content

AZIP-4 L1 Block Header Access via an L1 Portal#44

Open
mrzeszutko wants to merge 2 commits into
AztecProtocol:mainfrom
mrzeszutko:mr/l1-block-header-portal
Open

AZIP-4 L1 Block Header Access via an L1 Portal#44
mrzeszutko wants to merge 2 commits into
AztecProtocol:mainfrom
mrzeszutko:mr/l1-block-header-portal

Conversation

@mrzeszutko

Copy link
Copy Markdown

Adds AZIP-4 (Core, status Draft) under AZIPs/azip-4.md.

This proposal is an out-of-protocol alternative to the in-protocol "L1 Block Header Access" design in #24. It reaches roughly the same capability — letting Noir contracts read and prove Ethereum L1 state — with no changes to protocol circuits, the AVM, the kernels, or any deployed application ABI.

  1. L1 portal (BlockHashPortal) reads blockhash(block.number - 1), packs {l1BlockNumber, l1BlockHash} into a sha256ToField commitment, and sends it through the existing L1→L2 inbox.
  2. L2 standard contract (BlockHashStore) — deterministic address via the standard-contract recipe, not a magic-slot protocol contract — consumes the message once, verifies a witnessed RLP header against the committed hash, and memoizes a Poseidon2 header commitment keyed by L1 block number. Readers verify their RLP header against the commitment with a ~1–2k-gate Poseidon2 check in place of a ~25k-gate keccak.
  3. New slashing offense. Proposers MUST include pushLatest() before propose() in their checkpoint multicall; omission or out-of-order placement is a SMALL-severity offense (10e18 wei) decided from the L1 transaction receipt.

Trade-offs vs. #24: higher L1 gas per checkpoint, higher per-read gas for header-field-only reads, and an L1→L2 readability latency floor of LAG × AZTEC_SLOT_DURATION = 144 s. In exchange: no critical-path / circuit / AVM audit, no recompile of deployed application circuits, and no change to the L1 GenesisState — a new standard contract added to a running rollup without a new Rollup version.

Credit for the original out-of-protocol design to @iAmMichaelConnor, seeded as a comment (#12 (comment)) by @joeandrews on the AZIP-4 discussion.

cc @iAmMichaelConnor @joeandrews

@mrzeszutko mrzeszutko requested a review from a team June 8, 2026 14:36
Comment thread AZIPs/azip-4.md

## Impacted Stakeholders

**Sequencers / proposers.** Proposing a checkpoint gains one new duty: include `BlockHashPortal.pushLatest()` before `propose()` in the same L1 transaction. Omission or out-of-order placement is slashable. The added L1 gas is ~10k gas in-protocol plus ~20–50k gas for the portal call per checkpoint, depending on whether the inbox tree was already touched between checkpoints. The call takes no arguments, and proposers are not responsible for consuming the message on L2.

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.

Is this effectively introducing a proposer slash for missing a checkpoint proposal?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No — this offense is conditioned on a checkpoint actually being proposed. Detection anchors on the rollup's CheckpointProposed log in the proposer's L1 tx receipt and asks: "did a BlockHashPushed log from the configured portal appear before it?" A slot with no CheckpointProposed log produces no anchor, so this offense doesn't fire.

Comment thread AZIPs/azip-4.md
- **Witnesses.** `l1BlockHash` and `rlp_header` are supplied to `submit()`. The store recomputes `content`, verifies `keccak256(rlp_header) == l1BlockHash`, and stores `poseidon2(pack_to_fields(rlp_header))`.
- **Number binding.** `l1BlockNumber` MUST be in the preimage; otherwise a caller could store a real hash under the wrong block number.
- **Canonical serialization.** Field order, concatenation, endianness, and reduction MUST be identical in Solidity and Noir.
- **`secretHash`.** MUST be `0`.

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.

Shouldn't secretHash be the hash of zero, instead of zero, in order to consume the message from L2?

Comment thread AZIPs/azip-4.md

Ordering also lands the message in the earlier in-progress tree when `propose()` crosses the `LAG` boundary, surfacing the value one checkpoint sooner; freshness is the latency rationale, but the slashing rule in (d) is what makes it enforceable.

### (d) New slashing offense

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.

Fable found a nasty issue with this slashing offense: it is triggerable by a third party that frontruns the proposer.

propose() authenticates the proposer by their signature inside the attestations, not by msg.sender (ProposeLib.sol — "Only the designated proposer for the current slot can propose a checkpoint, enforced by validating the proposer validator signature among attestations"; only the escape-hatch path checks msg.sender). A checkpoint proposal is therefore relayable: an attacker who observes the proposer's multicall in the public mempool (calldata, attestations, and blob sidecars are all visible) can re-bundle the bare propose() call into their own transaction without pushLatest() and front-run. The checkpoint lands attributed to the honest proposer, the receipt has CheckpointProposed but no BlockHashPushed, and the watcher slashes deterministically with no appeal path — the proposal explicitly states detection is "decided purely by inspection of the proposer's finalized L1 transaction."

This would be the first offense in the framework whose trigger is not solely a function of the validator's own behavior. The cost is bounded (the attacker pays gas + blob fees to burn 10e18 of the victim's stake), but a slashing spec needs to either acknowledge and accept this, recommend private orderflow, or weaken the rule. Section (c) considers accomplice pushes masking an omission but not adversarial re-bundling creating one.

Comment thread AZIPs/azip-4.md

`submit()` MUST verify `keccak256(rlp_header) == l1BlockHash` before writing `header_commitments[l1BlockNumber]`. The keccak is paid once per L1 block by the submitter (a keeper or first caller) in AVM gas; every subsequent reader skips it, verifying the cheap Poseidon2 commitment instead (see "Application-side verification" below). If the assertion fails, the entire `submit()` call reverts — including the inbox consume — leaving the message available for re-submission with the correct `rlp_header`.

`submit()` SHOULD emit a `BlockHashMemoized(l1BlockNumber)` public log on the success path (i.e. when memoization actually occurs), mirroring the L1 portal's `BlockHashPushed(n)` event so off-chain indexers, explorers, and keepers can detect "block `n` is now memoized" without polling public state. The log MUST NOT fire on the idempotent early-return branch, so the presence of a log carries the meaning "this call performed the memoization." The log's shape is non-normative; consumers MUST NOT couple to its field encoding. The authoritative signal that block `n` is memoized is `blockhashes[n] != 0` — the log is an off-chain notification convenience, not a substitute for the storage read.

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.

Should this be header_commitments[n]? The store mapping is called header_commitments everywhere else in the spec (the submit() pseudocode, (f), and Security Considerations) — blockhashes only appears in the rejected "memoizing the canonical bytes32 block hash" alternative, so this looks like a leftover from that draft.

Suggested change
`submit()` SHOULD emit a `BlockHashMemoized(l1BlockNumber)` public log on the success path (i.e. when memoization actually occurs), mirroring the L1 portal's `BlockHashPushed(n)` event so off-chain indexers, explorers, and keepers can detect "block `n` is now memoized" without polling public state. The log MUST NOT fire on the idempotent early-return branch, so the presence of a log carries the meaning "this call performed the memoization." The log's shape is non-normative; consumers MUST NOT couple to its field encoding. The authoritative signal that block `n` is memoized is `blockhashes[n] != 0` — the log is an off-chain notification convenience, not a substitute for the storage read.
`submit()` SHOULD emit a `BlockHashMemoized(l1BlockNumber)` public log on the success path (i.e. when memoization actually occurs), mirroring the L1 portal's `BlockHashPushed(n)` event so off-chain indexers, explorers, and keepers can detect "block `n` is now memoized" without polling public state. The log MUST NOT fire on the idempotent early-return branch, so the presence of a log carries the meaning "this call performed the memoization." The log's shape is non-normative; consumers MUST NOT couple to its field encoding. The authoritative signal that block `n` is memoized is `header_commitments[n] != 0` — the log is an off-chain notification convenience, not a substitute for the storage read.

Comment thread AZIPs/azip-4.md

The store consumes the inbox message exactly once, verifies that the canonical RLP-encoded L1 block header (supplied as a witness) keccak-hashes to the inbox-committed hash, and memoizes a Poseidon2 commitment of the verified RLP header into public state keyed by L1 block number. Applications then **read** the header commitment — never re-consuming the message — verify their RLP-header witness against it with a cheap Poseidon2 check (~1–2k gates in place of a ~25k-gate keccak), and run Merkle-Patricia-trie, receipt, or beacon-state proofs against the extracted roots, exactly as the in-protocol design does.

The store is a "standard contract" with a deterministic address (the same derivation recipe as today's `AuthRegistry` / `PublicChecks`: canonical salt, zero deployer, no-arg constructor), exposed to apps via an aztec-nr constant and published once by a permissionless L2 transaction — **not** a magic-slot protocol contract. The design therefore introduces no kernel, protocol-circuit, or AVM changes, no recompile of deployed application circuits, and no change to the L1 `GenesisState`; a new standard contract can be added to a running rollup without a new Rollup version. The only consensus-layer change is the new slashing offense; nodes adopt it via a grace window (detection enabled, voting disarmed) before the AZUP-scheduled activation epoch arms slashing — see Backwards Compatibility. The trade-offs are higher L1 gas per checkpoint, higher AVM gas in `submit()` (one keccak + one Poseidon2 over the ~550-byte RLP header, amortized once per L1 block across all readers), and an L1→L2 readability latency bounded below by the inbox lag (~144 s expected, plus ~72 s of additional latency per consecutive missed push).

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.

How is the L1 portal address baked into the standard contract, given there's no ctor? Immutables? Wouldn't that yield a different address per network?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants