@@ -17,7 +17,7 @@ use libssz_derive::{HashTreeRoot, SszDecode, SszEncode};
1717use libssz_types:: SszList ;
1818use serde:: { Deserialize , Serialize } ;
1919
20- use crate :: primitives:: { ByteList , H256 } ;
20+ use crate :: primitives:: { ByteList , H256 , HashTreeRoot as _ } ;
2121
2222/// `BYTES_PER_LOGS_BLOOM` — fixed-size logs bloom filter.
2323pub const BYTES_PER_LOGS_BLOOM : usize = 256 ;
@@ -120,6 +120,105 @@ impl Default for ExecutionPayloadV3 {
120120 }
121121}
122122
123+ impl ExecutionPayloadV3 {
124+ /// Project this payload into its `ExecutionPayloadHeader`.
125+ ///
126+ /// Capella spec (`process_execution_payload`): variable-length `transactions`
127+ /// and `withdrawals` collapse to their SSZ hash tree roots; every other
128+ /// field copies verbatim. This is what the state caches between blocks
129+ /// so the next payload's `parent_hash` can be validated without re-hashing
130+ /// the prior block body.
131+ pub fn to_header ( & self ) -> ExecutionPayloadHeader {
132+ ExecutionPayloadHeader {
133+ parent_hash : self . parent_hash ,
134+ fee_recipient : self . fee_recipient ,
135+ state_root : self . state_root ,
136+ receipts_root : self . receipts_root ,
137+ logs_bloom : self . logs_bloom ,
138+ prev_randao : self . prev_randao ,
139+ block_number : self . block_number ,
140+ gas_limit : self . gas_limit ,
141+ gas_used : self . gas_used ,
142+ timestamp : self . timestamp ,
143+ extra_data : self . extra_data . clone ( ) ,
144+ base_fee_per_gas : self . base_fee_per_gas ,
145+ block_hash : self . block_hash ,
146+ transactions_root : self . transactions . hash_tree_root ( ) ,
147+ withdrawals_root : self . withdrawals . hash_tree_root ( ) ,
148+ blob_gas_used : self . blob_gas_used ,
149+ excess_blob_gas : self . excess_blob_gas ,
150+ }
151+ }
152+ }
153+
154+ /// Cached projection of an `ExecutionPayloadV3` that the consensus state
155+ /// carries between blocks. Mirrors the Capella+Deneb `ExecutionPayloadHeader`:
156+ /// every fixed-size field copies from the payload verbatim; the two
157+ /// variable-length lists (`transactions`, `withdrawals`) collapse to their
158+ /// SSZ hash-tree roots so the header itself stays fixed-size-bounded.
159+ #[ derive( Debug , Clone , Serialize , Deserialize , SszEncode , SszDecode , HashTreeRoot ) ]
160+ #[ serde( rename_all = "camelCase" ) ]
161+ pub struct ExecutionPayloadHeader {
162+ pub parent_hash : H256 ,
163+ #[ serde( with = "hex_address" ) ]
164+ pub fee_recipient : [ u8 ; 20 ] ,
165+ pub state_root : H256 ,
166+ pub receipts_root : H256 ,
167+ #[ serde( with = "hex_bytes_fixed" ) ]
168+ pub logs_bloom : [ u8 ; BYTES_PER_LOGS_BLOOM ] ,
169+ pub prev_randao : H256 ,
170+ #[ serde( with = "hex_u64" ) ]
171+ pub block_number : u64 ,
172+ #[ serde( with = "hex_u64" ) ]
173+ pub gas_limit : u64 ,
174+ #[ serde( with = "hex_u64" ) ]
175+ pub gas_used : u64 ,
176+ #[ serde( with = "hex_u64" ) ]
177+ pub timestamp : u64 ,
178+ #[ serde( with = "byte_list_hex" ) ]
179+ pub extra_data : ByteList < MAX_EXTRA_DATA_BYTES > ,
180+ #[ serde( with = "hex_u256" ) ]
181+ pub base_fee_per_gas : [ u8 ; 32 ] ,
182+ pub block_hash : H256 ,
183+ pub transactions_root : H256 ,
184+ pub withdrawals_root : H256 ,
185+ #[ serde( with = "hex_u64" ) ]
186+ pub blob_gas_used : u64 ,
187+ #[ serde( with = "hex_u64" ) ]
188+ pub excess_blob_gas : u64 ,
189+ }
190+
191+ /// Manual `Default` (same reason as `ExecutionPayloadV3`: `[u8; 256]`).
192+ impl Default for ExecutionPayloadHeader {
193+ fn default ( ) -> Self {
194+ Self {
195+ parent_hash : H256 :: default ( ) ,
196+ fee_recipient : [ 0u8 ; 20 ] ,
197+ state_root : H256 :: default ( ) ,
198+ receipts_root : H256 :: default ( ) ,
199+ logs_bloom : [ 0u8 ; BYTES_PER_LOGS_BLOOM ] ,
200+ prev_randao : H256 :: default ( ) ,
201+ block_number : 0 ,
202+ gas_limit : 0 ,
203+ gas_used : 0 ,
204+ timestamp : 0 ,
205+ extra_data : ByteList :: default ( ) ,
206+ base_fee_per_gas : [ 0u8 ; 32 ] ,
207+ block_hash : H256 :: default ( ) ,
208+ transactions_root : H256 :: default ( ) ,
209+ withdrawals_root : H256 :: default ( ) ,
210+ blob_gas_used : 0 ,
211+ excess_blob_gas : 0 ,
212+ }
213+ }
214+ }
215+
216+ impl From < & ExecutionPayloadV3 > for ExecutionPayloadHeader {
217+ fn from ( p : & ExecutionPayloadV3 ) -> Self {
218+ p. to_header ( )
219+ }
220+ }
221+
123222// ---------- Hex serde helpers ----------
124223//
125224// `pub` so engine-API wire types living in `ethlambda-ethrex-client`
@@ -311,8 +410,6 @@ pub mod withdrawals_serde {
311410mod tests {
312411 use super :: * ;
313412
314- use crate :: primitives:: HashTreeRoot as _;
315-
316413 #[ test]
317414 fn hex_u64_roundtrip ( ) {
318415 #[ derive( Serialize , Deserialize ) ]
@@ -470,4 +567,79 @@ mod tests {
470567 let back = Withdrawal :: from_ssz_bytes ( & bytes) . unwrap ( ) ;
471568 assert_eq ! ( back. hash_tree_root( ) , original. hash_tree_root( ) ) ;
472569 }
570+
571+ #[ test]
572+ fn execution_payload_header_default_is_zero_init ( ) {
573+ let h = ExecutionPayloadHeader :: default ( ) ;
574+ assert ! ( h. parent_hash. is_zero( ) ) ;
575+ assert ! ( h. block_hash. is_zero( ) ) ;
576+ assert ! ( h. transactions_root. is_zero( ) ) ;
577+ assert ! ( h. withdrawals_root. is_zero( ) ) ;
578+ assert_eq ! ( h. fee_recipient, [ 0u8 ; 20 ] ) ;
579+ assert_eq ! ( h. block_number, 0 ) ;
580+ }
581+
582+ #[ test]
583+ fn execution_payload_header_ssz_and_json_roundtrip ( ) {
584+ use libssz:: { SszDecode , SszEncode } ;
585+ let header = ExecutionPayloadHeader {
586+ parent_hash : H256 ( [ 1u8 ; 32 ] ) ,
587+ block_hash : H256 ( [ 2u8 ; 32 ] ) ,
588+ transactions_root : H256 ( [ 3u8 ; 32 ] ) ,
589+ withdrawals_root : H256 ( [ 4u8 ; 32 ] ) ,
590+ block_number : 42 ,
591+ timestamp : 1_700_000_000 ,
592+ ..Default :: default ( )
593+ } ;
594+
595+ let json = serde_json:: to_string ( & header) . unwrap ( ) ;
596+ let from_json: ExecutionPayloadHeader = serde_json:: from_str ( & json) . unwrap ( ) ;
597+ assert_eq ! ( from_json. hash_tree_root( ) , header. hash_tree_root( ) ) ;
598+
599+ let ssz_bytes = header. to_ssz ( ) ;
600+ let from_ssz = ExecutionPayloadHeader :: from_ssz_bytes ( & ssz_bytes) . unwrap ( ) ;
601+ assert_eq ! ( from_ssz. hash_tree_root( ) , header. hash_tree_root( ) ) ;
602+ }
603+
604+ #[ test]
605+ fn to_header_projects_lists_to_their_roots ( ) {
606+ let payload = ExecutionPayloadV3 {
607+ transactions : Transactions :: try_from ( vec ! [
608+ ByteList :: <MAX_BYTES_PER_TRANSACTION >:: try_from( vec![ 0x01 , 0x02 ] ) . unwrap( ) ,
609+ ByteList :: <MAX_BYTES_PER_TRANSACTION >:: try_from( vec![ 0x03 , 0x04 , 0x05 ] ) . unwrap( ) ,
610+ ] )
611+ . unwrap ( ) ,
612+ withdrawals : Withdrawals :: try_from ( vec ! [ Withdrawal {
613+ index: 1 ,
614+ validator_index: 2 ,
615+ address: [ 9u8 ; 20 ] ,
616+ amount: 100 ,
617+ } ] )
618+ . unwrap ( ) ,
619+ block_number : 7 ,
620+ ..Default :: default ( )
621+ } ;
622+ let header = payload. to_header ( ) ;
623+
624+ // The variable-length fields collapse to their hash tree roots.
625+ assert_eq ! (
626+ header. transactions_root,
627+ payload. transactions. hash_tree_root( )
628+ ) ;
629+ assert_eq ! (
630+ header. withdrawals_root,
631+ payload. withdrawals. hash_tree_root( )
632+ ) ;
633+ // Non-zero because both lists are non-empty.
634+ assert ! ( !header. transactions_root. is_zero( ) ) ;
635+ assert ! ( !header. withdrawals_root. is_zero( ) ) ;
636+ // Every other field copies verbatim.
637+ assert_eq ! ( header. block_number, payload. block_number) ;
638+ assert_eq ! ( header. parent_hash, payload. parent_hash) ;
639+ assert_eq ! ( header. fee_recipient, payload. fee_recipient) ;
640+
641+ // `From<&ExecutionPayloadV3>` and `to_header()` are equivalent.
642+ let header_via_from: ExecutionPayloadHeader = ( & payload) . into ( ) ;
643+ assert_eq ! ( header_via_from. hash_tree_root( ) , header. hash_tree_root( ) ) ;
644+ }
473645}
0 commit comments