trevm 0.34.2

A typestate API wrapper for the revm EVM implementation
Documentation
use alloy::{
    consensus::{proofs::ordered_trie_root_with_encoder, ReceiptEnvelope, TxReceipt},
    eips::eip2718::Encodable2718,
    primitives::{Address, Bloom, Bytes, Log, B256},
};
use std::sync::OnceLock;

/// Information externalized during block execution.
///
/// This struct is used to collect the results of executing a block of
/// transactions. It accumulates the receipts and senders of the transactions.
#[derive(Debug, Clone)]
pub struct BlockOutput<T: TxReceipt = ReceiptEnvelope> {
    /// The receipts of the transactions in the block, in order.
    receipts: Vec<T>,

    /// The senders of the transactions in the block, in order.
    senders: Vec<Address>,

    /// The logs bloom of the block.
    bloom: OnceLock<Bloom>,

    /// The receipt root of the block.
    receipt_root: OnceLock<B256>,
}

impl Default for BlockOutput {
    fn default() -> Self {
        Self::with_capacity(0)
    }
}

impl<T: TxReceipt<Log = alloy::primitives::Log>> BlockOutput<T> {
    /// Create a new block output with memory allocated to hold `capacity`
    /// transaction outcomes.
    pub fn with_capacity(capacity: usize) -> Self {
        Self {
            receipts: Vec::with_capacity(capacity),
            senders: Vec::with_capacity(capacity),
            bloom: Default::default(),
            receipt_root: Default::default(),
        }
    }

    fn seal_bloom(&self) {
        self.bloom.get_or_init(|| {
            self.receipts.iter().fold(Bloom::default(), |mut bloom, r| {
                bloom |= r.bloom();
                bloom
            })
        });
    }

    fn unseal(&mut self) {
        self.bloom.take();
        self.receipt_root.take();
    }

    /// Reserve memory for `capacity` transaction outcomes.
    pub fn reserve(&mut self, capacity: usize) {
        self.receipts.reserve(capacity);
        self.senders.reserve(capacity);
    }

    /// Get a reference to the receipts of the transactions in the block.
    #[allow(clippy::missing_const_for_fn)] // false positive
    pub fn receipts(&self) -> &[T] {
        &self.receipts
    }

    /// Get an iterator over the logs of the transactions in the block.
    pub fn logs(&self) -> impl Iterator<Item = &Log> {
        self.receipts.iter().flat_map(|r| r.logs())
    }

    /// Get the logs bloom of the block.
    pub fn logs_bloom(&self) -> Bloom {
        self.seal_bloom();
        *self.bloom.get().unwrap()
    }

    /// Get a reference the senders of the transactions in the block.
    #[allow(clippy::missing_const_for_fn)] // false positive
    pub fn senders(&self) -> &[Address] {
        &self.senders
    }

    /// Get the cumulative gas used in the block.
    pub fn cumulative_gas_used(&self) -> u64 {
        self.receipts().last().map(TxReceipt::cumulative_gas_used).unwrap_or_default()
    }

    /// Accumulate the result of a transaction execution. If `parse_deposits` is
    /// true, the logs of the transaction will be scanned for deposit events
    /// according to the [EIP-6110] specification.
    ///
    /// [EIP-6110]: https://eips.ethereum.org/EIPS/eip-6110
    pub fn push_result(&mut self, receipt: T, sender: Address) {
        self.unseal();
        self.push_receipt(receipt);
        self.push_sender(sender);
    }

    /// Push a receipt onto the list of receipts.
    fn push_receipt(&mut self, receipt: T) {
        self.receipts.push(receipt);
    }

    /// Push a sender onto the list of senders.
    fn push_sender(&mut self, sender: Address) {
        self.senders.push(sender);
    }

    /// Find deposits in the logs of the transactions in the block.
    pub fn find_deposit_requests(&self) -> impl Iterator<Item = Bytes> + use<'_, T> {
        crate::system::eip6110::check_logs_for_deposits(
            self.receipts().iter().flat_map(TxReceipt::logs),
        )
    }

    /// Deconstruct the block output into its parts, returning any memoized
    /// bloom and receipt root values.
    pub fn into_parts(self) -> (Vec<T>, Vec<Address>, Option<Bloom>, Option<B256>) {
        (self.receipts, self.senders, self.bloom.into_inner(), self.receipt_root.into_inner())
    }
}

