Skip to main content

tempo_payload_builder/
encode.rs

1//! Execution block encoding helpers for the payload builder.
2//!
3//! This module carries transaction-list bytes produced by the roots task into the asynchronous
4//! block encoder so payload construction can reuse already encoded transactions while preserving
5//! byte-for-byte compatibility with regular RLP block encoding.
6
7use alloy_consensus::BlockHeader as _;
8use alloy_eips::eip2718::Typed2718;
9use alloy_primitives::Bytes;
10use alloy_rlp::Encodable;
11use reth_primitives_traits::{RecoveredBlock, SealedBlock};
12use std::sync::Arc;
13use tempo_payload_types::EncodedBlock;
14use tempo_primitives::TempoTxEnvelope;
15use tracing::warn;
16
17/// RLP transaction-list bytes for the execution block.
18///
19/// The roots task already encodes every transaction in EIP-2718 form for the transaction trie. This
20/// stores those bytes in the form required by the block body transaction list, so the later full
21/// block encoder can copy the transaction-list RLP instead of encoding every transaction again.
22#[derive(Clone, Debug)]
23pub(crate) struct EncodedBlockTransactionList {
24    transaction_count: usize,
25    /// Complete RLP list for the block body transactions field.
26    ///
27    /// Legacy transactions are list elements and typed EIP-2718 transactions are string elements.
28    rlp: Bytes,
29}
30
31impl EncodedBlockTransactionList {
32    /// Encodes the block with these already encoded transactions when the transaction count matches.
33    ///
34    /// Falls back to a full block encoding if the transaction count does not match.
35    ///
36    /// Returns `true` if the cached transactions were reused.
37    fn encode_block_with_transactions(
38        &self,
39        block: &SealedBlock<tempo_primitives::Block>,
40        out: &mut Vec<u8>,
41    ) -> bool {
42        let body = block.body();
43        if body.transactions.len() != self.transaction_count {
44            warn!(
45                block_number = block.number(),
46                block_hash = ?block.hash(),
47                block_transactions = body.transactions.len(),
48                encoded_transactions = self.transaction_count,
49                "cached execution block transaction list did not match block body"
50            );
51            block.encode(out);
52            return false;
53        }
54
55        let payload_length = block.header().length()
56            + self.rlp.len()
57            + body.ommers.length()
58            + body.withdrawals.as_ref().map_or(0, Encodable::length);
59
60        alloy_rlp::Header {
61            list: true,
62            payload_length,
63        }
64        .encode(out);
65        block.header().encode(out);
66        // The remaining fields are the block body encoding: transactions, ommers, withdrawals.
67        out.extend_from_slice(&self.rlp);
68        body.ommers.encode(out);
69        if let Some(withdrawals) = &body.withdrawals {
70            withdrawals.encode(out);
71        }
72
73        true
74    }
75}
76
77/// Incrementally builds the RLP transaction-list bytes used inside the execution block body.
78#[derive(Debug, Default)]
79pub(crate) struct EncodedBlockTransactionsBuilder {
80    transaction_count: usize,
81    payload: Vec<u8>,
82}
83
84impl EncodedBlockTransactionsBuilder {
85    /// Appends one encoded transaction as a block-body transaction-list element.
86    ///
87    /// Legacy transaction bytes are already RLP list elements. Typed EIP-2718 transaction bytes are
88    /// wrapped as an RLP string so the final transaction list matches regular block encoding.
89    pub(crate) fn push(&mut self, transaction: &TempoTxEnvelope, encoded_2718: &[u8]) {
90        self.transaction_count += 1;
91        if !transaction.is_legacy() {
92            alloy_rlp::Header {
93                list: false,
94                payload_length: encoded_2718.len(),
95            }
96            .encode(&mut self.payload);
97        }
98        self.payload.extend_from_slice(encoded_2718);
99    }
100
101    pub(crate) fn finish(self) -> EncodedBlockTransactionList {
102        let header = alloy_rlp::Header {
103            list: true,
104            payload_length: self.payload.len(),
105        };
106        let mut rlp = Vec::with_capacity(header.length_with_payload());
107        header.encode(&mut rlp);
108        rlp.extend_from_slice(&self.payload);
109        EncodedBlockTransactionList {
110            transaction_count: self.transaction_count,
111            rlp: rlp.into(),
112        }
113    }
114}
115
116/// Encodes the execution block into the shared cache when dropped.
117///
118/// The payload builder creates this after assembling a recovered block, passes a clone of
119/// `encoded_block` into `TempoBuiltPayload`, then moves the encoder into a blocking task. Dropping
120/// the encoder performs the actual block RLP encoding unless another consumer has already filled
121/// the cache. When available, the encoder reuses transaction-list bytes produced by the roots task
122/// instead of encoding every transaction again.
123#[derive(Debug)]
124pub(crate) struct ExecutionBlockEncoder {
125    block: Arc<RecoveredBlock<tempo_primitives::Block>>,
126    estimated_rlp_block_size: usize,
127    encoded_transactions: EncodedBlockTransactionList,
128    encoded_block: EncodedBlock,
129}
130
131impl ExecutionBlockEncoder {
132    pub(crate) fn new(
133        block: Arc<RecoveredBlock<tempo_primitives::Block>>,
134        estimated_rlp_block_size: usize,
135        encoded_transactions: EncodedBlockTransactionList,
136    ) -> Self {
137        Self {
138            block,
139            estimated_rlp_block_size,
140            encoded_transactions,
141            encoded_block: EncodedBlock::default(),
142        }
143    }
144
145    pub(crate) fn encoded_block(&self) -> EncodedBlock {
146        self.encoded_block.clone()
147    }
148
149    pub(crate) fn encode_block(&self) -> &Bytes {
150        self.encoded_block.get_or_encode_with(|| {
151            let block = self.block.sealed_block();
152            let mut encoded = Vec::with_capacity(self.estimated_rlp_block_size);
153            self.encoded_transactions
154                .encode_block_with_transactions(block, &mut encoded);
155            encoded.into()
156        })
157    }
158}
159
160impl Drop for ExecutionBlockEncoder {
161    fn drop(&mut self) {
162        let _ = self.encode_block();
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use alloy_consensus::{BlockBody, Signed, TxEip1559, TxLegacy};
170    use alloy_eips::{
171        eip2718::Encodable2718,
172        eip4895::{Withdrawal, Withdrawals},
173    };
174    use alloy_primitives::{Address, B256, Bytes, Signature, U256};
175    use alloy_rlp::Encodable;
176    use proptest::prelude::*;
177    use reth_primitives_traits::{RecoveredBlock, SealedBlock};
178    use std::sync::Arc;
179    use tempo_primitives::{Block, Header, TempoHeader};
180
181    fn arb_address() -> impl Strategy<Value = Address> {
182        any::<[u8; 20]>().prop_map(Address::from)
183    }
184
185    fn arb_b256() -> impl Strategy<Value = B256> {
186        any::<[u8; 32]>().prop_map(B256::from)
187    }
188
189    fn arb_bytes(max_len: usize) -> impl Strategy<Value = Bytes> {
190        prop::collection::vec(any::<u8>(), 0..=max_len).prop_map(Bytes::from)
191    }
192
193    fn arb_u256() -> impl Strategy<Value = U256> {
194        any::<[u64; 4]>().prop_map(U256::from_limbs)
195    }
196
197    fn arb_legacy_tx() -> impl Strategy<Value = TempoTxEnvelope> {
198        (
199            prop::option::of(any::<u64>()),
200            any::<u64>(),
201            any::<u128>(),
202            any::<u64>(),
203            arb_address(),
204            arb_u256(),
205            arb_bytes(128),
206        )
207            .prop_map(
208                |(chain_id, nonce, gas_price, gas_limit, to, value, input)| {
209                    TempoTxEnvelope::Legacy(Signed::new_unhashed(
210                        TxLegacy {
211                            chain_id,
212                            nonce,
213                            gas_price,
214                            gas_limit,
215                            to: to.into(),
216                            value,
217                            input,
218                        },
219                        Signature::test_signature(),
220                    ))
221                },
222            )
223    }
224
225    fn arb_eip1559_tx() -> impl Strategy<Value = TempoTxEnvelope> {
226        (
227            any::<u64>(),
228            any::<u64>(),
229            any::<u64>(),
230            any::<u128>(),
231            any::<u128>(),
232            arb_address(),
233            arb_u256(),
234            arb_bytes(128),
235        )
236            .prop_map(
237                |(
238                    chain_id,
239                    nonce,
240                    gas_limit,
241                    max_fee_per_gas,
242                    max_priority_fee_per_gas,
243                    to,
244                    value,
245                    input,
246                )| {
247                    TempoTxEnvelope::Eip1559(Signed::new_unhashed(
248                        TxEip1559 {
249                            chain_id,
250                            nonce,
251                            gas_limit,
252                            max_fee_per_gas,
253                            max_priority_fee_per_gas,
254                            to: to.into(),
255                            value,
256                            access_list: Default::default(),
257                            input,
258                        },
259                        Signature::test_signature(),
260                    ))
261                },
262            )
263    }
264
265    fn arb_tx() -> impl Strategy<Value = TempoTxEnvelope> {
266        prop_oneof![arb_legacy_tx(), arb_eip1559_tx()]
267    }
268
269    fn arb_header() -> impl Strategy<Value = TempoHeader> {
270        (
271            (
272                arb_b256(),
273                arb_address(),
274                arb_b256(),
275                arb_b256(),
276                arb_b256(),
277            ),
278            (
279                any::<u64>(),
280                any::<u64>(),
281                any::<u64>(),
282                any::<u64>(),
283                arb_bytes(32),
284            ),
285            (
286                prop::option::of(any::<u64>()),
287                any::<u64>(),
288                any::<u64>(),
289                any::<u64>(),
290            ),
291        )
292            .prop_map(
293                |(
294                    (parent_hash, beneficiary, state_root, transactions_root, receipts_root),
295                    (number, gas_limit, gas_used, timestamp, extra_data),
296                    (base_fee_per_gas, general_gas_limit, shared_gas_limit, timestamp_millis_part),
297                )| TempoHeader {
298                    general_gas_limit,
299                    shared_gas_limit,
300                    timestamp_millis_part,
301                    inner: Header {
302                        parent_hash,
303                        beneficiary,
304                        state_root,
305                        transactions_root,
306                        receipts_root,
307                        number,
308                        gas_limit,
309                        gas_used,
310                        timestamp,
311                        extra_data,
312                        base_fee_per_gas,
313                        ..Default::default()
314                    },
315                    consensus_context: None,
316                },
317            )
318    }
319
320    fn arb_withdrawals() -> impl Strategy<Value = Option<Withdrawals>> {
321        prop::option::of(
322            prop::collection::vec(
323                (any::<u64>(), any::<u64>(), arb_address(), any::<u64>()),
324                0..=4,
325            )
326            .prop_map(|withdrawals| {
327                Withdrawals::new(
328                    withdrawals
329                        .into_iter()
330                        .map(|(index, validator_index, address, amount)| Withdrawal {
331                            index,
332                            validator_index,
333                            address,
334                            amount,
335                        })
336                        .collect(),
337                )
338            }),
339        )
340    }
341
342    fn arb_block() -> impl Strategy<Value = Block> {
343        (
344            arb_header(),
345            prop::collection::vec(arb_tx(), 0..=8),
346            prop::collection::vec(arb_header(), 0..=2),
347            arb_withdrawals(),
348        )
349            .prop_map(|(mut header, transactions, ommers, withdrawals)| {
350                header.inner.withdrawals_root = withdrawals.as_ref().map(|_| B256::ZERO);
351                Block {
352                    header,
353                    body: BlockBody {
354                        transactions,
355                        ommers,
356                        withdrawals,
357                    },
358                }
359            })
360    }
361
362    fn legacy_tx(input: Bytes) -> TempoTxEnvelope {
363        TempoTxEnvelope::Legacy(Signed::new_unhashed(
364            TxLegacy {
365                chain_id: Some(1),
366                nonce: 0,
367                gas_price: 1,
368                gas_limit: 21_000,
369                to: Address::random().into(),
370                value: U256::ZERO,
371                input,
372            },
373            Signature::test_signature(),
374        ))
375    }
376
377    fn eip1559_tx(input: Bytes) -> TempoTxEnvelope {
378        TempoTxEnvelope::Eip1559(Signed::new_unhashed(
379            TxEip1559 {
380                chain_id: 1,
381                nonce: 1,
382                gas_limit: 21_000,
383                max_fee_per_gas: 2,
384                max_priority_fee_per_gas: 1,
385                to: Address::random().into(),
386                value: U256::ZERO,
387                access_list: Default::default(),
388                input,
389            },
390            Signature::test_signature(),
391        ))
392    }
393
394    fn encoded_block_transactions(transactions: &[TempoTxEnvelope]) -> EncodedBlockTransactionList {
395        let mut builder = EncodedBlockTransactionsBuilder::default();
396        let mut buf = Vec::new();
397        for transaction in transactions {
398            buf.clear();
399            transaction.encode_2718(&mut buf);
400            builder.push(transaction, &buf);
401        }
402        builder.finish()
403    }
404
405    fn full_block_encoding(block: &SealedBlock<Block>) -> Vec<u8> {
406        let mut expected = Vec::new();
407        block.encode(&mut expected);
408        expected
409    }
410
411    #[test]
412    fn encoded_block_transaction_list_matches_alloy_encoding() {
413        let transactions = vec![
414            legacy_tx(Bytes::from_static(b"legacy")),
415            eip1559_tx(Bytes::from_static(b"typed")),
416        ];
417
418        let encoded_transactions = encoded_block_transactions(&transactions);
419        let expected = alloy_rlp::encode(&transactions);
420
421        assert_eq!(encoded_transactions.transaction_count, transactions.len());
422        assert_eq!(encoded_transactions.rlp.as_ref(), expected.as_slice());
423    }
424
425    #[test]
426    fn cached_transaction_list_block_encoding_matches_full_block_encoding() {
427        let transactions = vec![
428            legacy_tx(Bytes::from_static(b"legacy")),
429            eip1559_tx(Bytes::from_static(b"typed")),
430        ];
431        let encoded_transactions = encoded_block_transactions(&transactions);
432        let block = SealedBlock::seal_slow(Block {
433            header: TempoHeader::default(),
434            body: BlockBody {
435                transactions,
436                ommers: vec![TempoHeader::default()],
437                withdrawals: Some(Withdrawals::new(vec![Withdrawal {
438                    index: 1,
439                    validator_index: 2,
440                    address: Address::random(),
441                    amount: 3,
442                }])),
443            },
444        });
445
446        let mut encoded_from_cache = Vec::new();
447        assert!(
448            encoded_transactions.encode_block_with_transactions(&block, &mut encoded_from_cache)
449        );
450
451        let mut expected = Vec::new();
452        block.encode(&mut expected);
453
454        assert_eq!(encoded_from_cache, expected);
455    }
456
457    #[test]
458    fn cached_transaction_list_block_encoding_falls_back_on_count_mismatch() {
459        let cached_transactions =
460            encoded_block_transactions(&[legacy_tx(Bytes::from_static(b"cached"))]);
461        let block_transactions = vec![
462            legacy_tx(Bytes::from_static(b"legacy")),
463            eip1559_tx(Bytes::from_static(b"typed")),
464        ];
465        let block = SealedBlock::seal_slow(Block {
466            header: TempoHeader::default(),
467            body: BlockBody {
468                transactions: block_transactions,
469                ommers: vec![TempoHeader::default()],
470                withdrawals: None,
471            },
472        });
473
474        let mut encoded = Vec::new();
475        assert!(!cached_transactions.encode_block_with_transactions(&block, &mut encoded));
476
477        let mut expected = Vec::new();
478        block.encode(&mut expected);
479
480        assert_eq!(encoded, expected);
481    }
482
483    proptest! {
484        #![proptest_config(ProptestConfig::with_cases(256))]
485
486        #[test]
487        fn proptest_encoded_block_transaction_list_matches_alloy_encoding(
488            transactions in prop::collection::vec(arb_tx(), 0..=16),
489        ) {
490            let encoded_transactions = encoded_block_transactions(&transactions);
491            let expected = alloy_rlp::encode(&transactions);
492
493            prop_assert_eq!(encoded_transactions.transaction_count, transactions.len());
494            prop_assert_eq!(encoded_transactions.rlp.as_ref(), expected.as_slice());
495        }
496
497        #[test]
498        fn proptest_cached_transaction_list_block_encoding_matches_full_block_encoding(
499            block in arb_block(),
500        ) {
501            let encoded_transactions = encoded_block_transactions(&block.body.transactions);
502            let block = SealedBlock::seal_slow(block);
503            let expected = full_block_encoding(&block);
504
505            let mut encoded = Vec::new();
506            prop_assert!(encoded_transactions.encode_block_with_transactions(
507                &block,
508                &mut encoded
509            ));
510
511            prop_assert_eq!(encoded, expected);
512        }
513
514        #[test]
515        fn proptest_cached_transaction_list_block_encoding_falls_back_to_full_block_encoding(
516            block in arb_block(),
517            cached_transactions in prop::collection::vec(arb_tx(), 0..=8),
518        ) {
519            prop_assume!(cached_transactions.len() != block.body.transactions.len());
520            let encoded_transactions = encoded_block_transactions(&cached_transactions);
521            let block = SealedBlock::seal_slow(block);
522            let expected = full_block_encoding(&block);
523
524            let mut encoded = Vec::new();
525            prop_assert!(!encoded_transactions.encode_block_with_transactions(
526                &block,
527                &mut encoded
528            ));
529
530            prop_assert_eq!(encoded, expected);
531        }
532
533        #[test]
534        fn proptest_execution_block_encoder_matches_full_block_encoding(block in arb_block()) {
535            let encoded_transactions = encoded_block_transactions(&block.body.transactions);
536            let expected_block = SealedBlock::seal_slow(block.clone());
537            let expected = full_block_encoding(&expected_block);
538            let senders = vec![Address::ZERO; block.body.transactions.len()];
539            let recovered_block = Arc::new(RecoveredBlock::new_unhashed(block, senders));
540            let encoder = ExecutionBlockEncoder::new(
541                recovered_block,
542                expected.len(),
543                encoded_transactions,
544            );
545
546            prop_assert_eq!(encoder.encode_block().as_ref(), expected.as_slice());
547        }
548    }
549}