11use alloy:: {
2- consensus:: { ReceiptEnvelope , TxReceipt } ,
3- primitives:: { Address , Bloom , Bytes , Log } ,
2+ consensus:: { proofs:: ordered_trie_root_with_encoder, ReceiptEnvelope , TxReceipt } ,
3+ eips:: eip2718:: Encodable2718 ,
4+ primitives:: { Address , Bloom , Bytes , Log , B256 } ,
45} ;
56use std:: sync:: OnceLock ;
67
@@ -18,6 +19,9 @@ pub struct BlockOutput<T: TxReceipt = ReceiptEnvelope> {
1819
1920 /// The logs bloom of the block.
2021 bloom : OnceLock < Bloom > ,
22+
23+ /// The receipt root of the block.
24+ receipt_root : OnceLock < B256 > ,
2125}
2226
2327impl Default for BlockOutput {
@@ -34,21 +38,22 @@ impl<T: TxReceipt<Log = alloy::primitives::Log>> BlockOutput<T> {
3438 receipts : Vec :: with_capacity ( capacity) ,
3539 senders : Vec :: with_capacity ( capacity) ,
3640 bloom : Default :: default ( ) ,
41+ receipt_root : Default :: default ( ) ,
3742 }
3843 }
3944
40- fn seal ( & self ) {
45+ fn seal_bloom ( & self ) {
4146 self . bloom . get_or_init ( || {
42- let mut bloom = Bloom :: default ( ) ;
43- for log in self . logs ( ) {
44- bloom. accrue_log ( log) ;
45- }
46- bloom
47+ self . receipts . iter ( ) . fold ( Bloom :: default ( ) , |mut bloom, r| {
48+ bloom |= r. bloom ( ) ;
49+ bloom
50+ } )
4751 } ) ;
4852 }
4953
5054 fn unseal ( & mut self ) {
5155 self . bloom . take ( ) ;
56+ self . receipt_root . take ( ) ;
5257 }
5358
5459 /// Reserve memory for `capacity` transaction outcomes.
@@ -70,8 +75,8 @@ impl<T: TxReceipt<Log = alloy::primitives::Log>> BlockOutput<T> {
7075
7176 /// Get the logs bloom of the block.
7277 pub fn logs_bloom ( & self ) -> Bloom {
73- self . seal ( ) ;
74- self . bloom . get ( ) . cloned ( ) . unwrap ( )
78+ self . seal_bloom ( ) ;
79+ * self . bloom . get ( ) . unwrap ( )
7580 }
7681
7782 /// Get a reference the senders of the transactions in the block.
@@ -113,10 +118,54 @@ impl<T: TxReceipt<Log = alloy::primitives::Log>> BlockOutput<T> {
113118 )
114119 }
115120
116- /// Deconstruct the block output into its parts.
117- pub fn into_parts ( self ) -> ( Vec < T > , Vec < Address > , Bloom ) {
118- let bloom = self . logs_bloom ( ) ;
119- ( self . receipts , self . senders , bloom)
121+ /// Deconstruct the block output into its parts, returning any memoized
122+ /// bloom and receipt root values.
123+ pub fn into_parts ( self ) -> ( Vec < T > , Vec < Address > , Option < Bloom > , Option < B256 > ) {
124+ ( self . receipts , self . senders , self . bloom . into_inner ( ) , self . receipt_root . into_inner ( ) )
125+ }
126+ }
127+
128+ impl < T : TxReceipt + Encodable2718 > BlockOutput < T > {
129+ /// Seal the block output, computing and memoizing the logs bloom and
130+ /// receipt root in a single pass over the receipts. The block bloom is
131+ /// derived as a side effect of receipt root computation by accumulating
132+ /// per-receipt blooms during trie encoding.
133+ ///
134+ /// Subsequent calls to [`logs_bloom`] and [`receipt_root`] will return
135+ /// the memoized values without recomputation.
136+ ///
137+ /// [`logs_bloom`]: Self::logs_bloom
138+ /// [`receipt_root`]: Self::receipt_root
139+ pub fn seal ( & self ) {
140+ if self . bloom . get ( ) . is_some ( ) && self . receipt_root . get ( ) . is_some ( ) {
141+ return ;
142+ }
143+
144+ let mut block_bloom = Bloom :: default ( ) ;
145+ let root = ordered_trie_root_with_encoder ( & self . receipts , |r, buf| {
146+ block_bloom |= r. bloom ( ) ;
147+ r. encode_2718 ( buf) ;
148+ } ) ;
149+
150+ self . bloom . get_or_init ( || block_bloom) ;
151+ self . receipt_root . get_or_init ( || root) ;
152+ }
153+
154+ /// Get the receipt root of the block.
155+ pub fn receipt_root ( & self ) -> B256 {
156+ self . seal ( ) ;
157+ * self . receipt_root . get ( ) . unwrap ( )
158+ }
159+
160+ /// Seal and deconstruct the block output into its parts.
161+ pub fn into_sealed_parts ( self ) -> ( Vec < T > , Vec < Address > , Bloom , B256 ) {
162+ self . seal ( ) ;
163+ (
164+ self . receipts ,
165+ self . senders ,
166+ self . bloom . into_inner ( ) . expect ( "seal sets bloom" ) ,
167+ self . receipt_root . into_inner ( ) . expect ( "seal sets receipt_root" ) ,
168+ )
120169 }
121170}
122171
@@ -131,6 +180,16 @@ impl<T: TxReceipt + Eq> Eq for BlockOutput<T> {}
131180#[ cfg( test) ]
132181mod tests {
133182 use super :: * ;
183+ use alloy:: {
184+ consensus:: {
185+ constants:: EMPTY_ROOT_HASH , Receipt , ReceiptEnvelope , ReceiptWithBloom , TxType ,
186+ } ,
187+ primitives:: { b256, Address , Bloom } ,
188+ } ;
189+
190+ fn envelope ( tx_type : TxType , receipt : ReceiptWithBloom < Receipt > ) -> ReceiptEnvelope {
191+ ReceiptEnvelope :: from_typed ( tx_type, receipt)
192+ }
134193
135194 #[ test]
136195 fn block_output_eq_with_one_populated_bloom ( ) {
@@ -141,4 +200,178 @@ mod tests {
141200 assert ! ( output_b. bloom. get( ) . is_none( ) ) ;
142201 assert_eq ! ( output_a, output_b) ;
143202 }
203+
204+ #[ test]
205+ fn empty_receipt_root ( ) {
206+ let output = BlockOutput :: default ( ) ;
207+ assert_eq ! ( output. receipt_root( ) , EMPTY_ROOT_HASH ) ;
208+ assert_eq ! ( output. logs_bloom( ) , Bloom :: ZERO ) ;
209+ }
210+
211+ #[ test]
212+ fn seal_computes_bloom_and_root ( ) {
213+ let output = BlockOutput :: default ( ) ;
214+ assert ! ( output. bloom. get( ) . is_none( ) ) ;
215+ assert ! ( output. receipt_root. get( ) . is_none( ) ) ;
216+
217+ output. seal ( ) ;
218+
219+ assert ! ( output. bloom. get( ) . is_some( ) ) ;
220+ assert ! ( output. receipt_root. get( ) . is_some( ) ) ;
221+ }
222+
223+ #[ test]
224+ fn single_eip2930_receipt_root ( ) {
225+ // Test vector from:
226+ // - https://github.com/alloy-rs/alloy/blob/main/crates/consensus/src/receipt/mod.rs (check_receipt_root_optimism)
227+ // - https://github.com/paradigmxyz/reth/blob/main/crates/ethereum/primitives/src/receipt.rs (check_receipt_root_optimism)
228+ let logs = vec ! [ Log :: new_unchecked( Address :: ZERO , vec![ ] , Default :: default ( ) ) ] ;
229+ let bloom = Bloom :: from ( alloy:: primitives:: hex!(
230+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"
231+ ) ) ;
232+
233+ let receipt = ReceiptWithBloom {
234+ receipt : Receipt {
235+ status : alloy:: consensus:: Eip658Value :: success ( ) ,
236+ cumulative_gas_used : 102068 ,
237+ logs,
238+ } ,
239+ logs_bloom : bloom,
240+ } ;
241+
242+ let mut output = BlockOutput :: default ( ) ;
243+ output. push_result ( envelope ( TxType :: Eip2930 , receipt) , Address :: ZERO ) ;
244+
245+ assert_eq ! (
246+ output. receipt_root( ) ,
247+ b256!( "0xfe70ae4a136d98944951b2123859698d59ad251a381abc9960fa81cae3d0d4a0" ) ,
248+ ) ;
249+ assert_eq ! ( output. logs_bloom( ) , bloom) ;
250+ }
251+
252+ #[ test]
253+ fn mixed_receipt_types ( ) {
254+ // Adapted from https://github.com/paradigmxyz/reth/blob/main/crates/engine/tree/src/tree/payload_processor/receipt_root_task.rs
255+ // (test_receipt_root_matches_standard_calculation)
256+ let default_receipt = || ReceiptWithBloom {
257+ receipt : Receipt {
258+ status : alloy:: consensus:: Eip658Value :: success ( ) ,
259+ cumulative_gas_used : 0 ,
260+ logs : vec ! [ ] ,
261+ } ,
262+ logs_bloom : Bloom :: ZERO ,
263+ } ;
264+
265+ let legacy = {
266+ let mut r = default_receipt ( ) ;
267+ r. receipt . cumulative_gas_used = 21000 ;
268+ envelope ( TxType :: Legacy , r)
269+ } ;
270+ let eip1559 = {
271+ let mut r = default_receipt ( ) ;
272+ r. receipt . cumulative_gas_used = 42000 ;
273+ r. receipt . logs =
274+ vec ! [ Log :: new_unchecked( Address :: ZERO , vec![ B256 :: ZERO ] , Bytes :: new( ) ) ] ;
275+ r. logs_bloom = alloy:: consensus:: TxReceipt :: bloom ( & r. receipt ) ;
276+ envelope ( TxType :: Eip1559 , r)
277+ } ;
278+ let eip2930 = {
279+ let mut r = default_receipt ( ) ;
280+ r. receipt . cumulative_gas_used = 63000 ;
281+ r. receipt . status = alloy:: consensus:: Eip658Value :: Eip658 ( false ) ;
282+ envelope ( TxType :: Eip2930 , r)
283+ } ;
284+
285+ let mut output = BlockOutput :: default ( ) ;
286+ output. push_result ( legacy, Address :: ZERO ) ;
287+ output. push_result ( eip1559, Address :: ZERO ) ;
288+ output. push_result ( eip2930, Address :: ZERO ) ;
289+
290+ assert_eq ! (
291+ output. receipt_root( ) ,
292+ b256!( "0xa4746a21d06f407a22200fed3491774c1b455736af092b7dcd7565de6b11da0c" ) ,
293+ ) ;
294+ assert_eq ! (
295+ output. logs_bloom( ) ,
296+ Bloom :: from( alloy:: primitives:: hex!(
297+ "00000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000800000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000"
298+ ) ) ,
299+ ) ;
300+ }
301+
302+ #[ test]
303+ fn into_sealed_parts_returns_all ( ) {
304+ let receipt = ReceiptWithBloom {
305+ receipt : Receipt {
306+ status : alloy:: consensus:: Eip658Value :: success ( ) ,
307+ cumulative_gas_used : 100 ,
308+ logs : vec ! [ ] ,
309+ } ,
310+ logs_bloom : Bloom :: ZERO ,
311+ } ;
312+
313+ let mut output = BlockOutput :: default ( ) ;
314+ output. push_result ( envelope ( TxType :: Legacy , receipt) , Address :: ZERO ) ;
315+
316+ let ( receipts, senders, bloom, root) = output. into_sealed_parts ( ) ;
317+ assert_eq ! ( receipts. len( ) , 1 ) ;
318+ assert_eq ! ( senders. len( ) , 1 ) ;
319+ assert_eq ! (
320+ root,
321+ b256!( "0x134447aac1b9bfa029f21da1cad6d6a71f841f16dc45134b24f14effe1efe791" ) ,
322+ ) ;
323+ assert_eq ! ( bloom, Bloom :: ZERO ) ;
324+ }
325+
326+ #[ test]
327+ fn into_parts_without_seal ( ) {
328+ let mut output = BlockOutput :: default ( ) ;
329+ output. push_result (
330+ envelope (
331+ TxType :: Legacy ,
332+ ReceiptWithBloom {
333+ receipt : Receipt {
334+ status : alloy:: consensus:: Eip658Value :: success ( ) ,
335+ cumulative_gas_used : 100 ,
336+ logs : vec ! [ ] ,
337+ } ,
338+ logs_bloom : Bloom :: ZERO ,
339+ } ,
340+ ) ,
341+ Address :: ZERO ,
342+ ) ;
343+
344+ let ( receipts, senders, bloom, root) = output. into_parts ( ) ;
345+ assert_eq ! ( receipts. len( ) , 1 ) ;
346+ assert_eq ! ( senders. len( ) , 1 ) ;
347+ assert ! ( bloom. is_none( ) ) ;
348+ assert ! ( root. is_none( ) ) ;
349+ }
350+
351+ #[ test]
352+ fn unseal_clears_memoized_values ( ) {
353+ let mut output = BlockOutput :: default ( ) ;
354+ output. seal ( ) ;
355+ assert ! ( output. bloom. get( ) . is_some( ) ) ;
356+ assert ! ( output. receipt_root. get( ) . is_some( ) ) ;
357+
358+ output. push_result (
359+ envelope (
360+ TxType :: Legacy ,
361+ ReceiptWithBloom {
362+ receipt : Receipt {
363+ status : alloy:: consensus:: Eip658Value :: success ( ) ,
364+ cumulative_gas_used : 100 ,
365+ logs : vec ! [ ] ,
366+ } ,
367+ logs_bloom : Bloom :: ZERO ,
368+ } ,
369+ ) ,
370+ Address :: ZERO ,
371+ ) ;
372+
373+ // push_result calls unseal
374+ assert ! ( output. bloom. get( ) . is_none( ) ) ;
375+ assert ! ( output. receipt_root. get( ) . is_none( ) ) ;
376+ }
144377}
0 commit comments