impl<T: TxReceipt + Encodable2718> BlockOutput<T> {
    /// Seal the block output, computing and memoizing the logs bloom and
    /// receipt root in a single pass over the receipts. The block bloom is
    /// derived as a side effect of receipt root computation by accumulating
    /// per-receipt blooms during trie encoding.
    ///
    /// Subsequent calls to [`logs_bloom`] and [`receipt_root`] will return
    /// the memoized values without recomputation.
    ///
    /// [`logs_bloom`]: Self::logs_bloom
    /// [`receipt_root`]: Self::receipt_root
    pub fn seal(&self) {
        if self.bloom.get().is_some() && self.receipt_root.get().is_some() {
            return;
        }

        let mut block_bloom = Bloom::default();
        let root = ordered_trie_root_with_encoder(&self.receipts, |r, buf| {
            block_bloom |= r.bloom();
            r.encode_2718(buf);
        });

        self.bloom.get_or_init(|| block_bloom);
        self.receipt_root.get_or_init(|| root);
    }

    /// Get the receipt root of the block.
    pub fn receipt_root(&self) -> B256 {
        self.seal();
        *self.receipt_root.get().unwrap()
    }

    /// Seal and deconstruct the block output into its parts.
    pub fn into_sealed_parts(self) -> (Vec<T>, Vec<Address>, Bloom, B256) {
        self.seal();
        (
            self.receipts,
            self.senders,
            self.bloom.into_inner().expect("seal sets bloom"),
            self.receipt_root.into_inner().expect("seal sets receipt_root"),
        )
    }
}

impl<T: TxReceipt + PartialEq> PartialEq for BlockOutput<T> {
    fn eq(&self, other: &Self) -> bool {
        self.receipts == other.receipts && self.senders == other.senders
    }
}

impl<T: TxReceipt + Eq> Eq for BlockOutput<T> {}

#[cfg(test)]
mod tests {
    use super::*;
    use alloy::{
        consensus::{
            constants::EMPTY_ROOT_HASH, Receipt, ReceiptEnvelope, ReceiptWithBloom, TxType,
        },
        primitives::{b256, Address, Bloom},
    };

    fn envelope(tx_type: TxType, receipt: ReceiptWithBloom<Receipt>) -> ReceiptEnvelope {
        ReceiptEnvelope::from_typed(tx_type, receipt)
    }

    #[test]
    fn block_output_eq_with_one_populated_bloom() {
        let output_a = BlockOutput::default();
        let output_b = BlockOutput::default();
        output_a.logs_bloom();
        assert!(output_a.bloom.get().is_some());
        assert!(output_b.bloom.get().is_none());
        assert_eq!(output_a, output_b);
    }

    #[test]
    fn empty_receipt_root() {
        let output = BlockOutput::default();
        assert_eq!(output.receipt_root(), EMPTY_ROOT_HASH);
        assert_eq!(output.logs_bloom(), Bloom::ZERO);
    }

    #[test]
    fn seal_computes_bloom_and_root() {
        let output = BlockOutput::default();
        assert!(output.bloom.get().is_none());
        assert!(output.receipt_root.get().is_none());

        output.seal();

        assert!(output.bloom.get().is_some());
        assert!(output.receipt_root.get().is_some());
    }

    #[test]
    fn single_eip2930_receipt_root() {
        // Test vector from:
        // - https://github.com/alloy-rs/alloy/blob/main/crates/consensus/src/receipt/mod.rs (check_receipt_root_optimism)
        // - https://github.com/paradigmxyz/reth/blob/main/crates/ethereum/primitives/src/receipt.rs (check_receipt_root_optimism)
        let logs = vec![Log::new_unchecked(Address::ZERO, vec![], Default::default())];
        let bloom = Bloom::from(alloy::primitives::hex!(
            "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"
        ));

        let receipt = ReceiptWithBloom {
            receipt: Receipt {
                status: alloy::consensus::Eip658Value::success(),
                cumulative_gas_used: 102068,
                logs,
            },
            logs_bloom: bloom,
        };

        let mut output = BlockOutput::default();
        output.push_result(envelope(TxType::Eip2930, receipt), Address::ZERO);

        assert_eq!(
            output.receipt_root(),
            b256!("0xfe70ae4a136d98944951b2123859698d59ad251a381abc9960fa81cae3d0d4a0"),
        );
        assert_eq!(output.logs_bloom(), bloom);
    }

