-
Notifications
You must be signed in to change notification settings - Fork 206
feat(l1): fix EIP-8025 compliance #6516
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
28ef2d4
dfa9929
a485d97
0058d27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,33 +4,38 @@ | |
| //! `NewPayloadRequest` and producing the `PublicInput` committed to by | ||
| //! execution proofs. | ||
|
|
||
| use bytes::Bytes; | ||
| use libssz::{SszDecode, SszEncode}; | ||
| use libssz_derive::{HashTreeRoot, SszDecode, SszEncode}; | ||
| use libssz_merkle::{HashTreeRoot, Sha256Hasher}; | ||
| use libssz_types::{SszList, SszVector}; | ||
|
|
||
| use super::requests::EncodedRequests; | ||
|
|
||
| // ── Spec limits (Electra) ────────────────────────────────────────── | ||
|
|
||
| /// MAX_TRANSACTIONS_PER_PAYLOAD (Electra). | ||
| const MAX_TRANSACTIONS: usize = 1_048_576; | ||
| /// MAX_WITHDRAWALS_PER_PAYLOAD (Electra). | ||
| const MAX_WITHDRAWALS: usize = 16; | ||
| /// MAX_BYTES_PER_TRANSACTION. | ||
| /// `MAX_TRANSACTIONS_PER_PAYLOAD` (Electra). | ||
| const MAX_TRANSACTIONS_PER_PAYLOAD: usize = 1_048_576; | ||
| /// `MAX_WITHDRAWALS_PER_PAYLOAD` (Electra). | ||
| const MAX_WITHDRAWALS_PER_PAYLOAD: usize = 16; | ||
| /// `MAX_BYTES_PER_TRANSACTION`. | ||
| const MAX_BYTES_PER_TRANSACTION: usize = 1_073_741_824; | ||
| /// MAX_EXTRA_DATA_BYTES. | ||
| /// `MAX_EXTRA_DATA_BYTES`. | ||
| const MAX_EXTRA_DATA_BYTES: usize = 32; | ||
| /// MAX_DEPOSIT_REQUESTS_PER_PAYLOAD (Electra). | ||
| const MAX_DEPOSIT_REQUESTS: usize = 8192; | ||
| /// MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD (Electra). | ||
| const MAX_WITHDRAWAL_REQUESTS: usize = 16; | ||
| /// MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD (Electra). | ||
| const MAX_CONSOLIDATION_REQUESTS: usize = 1; | ||
|
Comment on lines
-26
to
-27
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that the previous |
||
| /// MAX_BLOB_COMMITMENTS_PER_BLOCK (Electra). | ||
| const MAX_BLOB_COMMITMENTS: usize = 4096; | ||
| /// MAX_EXECUTION_REQUESTS (EIP-7685). | ||
| const MAX_EXECUTION_REQUESTS: usize = 16; | ||
| /// MAX_EXECUTION_REQUEST_BYTES. | ||
| const MAX_EXECUTION_REQUEST_BYTES: usize = 1_073_741_824; | ||
| /// `MAX_DEPOSIT_REQUESTS_PER_PAYLOAD` (Electra). | ||
| const MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: usize = 8192; | ||
| /// `MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD` (Electra). | ||
| const MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: usize = 16; | ||
| /// `MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD` (Electra). | ||
| const MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: usize = 2; | ||
| /// `MAX_BLOB_COMMITMENTS_PER_BLOCK` (Electra). | ||
| const MAX_BLOB_COMMITMENTS_PER_BLOCK: usize = 4096; | ||
|
|
||
| // ── EIP-7685 request type prefixes ───────────────────────────────── | ||
|
|
||
| const DEPOSIT_REQUEST_TYPE: u8 = 0x00; | ||
| const WITHDRAWAL_REQUEST_TYPE: u8 = 0x01; | ||
| const CONSOLIDATION_REQUEST_TYPE: u8 = 0x02; | ||
|
|
||
| // ── Bytes20 wrapper (address) ────────────────────────────────────── | ||
| // | ||
|
|
@@ -95,7 +100,7 @@ impl From<Bytes20> for [u8; 20] { | |
| // `logs_bloom` is `ByteVector[BYTES_PER_LOGS_BLOOM]` in the CL spec — | ||
| // a fixed-length SSZ vector of 256 bytes. | ||
|
|
||
| /// BYTES_PER_LOGS_BLOOM from the CL spec. | ||
| /// `BYTES_PER_LOGS_BLOOM` from the CL spec. | ||
| pub const BYTES_PER_LOGS_BLOOM: usize = 256; | ||
|
|
||
| /// `ByteVector[256]` — the logs bloom as a fixed-size SSZ vector. | ||
|
|
@@ -158,41 +163,52 @@ pub struct ExecutionPayload { | |
| /// `base_fee_per_gas` encoded as a 256-bit unsigned integer (little-endian). | ||
| pub base_fee_per_gas: [u8; 32], | ||
| pub block_hash: [u8; 32], | ||
| pub transactions: SszList<SszList<u8, MAX_BYTES_PER_TRANSACTION>, MAX_TRANSACTIONS>, | ||
| pub withdrawals: SszList<Withdrawal, MAX_WITHDRAWALS>, | ||
| pub transactions: SszList<SszList<u8, MAX_BYTES_PER_TRANSACTION>, MAX_TRANSACTIONS_PER_PAYLOAD>, | ||
| pub withdrawals: SszList<Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD>, | ||
| pub blob_gas_used: u64, | ||
| pub excess_blob_gas: u64, | ||
| pub deposit_requests: SszList<DepositRequest, MAX_DEPOSIT_REQUESTS>, | ||
| pub withdrawal_requests: SszList<WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS>, | ||
| pub consolidation_requests: SszList<ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS>, | ||
|
Comment on lines
-165
to
-167
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the specs, |
||
| } | ||
|
|
||
| // ── ExecutionPayloadHeader ───────────────────────────────────────── | ||
| // ── ExecutionRequests ────────────────────────────────────────────── | ||
|
|
||
| /// Headerized version of `ExecutionPayload`: variable-length lists are | ||
| /// replaced by their `hash_tree_root`. | ||
| /// SSZ `ExecutionRequests` container (Electra) — the typed EIP-7685 bundle | ||
| /// that the CL commits to alongside `ExecutionPayload`. | ||
| #[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] | ||
| pub struct ExecutionPayloadHeader { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't strictly required so we can remove. |
||
| pub parent_hash: [u8; 32], | ||
| pub fee_recipient: Bytes20, | ||
| pub state_root: [u8; 32], | ||
| pub receipts_root: [u8; 32], | ||
| pub logs_bloom: LogsBloom, | ||
| pub prev_randao: [u8; 32], | ||
| pub block_number: u64, | ||
| pub gas_limit: u64, | ||
| pub gas_used: u64, | ||
| pub timestamp: u64, | ||
| pub extra_data: SszList<u8, MAX_EXTRA_DATA_BYTES>, | ||
| pub base_fee_per_gas: [u8; 32], | ||
| pub block_hash: [u8; 32], | ||
| pub transactions_root: [u8; 32], | ||
| pub withdrawals_root: [u8; 32], | ||
| pub blob_gas_used: u64, | ||
| pub excess_blob_gas: u64, | ||
| pub deposit_requests_root: [u8; 32], | ||
| pub withdrawal_requests_root: [u8; 32], | ||
| pub consolidation_requests_root: [u8; 32], | ||
| pub struct ExecutionRequests { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my next comment to understand this. |
||
| pub deposits: SszList<DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD>, | ||
| pub withdrawals: SszList<WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD>, | ||
| pub consolidations: SszList<ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD>, | ||
| } | ||
|
|
||
| impl ExecutionRequests { | ||
| /// Produce the EIP-7685 encoded form: three `EncodedRequests` entries, | ||
| /// one per request type, each `[type_byte] ++ concat(ssz_encode(item))`. | ||
| /// | ||
| /// The three request types are all fixed-size SSZ containers, so their | ||
| /// SSZ encoding is byte-for-byte the EL wire concatenation that | ||
| /// `compute_requests_hash` expects. | ||
| pub fn to_encoded_requests(&self) -> Vec<EncodedRequests> { | ||
| fn encode<T: SszEncode>( | ||
| type_byte: u8, | ||
| items: impl IntoIterator<Item = T>, | ||
| ) -> EncodedRequests { | ||
| let mut buf = Vec::new(); | ||
| buf.push(type_byte); | ||
| for item in items { | ||
| item.ssz_append(&mut buf); | ||
| } | ||
| EncodedRequests(Bytes::from(buf)) | ||
| } | ||
|
|
||
| vec![ | ||
| encode(DEPOSIT_REQUEST_TYPE, self.deposits.iter().cloned()), | ||
| encode(WITHDRAWAL_REQUEST_TYPE, self.withdrawals.iter().cloned()), | ||
| encode( | ||
| CONSOLIDATION_REQUEST_TYPE, | ||
| self.consolidations.iter().cloned(), | ||
| ), | ||
| ] | ||
| } | ||
|
Comment on lines
+183
to
+211
|
||
| } | ||
|
|
||
| // ── NewPayloadRequest ────────────────────────────────────────────── | ||
|
|
@@ -202,20 +218,9 @@ pub struct ExecutionPayloadHeader { | |
| #[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] | ||
| pub struct NewPayloadRequest { | ||
| pub execution_payload: ExecutionPayload, | ||
| pub versioned_hashes: SszList<[u8; 32], MAX_BLOB_COMMITMENTS>, | ||
| pub parent_beacon_block_root: [u8; 32], | ||
| pub execution_requests: | ||
| SszList<SszList<u8, MAX_EXECUTION_REQUEST_BYTES>, MAX_EXECUTION_REQUESTS>, | ||
| } | ||
|
|
||
| /// Headerized version of `NewPayloadRequest`. | ||
| #[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] | ||
| pub struct NewPayloadRequestHeader { | ||
| pub execution_payload_header: ExecutionPayloadHeader, | ||
| pub versioned_hashes: SszList<[u8; 32], MAX_BLOB_COMMITMENTS>, | ||
| pub versioned_hashes: SszList<[u8; 32], MAX_BLOB_COMMITMENTS_PER_BLOCK>, | ||
| pub parent_beacon_block_root: [u8; 32], | ||
| pub execution_requests: | ||
| SszList<SszList<u8, MAX_EXECUTION_REQUEST_BYTES>, MAX_EXECUTION_REQUESTS>, | ||
| pub execution_requests: ExecutionRequests, | ||
|
Comment on lines
-217
to
+223
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The consensus-specs don't define Fixing it here. |
||
| } | ||
|
|
||
| // ── PublicInput ──────────────────────────────────────────────────── | ||
|
|
@@ -227,37 +232,6 @@ pub struct PublicInput { | |
| pub new_payload_request_root: [u8; 32], | ||
| } | ||
|
|
||
| // ── Conversion helpers ───────────────────────────────────────────── | ||
|
|
||
| impl ExecutionPayload { | ||
| /// Produce the headerized version by computing `hash_tree_root` of | ||
| /// each variable-length list field. | ||
| pub fn to_header(&self, hasher: &impl Sha256Hasher) -> ExecutionPayloadHeader { | ||
| ExecutionPayloadHeader { | ||
| parent_hash: self.parent_hash, | ||
| fee_recipient: self.fee_recipient, | ||
| state_root: self.state_root, | ||
| receipts_root: self.receipts_root, | ||
| logs_bloom: self.logs_bloom.clone(), | ||
| prev_randao: self.prev_randao, | ||
| block_number: self.block_number, | ||
| gas_limit: self.gas_limit, | ||
| gas_used: self.gas_used, | ||
| timestamp: self.timestamp, | ||
| extra_data: self.extra_data.clone(), | ||
| base_fee_per_gas: self.base_fee_per_gas, | ||
| block_hash: self.block_hash, | ||
| transactions_root: self.transactions.hash_tree_root(hasher), | ||
| withdrawals_root: self.withdrawals.hash_tree_root(hasher), | ||
| blob_gas_used: self.blob_gas_used, | ||
| excess_blob_gas: self.excess_blob_gas, | ||
| deposit_requests_root: self.deposit_requests.hash_tree_root(hasher), | ||
| withdrawal_requests_root: self.withdrawal_requests.hash_tree_root(hasher), | ||
| consolidation_requests_root: self.consolidation_requests.hash_tree_root(hasher), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl NewPayloadRequest { | ||
| /// Compute the `hash_tree_root` of this request — the value that | ||
| /// becomes the execution proof's public input. | ||
|
|
@@ -266,16 +240,6 @@ impl NewPayloadRequest { | |
| new_payload_request_root: self.hash_tree_root(hasher), | ||
| } | ||
| } | ||
|
|
||
| /// Produce the headerized version. | ||
| pub fn to_header(&self, hasher: &impl Sha256Hasher) -> NewPayloadRequestHeader { | ||
| NewPayloadRequestHeader { | ||
| execution_payload_header: self.execution_payload.to_header(hasher), | ||
| versioned_hashes: self.versioned_hashes.clone(), | ||
| parent_beacon_block_root: self.parent_beacon_block_root, | ||
| execution_requests: self.execution_requests.clone(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
|
|
@@ -291,61 +255,56 @@ mod tests { | |
| fee_recipient: Bytes20([2u8; 20]), | ||
| state_root: [3u8; 32], | ||
| receipts_root: [4u8; 32], | ||
| logs_bloom: vec![0u8; 256].try_into().unwrap(), | ||
| logs_bloom: vec![0u8; 256].try_into().expect("logs_bloom length"), | ||
| prev_randao: [5u8; 32], | ||
| block_number: 42, | ||
| gas_limit: 30_000_000, | ||
| gas_used: 21_000, | ||
| timestamp: 1_700_000_000, | ||
| extra_data: vec![0xAB, 0xCD].try_into().unwrap(), | ||
| extra_data: vec![0xAB, 0xCD].try_into().expect("extra_data fits"), | ||
| base_fee_per_gas: { | ||
| let mut b = [0u8; 32]; | ||
| b[0] = 7; // 7 in LE | ||
| b | ||
| }, | ||
| block_hash: [6u8; 32], | ||
| transactions: vec![vec![0xDE, 0xAD, 0xBE, 0xEF].try_into().unwrap()] | ||
| .try_into() | ||
| .unwrap(), | ||
| transactions: vec![ | ||
| vec![0xDE, 0xAD, 0xBE, 0xEF] | ||
| .try_into() | ||
| .expect("tx bytes fit"), | ||
| ] | ||
| .try_into() | ||
| .expect("txs fit"), | ||
| withdrawals: vec![Withdrawal { | ||
| index: 0, | ||
| validator_index: 1, | ||
| address: Bytes20([7u8; 20]), | ||
| amount: 1_000_000, | ||
| }] | ||
| .try_into() | ||
| .unwrap(), | ||
| .expect("withdrawals fit"), | ||
| blob_gas_used: 0, | ||
| excess_blob_gas: 0, | ||
| deposit_requests: vec![].try_into().unwrap(), | ||
| withdrawal_requests: vec![].try_into().unwrap(), | ||
| consolidation_requests: vec![].try_into().unwrap(), | ||
| } | ||
| } | ||
|
|
||
| fn empty_requests() -> ExecutionRequests { | ||
| ExecutionRequests { | ||
| deposits: vec![].try_into().expect("empty deposits"), | ||
| withdrawals: vec![].try_into().expect("empty withdrawals"), | ||
| consolidations: vec![].try_into().expect("empty consolidations"), | ||
| } | ||
| } | ||
|
|
||
| fn sample_request() -> NewPayloadRequest { | ||
| NewPayloadRequest { | ||
| execution_payload: sample_payload(), | ||
| versioned_hashes: vec![].try_into().unwrap(), | ||
| versioned_hashes: vec![].try_into().expect("empty versioned_hashes"), | ||
| parent_beacon_block_root: [8u8; 32], | ||
| execution_requests: vec![].try_into().unwrap(), | ||
| execution_requests: empty_requests(), | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_ssz_root_roundtrip_payload_vs_header() { | ||
| let request = sample_request(); | ||
| let header = request.to_header(&HASHER); | ||
|
|
||
| let request_root = request.hash_tree_root(&HASHER); | ||
| let header_root = header.hash_tree_root(&HASHER); | ||
|
|
||
| assert_eq!( | ||
| request_root, header_root, | ||
| "NewPayloadRequest root must equal NewPayloadRequestHeader root" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_ssz_root_changes_with_different_data() { | ||
| let request1 = sample_request(); | ||
|
|
@@ -359,44 +318,55 @@ mod tests { | |
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_empty_vs_nonempty_list_roots_differ() { | ||
| let payload = sample_payload(); | ||
| let header = payload.to_header(&HASHER); | ||
|
|
||
| let empty_payload = ExecutionPayload { | ||
| transactions: vec![].try_into().unwrap(), | ||
| withdrawals: vec![].try_into().unwrap(), | ||
| deposit_requests: vec![].try_into().unwrap(), | ||
| withdrawal_requests: vec![].try_into().unwrap(), | ||
| consolidation_requests: vec![].try_into().unwrap(), | ||
| ..sample_payload() | ||
| }; | ||
| let empty_header = empty_payload.to_header(&HASHER); | ||
|
|
||
| // Non-empty lists (sample has 1 tx + 1 withdrawal) produce different roots | ||
| // than empty lists. | ||
| assert_ne!( | ||
| header.transactions_root, empty_header.transactions_root, | ||
| "Non-empty transactions root must differ from empty" | ||
| ); | ||
| assert_ne!( | ||
| header.withdrawals_root, empty_header.withdrawals_root, | ||
| "Non-empty withdrawals root must differ from empty" | ||
| ); | ||
|
|
||
| // deposit/withdrawal/consolidation requests are empty in both, so roots match. | ||
| assert_eq!( | ||
| header.deposit_requests_root, empty_header.deposit_requests_root, | ||
| "Both-empty deposit_requests roots must match" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_ssz_root_is_deterministic() { | ||
| let request = sample_request(); | ||
| let root1 = request.hash_tree_root(&HASHER); | ||
| let root2 = request.hash_tree_root(&HASHER); | ||
| assert_eq!(root1, root2, "Same request must produce same root"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_execution_requests_to_encoded_bytes() { | ||
| let requests = ExecutionRequests { | ||
| deposits: vec![DepositRequest { | ||
| pubkey: [0x11; 48], | ||
| withdrawal_credentials: [0x22; 32], | ||
| amount: 32_000_000_000, | ||
| signature: [0x33; 96], | ||
| index: 7, | ||
| }] | ||
| .try_into() | ||
| .expect("one deposit fits"), | ||
| withdrawals: vec![WithdrawalRequest { | ||
| source_address: Bytes20([0x44; 20]), | ||
| validator_pubkey: [0x55; 48], | ||
| amount: 1_000_000, | ||
| }] | ||
| .try_into() | ||
| .expect("one withdrawal fits"), | ||
| consolidations: vec![ConsolidationRequest { | ||
| source_address: Bytes20([0x66; 20]), | ||
| source_pubkey: [0x77; 48], | ||
| target_pubkey: [0x88; 48], | ||
| }] | ||
| .try_into() | ||
| .expect("one consolidation fits"), | ||
| }; | ||
|
|
||
| let encoded = requests.to_encoded_requests(); | ||
| assert_eq!(encoded.len(), 3, "must emit 3 EIP-7685 entries"); | ||
|
|
||
| // Deposit: [0x00] ++ 192 bytes | ||
| assert_eq!(encoded[0].0[0], DEPOSIT_REQUEST_TYPE); | ||
| assert_eq!(encoded[0].0.len(), 1 + 192); | ||
|
|
||
| // Withdrawal: [0x01] ++ 76 bytes | ||
| assert_eq!(encoded[1].0[0], WITHDRAWAL_REQUEST_TYPE); | ||
| assert_eq!(encoded[1].0.len(), 1 + 76); | ||
|
|
||
| // Consolidation: [0x02] ++ 116 bytes | ||
| assert_eq!(encoded[2].0[0], CONSOLIDATION_REQUEST_TYPE); | ||
| assert_eq!(encoded[2].0.len(), 1 + 116); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some renamings to conform to the consensus-spec names, but also some fixes that I'll comment in particular.