arb_node/
consensus.rs

1//! Arbitrum consensus implementation.
2//!
3//! L2 blocks are validated by the sequencer and posted to L1.
4//! The consensus layer trusts the sequencer's block production
5//! and performs only basic structural validation.
6
7use std::{fmt::Debug, sync::Arc};
8
9use alloy_consensus::{TxReceipt, proofs::calculate_receipt_root};
10use alloy_primitives::Bloom;
11use reth_chainspec::{EthChainSpec, EthereumHardforks};
12use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
13use reth_execution_types::BlockExecutionResult;
14use reth_primitives_traits::{
15    Block, BlockHeader, GotExpected, NodePrimitives, Receipt, RecoveredBlock, SealedBlock,
16    SealedHeader, receipt::gas_spent_by_transactions,
17};
18
19/// Arbitrum consensus engine.
20///
21/// Trusts the sequencer for block validity. Performs minimal
22/// structural checks on headers.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ArbConsensus<CS> {
25    chain_spec: Arc<CS>,
26    verify_execution: bool,
27}
28
29impl<CS> ArbConsensus<CS> {
30    /// Consensus engine that trusts sequencer-produced blocks (the node default).
31    pub fn new(chain_spec: Arc<CS>) -> Self {
32        Self {
33            chain_spec,
34            verify_execution: false,
35        }
36    }
37
38    /// Consensus engine that re-checks execution output against the stored
39    /// header, for offline commands that re-execute persisted blocks.
40    pub fn new_verifying(chain_spec: Arc<CS>) -> Self {
41        Self {
42            chain_spec,
43            verify_execution: true,
44        }
45    }
46}
47
48impl<H, CS> HeaderValidator<H> for ArbConsensus<CS>
49where
50    H: BlockHeader,
51    CS: EthChainSpec<Header = H> + EthereumHardforks + Debug + Send + Sync,
52{
53    fn validate_header(&self, _header: &SealedHeader<H>) -> Result<(), ConsensusError> {
54        Ok(())
55    }
56
57    fn validate_header_against_parent(
58        &self,
59        _header: &SealedHeader<H>,
60        _parent: &SealedHeader<H>,
61    ) -> Result<(), ConsensusError> {
62        Ok(())
63    }
64}
65
66impl<B, CS> Consensus<B> for ArbConsensus<CS>
67where
68    B: Block,
69    CS: EthChainSpec<Header = B::Header> + EthereumHardforks + Debug + Send + Sync,
70{
71    fn validate_body_against_header(
72        &self,
73        _body: &B::Body,
74        _header: &SealedHeader<B::Header>,
75    ) -> Result<(), ConsensusError> {
76        Ok(())
77    }
78
79    fn validate_block_pre_execution(&self, _block: &SealedBlock<B>) -> Result<(), ConsensusError> {
80        Ok(())
81    }
82}
83
84impl<N, CS> FullConsensus<N> for ArbConsensus<CS>
85where
86    N: NodePrimitives,
87    CS: EthChainSpec<Header = N::BlockHeader> + EthereumHardforks + Debug + Send + Sync,
88{
89    fn validate_block_post_execution(
90        &self,
91        block: &RecoveredBlock<N::Block>,
92        result: &BlockExecutionResult<N::Receipt>,
93        receipt_root_bloom: Option<ReceiptRootBloom>,
94    ) -> Result<(), ConsensusError> {
95        if !self.verify_execution {
96            return Ok(());
97        }
98        verify_block_execution(block.header(), &result.receipts, receipt_root_bloom)
99    }
100}
101
102/// Check cumulative gas, receipts root, and logs bloom against the header.
103/// ArbOS blocks carry no EIP-7685 requests, so that branch is omitted.
104fn verify_block_execution<H, R>(
105    header: &H,
106    receipts: &[R],
107    receipt_root_bloom: Option<ReceiptRootBloom>,
108) -> Result<(), ConsensusError>
109where
110    H: BlockHeader,
111    R: Receipt,
112{
113    let cumulative_gas_used = receipts
114        .last()
115        .map(|r| r.cumulative_gas_used())
116        .unwrap_or(0);
117    if header.gas_used() != cumulative_gas_used {
118        return Err(ConsensusError::BlockGasUsed {
119            gas: GotExpected {
120                got: cumulative_gas_used,
121                expected: header.gas_used(),
122            },
123            gas_spent_by_tx: gas_spent_by_transactions(receipts),
124        });
125    }
126
127    let (receipts_root, logs_bloom) = receipt_root_bloom.unwrap_or_else(|| {
128        let with_bloom = receipts
129            .iter()
130            .map(TxReceipt::with_bloom_ref)
131            .collect::<Vec<_>>();
132        let root = calculate_receipt_root(&with_bloom);
133        let bloom = with_bloom
134            .iter()
135            .fold(Bloom::ZERO, |bloom, r| bloom | r.bloom_ref());
136        (root, bloom)
137    });
138
139    if receipts_root != header.receipts_root() {
140        return Err(ConsensusError::BodyReceiptRootDiff(
141            GotExpected {
142                got: receipts_root,
143                expected: header.receipts_root(),
144            }
145            .into(),
146        ));
147    }
148    if logs_bloom != header.logs_bloom() {
149        return Err(ConsensusError::BodyBloomLogDiff(
150            GotExpected {
151                got: logs_bloom,
152                expected: header.logs_bloom(),
153            }
154            .into(),
155        ));
156    }
157
158    Ok(())
159}
160
161#[cfg(test)]
162mod tests {
163    use alloy_consensus::{
164        Eip658Value, Header, Receipt as AlloyReceipt, TxReceipt, proofs::calculate_receipt_root,
165    };
166    use alloy_primitives::{B256, Bloom};
167    use arb_primitives::{ArbReceipt, ArbReceiptKind};
168    use reth_consensus::ConsensusError;
169
170    use super::verify_block_execution;
171
172    fn receipts() -> Vec<ArbReceipt> {
173        vec![ArbReceipt::new(ArbReceiptKind::Eip1559(AlloyReceipt {
174            status: Eip658Value::Eip658(true),
175            cumulative_gas_used: 21_000,
176            logs: Vec::new(),
177        }))]
178    }
179
180    fn matching_header(receipts: &[ArbReceipt]) -> Header {
181        let with_bloom = receipts
182            .iter()
183            .map(TxReceipt::with_bloom_ref)
184            .collect::<Vec<_>>();
185        Header {
186            gas_used: receipts
187                .last()
188                .map(|r| r.cumulative_gas_used())
189                .unwrap_or(0),
190            receipts_root: calculate_receipt_root(&with_bloom),
191            logs_bloom: with_bloom
192                .iter()
193                .fold(Bloom::ZERO, |b, r| b | r.bloom_ref()),
194            ..Default::default()
195        }
196    }
197
198    #[test]
199    fn accepts_matching_block() {
200        let receipts = receipts();
201        let header = matching_header(&receipts);
202        assert!(verify_block_execution(&header, &receipts, None).is_ok());
203    }
204
205    #[test]
206    fn rejects_gas_mismatch() {
207        let receipts = receipts();
208        let mut header = matching_header(&receipts);
209        header.gas_used += 1;
210        assert!(matches!(
211            verify_block_execution(&header, &receipts, None),
212            Err(ConsensusError::BlockGasUsed { .. })
213        ));
214    }
215
216    #[test]
217    fn rejects_receipts_root_mismatch() {
218        let receipts = receipts();
219        let mut header = matching_header(&receipts);
220        header.receipts_root = B256::ZERO;
221        assert!(matches!(
222            verify_block_execution(&header, &receipts, None),
223            Err(ConsensusError::BodyReceiptRootDiff(_))
224        ));
225    }
226
227    #[test]
228    fn rejects_logs_bloom_mismatch() {
229        let receipts = receipts();
230        let mut header = matching_header(&receipts);
231        header.logs_bloom = Bloom::from([1u8; 256]);
232        assert!(matches!(
233            verify_block_execution(&header, &receipts, None),
234            Err(ConsensusError::BodyBloomLogDiff(_))
235        ));
236    }
237}