    #[test]
    fn mixed_receipt_types() {
        // Adapted from https://github.com/paradigmxyz/reth/blob/main/crates/engine/tree/src/tree/payload_processor/receipt_root_task.rs
        // (test_receipt_root_matches_standard_calculation)
        let default_receipt = || ReceiptWithBloom {
            receipt: Receipt {
                status: alloy::consensus::Eip658Value::success(),
                cumulative_gas_used: 0,
                logs: vec![],
            },
            logs_bloom: Bloom::ZERO,
        };

        let legacy = {
            let mut r = default_receipt();
            r.receipt.cumulative_gas_used = 21000;
            envelope(TxType::Legacy, r)
        };
        let eip1559 = {
            let mut r = default_receipt();
            r.receipt.cumulative_gas_used = 42000;
            r.receipt.logs =
                vec![Log::new_unchecked(Address::ZERO, vec![B256::ZERO], Bytes::new())];
            r.logs_bloom = alloy::consensus::TxReceipt::bloom(&r.receipt);
            envelope(TxType::Eip1559, r)
        };
        let eip2930 = {
            let mut r = default_receipt();
            r.receipt.cumulative_gas_used = 63000;
            r.receipt.status = alloy::consensus::Eip658Value::Eip658(false);
            envelope(TxType::Eip2930, r)
        };

        let mut output = BlockOutput::default();
        output.push_result(legacy, Address::ZERO);
        output.push_result(eip1559, Address::ZERO);
        output.push_result(eip2930, Address::ZERO);

        assert_eq!(
            output.receipt_root(),
            b256!("0xa4746a21d06f407a22200fed3491774c1b455736af092b7dcd7565de6b11da0c"),
        );
        assert_eq!(
            output.logs_bloom(),
            Bloom::from(alloy::primitives::hex!(
                "00000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000800000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000"
            )),
        );
    }

    #[test]
    fn into_sealed_parts_returns_all() {
        let receipt = ReceiptWithBloom {
            receipt: Receipt {
                status: alloy::consensus::Eip658Value::success(),
                cumulative_gas_used: 100,
                logs: vec![],
            },
            logs_bloom: Bloom::ZERO,
        };

        let mut output = BlockOutput::default();
        output.push_result(envelope(TxType::Legacy, receipt), Address::ZERO);

        let (receipts, senders, bloom, root) = output.into_sealed_parts();
        assert_eq!(receipts.len(), 1);
        assert_eq!(senders.len(), 1);
        assert_eq!(
            root,
            b256!("0x134447aac1b9bfa029f21da1cad6d6a71f841f16dc45134b24f14effe1efe791"),
        );
        assert_eq!(bloom, Bloom::ZERO);
    }

    #[test]
    fn into_parts_without_seal() {
        let mut output = BlockOutput::default();
        output.push_result(
            envelope(
                TxType::Legacy,
                ReceiptWithBloom {
                    receipt: Receipt {
                        status: alloy::consensus::Eip658Value::success(),
                        cumulative_gas_used: 100,
                        logs: vec![],
                    },
                    logs_bloom: Bloom::ZERO,
                },
            ),
            Address::ZERO,
        );

        let (receipts, senders, bloom, root) = output.into_parts();
        assert_eq!(receipts.len(), 1);
        assert_eq!(senders.len(), 1);
        assert!(bloom.is_none());
        assert!(root.is_none());
    }

    #[test]
    fn unseal_clears_memoized_values() {
        let mut output = BlockOutput::default();
        output.seal();
        assert!(output.bloom.get().is_some());
        assert!(output.receipt_root.get().is_some());

        output.push_result(
            envelope(
                TxType::Legacy,
                ReceiptWithBloom {
                    receipt: Receipt {
                        status: alloy::consensus::Eip658Value::success(),
                        cumulative_gas_used: 100,
                        logs: vec![],
                    },
                    logs_bloom: Bloom::ZERO,
                },
            ),
            Address::ZERO,
        );

        // push_result calls unseal
        assert!(output.bloom.get().is_none());
        assert!(output.receipt_root.get().is_none());
    }
}