1use 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#[derive(Clone, Debug)]
23pub(crate) struct EncodedBlockTransactionList {
24 transaction_count: usize,
25 rlp: Bytes,
29}
30
31impl EncodedBlockTransactionList {
32 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 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#[derive(Debug, Default)]
79pub(crate) struct EncodedBlockTransactionsBuilder {
80 transaction_count: usize,
81 payload: Vec<u8>,
82}
83
84impl EncodedBlockTransactionsBuilder {
85 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#[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}