Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 42 additions & 25 deletions rollup_spec/Readme.md

Large diffs are not rendered by default.

212 changes: 165 additions & 47 deletions rollup_spec/block.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from ethereum.forks.osaka.blocks import Block as EthereumBlock, Header
from ethereum.forks.osaka.transactions import (
from .fork import (
Block as EthereumBlock,
Header,
Withdrawal,
Transaction,
recover_sender,
LegacyTransaction,
decode_transaction,
ProtocolFork,
)
from ethereum.state import Address
from dataclasses import dataclass
from dataclasses import dataclass, field
from ethereum_types.numeric import U64, Uint
from ethereum.crypto.hash import Hash32, keccak256
from ethereum_types.bytes import Bytes
from ethereum_types.bytes import Bytes, Bytes32
from ethereum_rlp import rlp
from enum import Enum
from typing import List, TypeAlias
Expand Down Expand Up @@ -141,37 +144,162 @@ def tx_hash(self) -> Hash32:
return keccak256(self.signed_tx_rlp)

@dataclass
class RollupBlock:
class DepositRequest:
Comment thread
Filter94 marked this conversation as resolved.
"""
Logical l2-execution block witness.
EIP-7685 deposit request carried by `NewPayloadRequest.executionRequests`.
The SSZ decoder materializes this logical object from the guest input bytes.
"""
pubkey: bytes
withdrawal_credentials: Bytes32
amount: U64
signature: bytes
index: U64


`block_rlp` is the canonical private input supplied by the coordinator. The
Python reference decodes an `EthereumBlock` from these bytes internally when
it needs an execution view.
@dataclass
class WithdrawalRequest:
"""
EIP-7002 withdrawal request carried by `NewPayloadRequest.executionRequests`.
"""
block_rlp: bytes
forced_transactions: List[ForcedTransactionWitness]
source_address: Address
validator_pubkey: bytes
amount: U64

def block_hash(header: Header) -> Hash32:

@dataclass
class ConsolidationRequest:
"""
EIP-7251 consolidation request carried by `NewPayloadRequest.executionRequests`.
"""
source_address: Address
source_pubkey: bytes
target_pubkey: bytes


@dataclass
class ExecutionRequests:
"""
Typed Engine API execution requests.

In SSZ these are bounded lists of fixed-size request containers, per
EIP-8025. They are explicit here because the guest validates
`NewPayloadRequest`, not a canonical block RLP.
"""
deposits: List[DepositRequest] = field(default_factory=list)
withdrawals: List[WithdrawalRequest] = field(default_factory=list)
consolidations: List[ConsolidationRequest] = field(default_factory=list)


@dataclass
class StatelessChainConfig:
"""
Chain context carried inside the stateless input.

Linea still carries its proof-range `ChainConfig` outside the standard
stateless payload because it also contains Linea-specific fields such as the
L2MessageService address and coinbase. The guest must reject a payload whose
stateless `chain_id` disagrees with the proof-range chain config. `active_fork`
is the resolved `ProtocolFork` decoded from the SSZ `chain_config.active_fork`
index (validated to be the spec's single supported fork — see `fork.py`).
"""
chain_id: U64
active_fork: ProtocolFork = ProtocolFork.Amsterdam


@dataclass
class ExecutionPayload:
"""
Logical Engine API execution payload inside `NewPayloadRequest`.

`transactions` are canonical signed tx bytes; the reference decodes an
individual transaction only when it needs sender recovery or the
execution-spec view.
"""
parent_hash: Hash32
fee_recipient: Address
state_root: Hash32
receipts_root: Hash32
logs_bloom: bytes
prev_randao: Bytes32
block_number: U64
gas_limit: Uint
gas_used: Uint
timestamp: U64
extra_data: bytes
base_fee_per_gas: Uint
block_hash: Hash32
transactions: List[bytes]
withdrawals: List[Withdrawal]
blob_gas_used: U64
excess_blob_gas: U64
block_access_list: bytes = b""
slot_number: U64 | None = None


@dataclass
class NewPayloadRequest:
"""
EIP-8025 payload request supplied to the l2-execution guest (replacing the
old `blockRlp` input). Binds the proof to a concrete Engine API payload:
versioned hashes, parent beacon block root, and typed execution requests.
"""
execution_payload: ExecutionPayload
versioned_hashes: List[Hash32]
parent_beacon_block_root: Hash32
execution_requests: ExecutionRequests = field(default_factory=ExecutionRequests)


@dataclass
class StatelessInput:
"""
Decoded stateless input matching the underlying engine's guest boundary.

Forced-transaction metadata is deliberately not here — it rides on
`LineaPayloadInput.rollup_extension` so the stateless input stays vanilla.
`public_keys` is decoded only because it is part of the stateless input; Linea
derives signers via `recover_sender(chainID, tx)`, not from it.
"""
new_payload_request: NewPayloadRequest
witness: "ExecutionWitness"
chain_config: StatelessChainConfig
public_keys: List[bytes] = field(default_factory=list)


@dataclass
class LineaRollupExtension:
"""
Linea-only fields beside the vanilla stateless input. Must not be appended to
the stateless-input SSZ byte slice passed to the decoder.
"""
block_hash computes the hash of a block header
forced_transactions: List[ForcedTransactionWitness] = field(default_factory=list)


@dataclass
class LineaPayloadInput:
"""
One block of Linea l2-execution guest input.

`stateless_input_ssz` is the vanilla stateless-input byte slice
(length-delimited, decoded on its own); `rollup_extension` is Linea-only
metadata consumed after payload execution, intentionally outside the
stateless input.
"""
stateless_input_ssz: bytes
rollup_extension: LineaRollupExtension = field(default_factory=LineaRollupExtension)

def block_hash(header: Header) -> Hash32:
"""Hash of a block header."""
return keccak256(rlp.encode(header))

def decode_block_rlp(block_rlp: bytes) -> EthereumBlock:
"""
Decode the canonical block RLP carried by the l2-execution private input
into the Ethereum execution-specs block view.
Decode a canonical block RLP carried by the rollup DA witness into the
Ethereum execution-specs block view.
"""
return rlp.decode_to(EthereumBlock, block_rlp)

def decode_signed_transaction_rlp(signed_tx_rlp: bytes) -> Transaction:
"""
Decode the canonical signed transaction bytes used for forced transactions.

Typed EIP-2718 transactions are encoded as type byte || RLP(payload). Legacy
transactions are plain RLP.
"""
"""Decode a forced transaction's signed bytes (typed EIP-2718 or legacy)."""
if len(signed_tx_rlp) == 0:
raise Exception("empty signed transaction RLP")
if signed_tx_rlp[0] in (1, 2, 3, 4):
Expand All @@ -195,33 +323,23 @@ def resolve_forced_transaction(ftx: ForcedTransactionWitness, chain_id: U64) ->
)


