1use crate::{TempoBlockExecutionCtx, evm::TempoEvm};
2use alloy_consensus::{Transaction, transaction::TxHashRef};
3use alloy_evm::{
4 Database, Evm,
5 block::{
6 BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockValidationError,
7 ExecutableTx, OnStateHook,
8 },
9 eth::{
10 EthBlockExecutor,
11 receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx},
12 },
13};
14use alloy_primitives::{Address, B256, Bytes, U256};
15use alloy_rlp::Decodable;
16use alloy_sol_types::SolCall;
17use commonware_codec::DecodeExt;
18use commonware_cryptography::{
19 Verifier,
20 ed25519::{PublicKey, Signature},
21};
22use reth_revm::{Inspector, State, context::result::ResultAndState};
23use revm::{
24 DatabaseCommit,
25 context::ContextTr,
26 state::{Account, Bytecode},
27};
28use std::collections::{HashMap, HashSet};
29use tempo_chainspec::{TempoChainSpec, hardfork::TempoHardforks};
30use tempo_precompiles::{
31 ACCOUNT_KEYCHAIN_ADDRESS, STABLECOIN_EXCHANGE_ADDRESS, TIP_FEE_MANAGER_ADDRESS,
32 TIP20_REWARDS_REGISTRY_ADDRESS, stablecoin_exchange::IStablecoinExchange,
33 tip_fee_manager::IFeeManager, tip20_rewards_registry::ITIP20RewardsRegistry,
34};
35use tempo_primitives::{
36 SubBlock, SubBlockMetadata, TempoReceipt, TempoTxEnvelope, subblock::PartialValidatorKey,
37};
38use tempo_revm::{TempoHaltReason, evm::TempoContext};
39use tracing::trace;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42enum BlockSection {
43 StartOfBlock { seen_tip20_rewards_registry: bool },
45 NonShared,
49 SubBlock { proposer: PartialValidatorKey },
51 GasIncentive,
53 System {
55 seen_fee_manager: bool,
56 seen_stablecoin_dex: bool,
57 seen_subblocks_signatures: bool,
58 },
59}
60
61#[derive(Debug, Clone, Copy, Default)]
63#[non_exhaustive]
64pub(crate) struct TempoReceiptBuilder;
65
66impl ReceiptBuilder for TempoReceiptBuilder {
67 type Transaction = TempoTxEnvelope;
68 type Receipt = TempoReceipt;
69
70 fn build_receipt<E: Evm>(
71 &self,
72 ctx: ReceiptBuilderCtx<'_, Self::Transaction, E>,
73 ) -> Self::Receipt {
74 let ReceiptBuilderCtx {
75 tx,
76 result,
77 cumulative_gas_used,
78 ..
79 } = ctx;
80 TempoReceipt {
81 tx_type: tx.tx_type(),
82 success: result.is_success(),
85 cumulative_gas_used,
86 logs: result.into_logs(),
87 }
88 }
89}
90
91pub(crate) struct TempoBlockExecutor<'a, DB: Database, I> {
93 pub(crate) inner: EthBlockExecutor<
94 'a,
95 TempoEvm<&'a mut State<DB>, I>,
96 &'a TempoChainSpec,
97 TempoReceiptBuilder,
98 >,
99
100 section: BlockSection,
101 seen_subblocks: Vec<(PartialValidatorKey, Vec<TempoTxEnvelope>)>,
102 validator_set: Option<Vec<B256>>,
103 shared_gas_limit: u64,
104 subblock_fee_recipients: HashMap<PartialValidatorKey, Address>,
105
106 non_shared_gas_left: u64,
107 non_payment_gas_left: u64,
108 incentive_gas_used: u64,
109}
110
111impl<'a, DB, I> TempoBlockExecutor<'a, DB, I>
112where
113 DB: Database,
114 I: Inspector<TempoContext<&'a mut State<DB>>>,
115{
116 pub(crate) fn new(
117 evm: TempoEvm<&'a mut State<DB>, I>,
118 ctx: TempoBlockExecutionCtx<'a>,
119 chain_spec: &'a TempoChainSpec,
120 ) -> Self {
121 Self {
122 incentive_gas_used: 0,
123 validator_set: ctx.validator_set,
124 non_payment_gas_left: ctx.general_gas_limit,
125 non_shared_gas_left: evm.block().gas_limit - ctx.shared_gas_limit,
126 shared_gas_limit: ctx.shared_gas_limit,
127 inner: EthBlockExecutor::new(
128 evm,
129 ctx.inner,
130 chain_spec,
131 TempoReceiptBuilder::default(),
132 ),
133 section: BlockSection::StartOfBlock {
134 seen_tip20_rewards_registry: false,
135 },
136 seen_subblocks: Vec::new(),
137 subblock_fee_recipients: ctx.subblock_fee_recipients,
138 }
139 }
140
141 fn validate_system_tx(
143 &self,
144 tx: &TempoTxEnvelope,
145 ) -> Result<BlockSection, BlockValidationError> {
146 let block = self.evm().block();
147 let block_timestamp = block.timestamp;
148 let block_number = block.number.to_be_bytes_vec();
149 let to = tx.to().unwrap_or_default();
150
151 if !self
152 .inner
153 .spec
154 .is_moderato_active_at_timestamp(block_timestamp.to::<u64>())
155 {
156 if let BlockSection::StartOfBlock {
159 seen_tip20_rewards_registry: false,
160 } = self.section
161 {
162 if to != TIP20_REWARDS_REGISTRY_ADDRESS {
163 return Err(BlockValidationError::msg(
164 "only rewards registry system transaction allowed at start of block",
165 ));
166 }
167
168 let finalize_streams_input = ITIP20RewardsRegistry::finalizeStreamsCall {}
169 .abi_encode()
170 .into_iter()
171 .chain(block_number)
172 .collect::<Bytes>();
173
174 if *tx.input() != finalize_streams_input {
175 return Err(BlockValidationError::msg(
176 "invalid TIP20 rewards registry system transaction",
177 ));
178 }
179
180 return Ok(BlockSection::StartOfBlock {
181 seen_tip20_rewards_registry: true,
182 });
183 }
184 }
185
186 let (mut seen_fee_manager, mut seen_stablecoin_dex, mut seen_subblocks_signatures) =
188 match self.section {
189 BlockSection::System {
190 seen_fee_manager,
191 seen_stablecoin_dex,
192 seen_subblocks_signatures,
193 } => (
194 seen_fee_manager,
195 seen_stablecoin_dex,
196 seen_subblocks_signatures,
197 ),
198 _ => (false, false, false),
199 };
200
201 if to == TIP_FEE_MANAGER_ADDRESS {
202 if seen_fee_manager {
203 return Err(BlockValidationError::msg(
204 "duplicate fee manager system transaction",
205 ));
206 }
207
208 let fee_input = IFeeManager::executeBlockCall
209 .abi_encode()
210 .into_iter()
211 .chain(block_number)
212 .collect::<Bytes>();
213
214 if *tx.input() != fee_input {
215 return Err(BlockValidationError::msg(
216 "invalid fee manager system transaction",
217 ));
218 }
219
220 seen_fee_manager = true;
221 } else if to == STABLECOIN_EXCHANGE_ADDRESS {
222 if seen_stablecoin_dex {
223 return Err(BlockValidationError::msg(
224 "duplicate stablecoin DEX system transaction",
225 ));
226 }
227
228 let dex_input = IStablecoinExchange::executeBlockCall {}
229 .abi_encode()
230 .into_iter()
231 .chain(block_number)
232 .collect::<Bytes>();
233
234 if *tx.input() != dex_input {
235 return Err(BlockValidationError::msg(
236 "invalid stablecoin DEX system transaction",
237 ));
238 }
239
240 seen_stablecoin_dex = true;
241 } else if to.is_zero() {
242 if seen_subblocks_signatures {
243 return Err(BlockValidationError::msg(
244 "duplicate subblocks metadata system transaction",
245 ));
246 }
247
248 if tx.input().len() < U256::BYTES
249 || tx.input()[tx.input().len() - U256::BYTES..] != block_number
250 {
251 return Err(BlockValidationError::msg(
252 "invalid subblocks metadata system transaction",
253 ));
254 }
255
256 let mut buf = &tx.input()[..tx.input().len() - U256::BYTES];
257 let Ok(metadata) = Vec::<SubBlockMetadata>::decode(&mut buf) else {
258 return Err(BlockValidationError::msg(
259 "invalid subblocks metadata system transaction",
260 ));
261 };
262
263 if !buf.is_empty() {
264 return Err(BlockValidationError::msg(
265 "invalid subblocks metadata system transaction",
266 ));
267 }
268
269 self.validate_shared_gas(&metadata)?;
270
271 seen_subblocks_signatures = true;
272 } else {
273 return Err(BlockValidationError::msg("invalid system transaction"));
274 }
275
276 Ok(BlockSection::System {
277 seen_fee_manager,
278 seen_stablecoin_dex,
279 seen_subblocks_signatures,
280 })
281 }
282
283 fn validate_shared_gas(
284 &self,
285 metadata: &[SubBlockMetadata],
286 ) -> Result<(), BlockValidationError> {
287 let Some(validator_set) = &self.validator_set else {
289 return Ok(());
290 };
291 let gas_per_subblock = self.shared_gas_limit / validator_set.len() as u64;
292
293 let mut incentive_gas = 0;
294 let mut seen = HashSet::new();
295 let mut next_non_empty = 0;
296 for metadata in metadata {
297 if !validator_set.contains(&metadata.validator) {
298 return Err(BlockValidationError::msg("invalid subblock validator"));
299 }
300
301 if !seen.insert(metadata.validator) {
302 return Err(BlockValidationError::msg(
303 "only one subblock per validator is allowed",
304 ));
305 }
306
307 let transactions = if let Some((validator, txs)) =
308 self.seen_subblocks.get(next_non_empty)
309 && validator.matches(metadata.validator)
310 {
311 next_non_empty += 1;
312 txs.clone()
313 } else {
314 Vec::new()
315 };
316
317 let reserved_gas = transactions.iter().map(|tx| tx.gas_limit()).sum::<u64>();
318
319 let signature_hash = SubBlock {
320 version: metadata.version,
321 fee_recipient: metadata.fee_recipient,
322 parent_hash: self.inner.ctx.parent_hash,
323 transactions: transactions.clone(),
324 }
325 .signature_hash();
326
327 let Ok(validator) = PublicKey::decode(&mut metadata.validator.as_ref()) else {
328 return Err(BlockValidationError::msg("invalid subblock validator"));
329 };
330
331 let Ok(signature) = Signature::decode(&mut metadata.signature.as_ref()) else {
332 return Err(BlockValidationError::msg(
333 "invalid subblock signature encoding",
334 ));
335 };
336
337 if !validator.verify(None, signature_hash.as_slice(), &signature) {
338 return Err(BlockValidationError::msg("invalid subblock signature"));
339 }
340
341 if reserved_gas > gas_per_subblock {
342 return Err(BlockValidationError::msg(
343 "subblock gas used exceeds gas per subblock",
344 ));
345 }
346
347 incentive_gas += gas_per_subblock - reserved_gas;
348 }
349
350 if next_non_empty != self.seen_subblocks.len() {
351 return Err(BlockValidationError::msg(
352 "failed to map all non-empty subblocks to metadata",
353 ));
354 }
355
356 if incentive_gas < self.incentive_gas_used {
357 return Err(BlockValidationError::msg("incentive gas limit exceeded"));
358 }
359
360 Ok(())
361 }
362
363 fn validate_tx(
364 &self,
365 tx: &TempoTxEnvelope,
366 gas_used: u64,
367 ) -> Result<BlockSection, BlockValidationError> {
368 let block = self.evm().block();
369 let block_timestamp = block.timestamp.to::<u64>();
370 let post_moderato = self
371 .inner
372 .spec
373 .is_moderato_active_at_timestamp(block_timestamp);
374
375 if tx.is_system_tx() {
377 self.validate_system_tx(tx)
378 } else if let Some(tx_proposer) = tx.subblock_proposer() {
379 match self.section {
380 BlockSection::StartOfBlock {
381 seen_tip20_rewards_registry,
382 } if !post_moderato && !seen_tip20_rewards_registry => {
383 Err(BlockValidationError::msg(
384 "TIP20 rewards registry system transaction was not seen",
385 ))
386 }
387 BlockSection::GasIncentive | BlockSection::System { .. } => {
388 Err(BlockValidationError::msg("subblock section already passed"))
389 }
390 BlockSection::StartOfBlock { .. } | BlockSection::NonShared => {
391 Ok(BlockSection::SubBlock {
392 proposer: tx_proposer,
393 })
394 }
395 BlockSection::SubBlock { proposer } => {
396 if proposer == tx_proposer
397 || !self.seen_subblocks.iter().any(|(p, _)| *p == tx_proposer)
398 {
399 Ok(BlockSection::SubBlock {
400 proposer: tx_proposer,
401 })
402 } else {
403 Err(BlockValidationError::msg(
404 "proposer's subblock already processed",
405 ))
406 }
407 }
408 }
409 } else {
410 match self.section {
411 BlockSection::StartOfBlock {
412 seen_tip20_rewards_registry,
413 } if !post_moderato && !seen_tip20_rewards_registry => {
414 Err(BlockValidationError::msg(
415 "TIP20 rewards registry system transaction was not seen",
416 ))
417 }
418 BlockSection::StartOfBlock { .. } | BlockSection::NonShared => {
419 if gas_used > self.non_shared_gas_left
420 || (!tx.is_payment() && gas_used > self.non_payment_gas_left)
421 {
422 Ok(BlockSection::GasIncentive)
426 } else {
427 Ok(BlockSection::NonShared)
428 }
429 }
430 BlockSection::SubBlock { .. } => {
431 Ok(BlockSection::GasIncentive)
434 }
435 BlockSection::GasIncentive => Ok(BlockSection::GasIncentive),
436 BlockSection::System { .. } => {
437 trace!(target: "tempo::block", tx_hash = ?*tx.tx_hash(), "Rejecting: regular transaction after system transaction");
438 Err(BlockValidationError::msg(
439 "regular transaction can't follow system transaction",
440 ))
441 }
442 }
443 }
444 }
445}
446
447impl<'a, DB, I> BlockExecutor for TempoBlockExecutor<'a, DB, I>
448where
449 DB: Database,
450 I: Inspector<TempoContext<&'a mut State<DB>>>,
451{
452 type Transaction = TempoTxEnvelope;
453 type Receipt = TempoReceipt;
454 type Evm = TempoEvm<&'a mut State<DB>, I>;
455
456 fn apply_pre_execution_changes(&mut self) -> Result<(), alloy_evm::block::BlockExecutionError> {
457 self.inner.apply_pre_execution_changes()?;
458
459 let block_timestamp = self.evm().block().timestamp.to::<u64>();
461 if self
462 .inner
463 .spec
464 .is_allegretto_active_at_timestamp(block_timestamp)
465 {
466 let evm = self.evm_mut();
467 let db = evm.ctx_mut().db_mut();
468
469 let acc = db
471 .load_cache_account(ACCOUNT_KEYCHAIN_ADDRESS)
472 .map_err(BlockExecutionError::other)?;
473
474 let mut acc_info = acc.account_info().unwrap_or_default();
476
477 if acc_info.is_empty_code_hash() {
479 let code = Bytecode::new_legacy(Bytes::from_static(&[0xef]));
481 acc_info.code_hash = code.hash_slow();
482 acc_info.code = Some(code);
483
484 let mut revm_acc: Account = acc_info.into();
486 revm_acc.mark_touch();
487
488 db.commit(HashMap::from_iter([(ACCOUNT_KEYCHAIN_ADDRESS, revm_acc)]));
491 }
492 }
493
494 Ok(())
495 }
496
497 fn execute_transaction_without_commit(
498 &mut self,
499 tx: impl ExecutableTx<Self>,
500 ) -> Result<ResultAndState<TempoHaltReason>, BlockExecutionError> {
501 let beneficiary = self.evm_mut().ctx_mut().block.beneficiary;
502 if self.evm().ctx().cfg.spec.is_allegretto()
504 && let Some(validator) = tx.tx().subblock_proposer()
505 {
506 let fee_recipient = *self
507 .subblock_fee_recipients
508 .get(&validator)
509 .ok_or(BlockExecutionError::msg("invalid subblock transaction"))?;
510
511 self.evm_mut().ctx_mut().block.beneficiary = fee_recipient;
512 }
513 let result = self.inner.execute_transaction_without_commit(tx);
514
515 self.evm_mut().ctx_mut().block.beneficiary = beneficiary;
516
517 result
518 }
519
520 fn commit_transaction(
521 &mut self,
522 output: ResultAndState<TempoHaltReason>,
523 tx: impl ExecutableTx<Self>,
524 ) -> Result<u64, BlockExecutionError> {
525 let next_section = self.validate_tx(tx.tx(), output.result.gas_used())?;
526
527 let gas_used = self.inner.commit_transaction(output, &tx)?;
528
529 let logs = self.inner.evm.take_revert_logs();
533 if !logs.is_empty() {
534 self.inner
535 .receipts
536 .last_mut()
537 .expect("receipt was just pushed")
538 .logs
539 .extend(logs);
540 }
541
542 self.section = next_section;
543
544 match self.section {
545 BlockSection::StartOfBlock { .. } => {
546 }
548 BlockSection::NonShared => {
549 self.non_shared_gas_left -= gas_used;
550 if !tx.tx().is_payment() {
551 self.non_payment_gas_left -= gas_used;
552 }
553 }
554 BlockSection::SubBlock { proposer } => {
555 let last_subblock = if let Some(last) = self
557 .seen_subblocks
558 .last_mut()
559 .filter(|(p, _)| *p == proposer)
560 {
561 last
562 } else {
563 self.seen_subblocks.push((proposer, Vec::new()));
564 self.seen_subblocks.last_mut().unwrap()
565 };
566
567 last_subblock.1.push(tx.tx().clone());
568 }
569 BlockSection::GasIncentive => {
570 self.incentive_gas_used += gas_used;
571 }
572 BlockSection::System { .. } => {
573 }
575 }
576
577 Ok(gas_used)
578 }
579
580 fn finish(
581 self,
582 ) -> Result<(Self::Evm, BlockExecutionResult<Self::Receipt>), BlockExecutionError> {
583 if self.section
585 != (BlockSection::System {
586 seen_fee_manager: true,
587 seen_stablecoin_dex: true,
588 seen_subblocks_signatures: true,
589 })
590 {
591 return Err(
592 BlockValidationError::msg("end-of-block system transactions not seen").into(),
593 );
594 }
595 self.inner.finish()
596 }
597
598 fn set_state_hook(&mut self, hook: Option<Box<dyn OnStateHook>>) {
599 self.inner.set_state_hook(hook)
600 }
601
602 fn evm_mut(&mut self) -> &mut Self::Evm {
603 self.inner.evm_mut()
604 }
605
606 fn evm(&self) -> &Self::Evm {
607 self.inner.evm()
608 }
609}