def _canonical_transaction_bytes(transaction: BlockTransaction) -> bytes:
"""
Return the canonical signed bytes for a transaction from an execution-spec
block view.

`Block.transactions` is `Tuple[bytes | LegacyTransaction, ...]`: typed
transactions are already EIP-2718 bytes, while legacy transactions are
decoded objects and need regular RLP encoding.
"""
match transaction:
case bytes():
return transaction
case LegacyTransaction():
return rlp.encode(transaction)
def parse_payload_transaction_rlps(payload: ExecutionPayload) -> List[bytes]:
"""Return the signed transaction byte strings from an Engine API payload."""
return [bytes(tx_rlp) for tx_rlp in payload.transactions]


def parse_block_transaction_rlps(block_rlp: bytes) -> List[bytes]:
def payload_transactions_for_execution(payload: ExecutionPayload) -> tuple[BlockTransaction, ...]:
"""
Return the signed transaction RLP byte strings decoded from `block_rlp`.

`blockRlp` is the canonical witness bytes in the logical schema. Decoded
`EthereumBlock` objects are only execution views, so hash checks over block
transactions must use bytes extracted from `block_rlp` rather than
re-encoding decoded transaction objects.
Convert Engine API transaction bytes to the execution-spec `apply_body`
shape: typed transactions stay as EIP-2718 bytes, legacy transactions are
decoded to `LegacyTransaction`.
"""
block = decode_block_rlp(block_rlp)
return [
_canonical_transaction_bytes(transaction)
for transaction in block.transactions
]
transactions: List[BlockTransaction] = []
for tx_rlp in parse_payload_transaction_rlps(payload):
if len(tx_rlp) == 0:
raise Exception("empty transaction in payload")
if tx_rlp[0] in (1, 2, 3, 4):
transactions.append(tx_rlp)
else:
transactions.append(rlp.decode_to(LegacyTransaction, tx_rlp))
return tuple(transactions)
119 changes: 119 additions & 0 deletions rollup_spec/canonical_ssz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""
Canonical consensus-layer SSZ containers, copied verbatim from consensus-specs
@ v1.6.1 so they track upstream as forks evolve. They use `remerkleable` (the
library consensus-specs generates against), so the class bodies are identical to
upstream — only the imports and `source:` comments are local.

To re-sync: bump the tag, re-copy the affected `class`/alias/constant bodies from
the referenced spec files, and re-run the stateless-input SSZ decode round-trip
(`stateless_input.py::decode_stateless_input_ssz`, guarded by `_strict_decode`).

Comment thread
Filter94 marked this conversation as resolved.
Only canonical CL containers live here; the bespoke EIP-8025 stateless-input
envelope is in `stateless_input.py`.
"""

from remerkleable.basic import uint64, uint256
from remerkleable.byte_arrays import ByteList, ByteVector, Bytes32, Bytes48, Bytes96
from remerkleable.complex import Container, List

# ── Preset constants ─────────────────────────────────────────────────────────
# source: presets/mainnet/bellatrix.yaml
# https://github.com/ethereum/consensus-specs/blob/v1.6.1/presets/mainnet/bellatrix.yaml
MAX_BYTES_PER_TRANSACTION = 2**30 # 1073741824
MAX_TRANSACTIONS_PER_PAYLOAD = 2**20 # 1048576
BYTES_PER_LOGS_BLOOM = 2**8 # 256
MAX_EXTRA_DATA_BYTES = 2**5 # 32
# source: presets/mainnet/capella.yaml
# https://github.com/ethereum/consensus-specs/blob/v1.6.1/presets/mainnet/capella.yaml
MAX_WITHDRAWALS_PER_PAYLOAD = 2**4 # 16
# source: presets/mainnet/electra.yaml
# https://github.com/ethereum/consensus-specs/blob/v1.6.1/presets/mainnet/electra.yaml
MAX_DEPOSIT_REQUESTS_PER_PAYLOAD = 2**13 # 8192
MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD = 2**4 # 16
MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD = 2**1 # 2

# ── Custom type aliases ──────────────────────────────────────────────────────
# source: specs/phase0/beacon-chain.md (Custom types table)
# https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/phase0/beacon-chain.md
Hash32 = Bytes32
Gwei = uint64
ValidatorIndex = uint64
BLSPubkey = Bytes48
BLSSignature = Bytes96
# source: specs/capella/beacon-chain.md (Custom types table)
# https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/capella/beacon-chain.md
WithdrawalIndex = uint64
# source: specs/bellatrix/beacon-chain.md (Custom types table)
# https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/bellatrix/beacon-chain.md
ExecutionAddress = ByteVector[20] # Bytes20
Transaction = ByteList[MAX_BYTES_PER_TRANSACTION]

# ── Containers (verbatim) ────────────────────────────────────────────────────
# source: specs/capella/beacon-chain.md
# https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/capella/beacon-chain.md


class Withdrawal(Container):
index: WithdrawalIndex
validator_index: ValidatorIndex
address: ExecutionAddress
amount: Gwei


# source: specs/deneb/beacon-chain.md (unchanged through Electra/Fulu @ v1.6.1)
# https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/deneb/beacon-chain.md


class ExecutionPayload(Container):
parent_hash: Hash32
fee_recipient: ExecutionAddress
state_root: Bytes32
receipts_root: Bytes32
logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM]
prev_randao: Bytes32
block_number: uint64
gas_limit: uint64
gas_used: uint64
timestamp: uint64
extra_data: ByteList[MAX_EXTRA_DATA_BYTES]
base_fee_per_gas: uint256
block_hash: Hash32
transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD]
withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD]
# [New in Deneb:EIP4844]
blob_gas_used: uint64
# [New in Deneb:EIP4844]
excess_blob_gas: uint64


# source: specs/electra/beacon-chain.md
# https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/electra/beacon-chain.md


class DepositRequest(Container):
pubkey: BLSPubkey
withdrawal_credentials: Bytes32
amount: Gwei
signature: BLSSignature
index: uint64


class WithdrawalRequest(Container):
source_address: ExecutionAddress
validator_pubkey: BLSPubkey
amount: Gwei


class ConsolidationRequest(Container):
source_address: ExecutionAddress
source_pubkey: BLSPubkey
target_pubkey: BLSPubkey


class ExecutionRequests(Container):
# [New in Electra:EIP6110]
deposits: List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD]
# [New in Electra:EIP7002:EIP7251]
withdrawals: List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD]
# [New in Electra:EIP7251]
consolidations: List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD]
Loading
Loading