Skip to main content

alloy_eip7928/
block_access_index.rs

1//! Contains the [`BlockAccessIndex`] newtype and its [`BlockAccessPhase`] classification.
2
3use core::fmt;
4
5/// Block access index within a block.
6///
7/// A block's indices are laid out as:
8/// - `0` — pre-execution (system contract calls, block-level setup, ...)
9/// - `1..=tx_len` — transaction execution (transaction index `i` maps to index `i + 1`)
10/// - `tx_len + 1` — post-execution (block rewards, withdrawals, ...)
11///
12/// Stored as a `u64` internally, but wrapped as a newtype so it cannot be accidentally
13/// confused with other `u64` indices (for example, an `account_id` passed alongside it).
14///
15/// RLP, borsh, and arbitrary representations are identical to the wrapped `u64`. Serde
16/// serializes as a hex `"quantity"` string (e.g. `"0x1a"`), matching the previous
17/// `BlockAccessIndex = u64` alias behavior when paired with the `crate::quantity` module.
18#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
19#[cfg_attr(feature = "rlp", derive(alloy_rlp::RlpEncodableWrapper, alloy_rlp::RlpDecodableWrapper))]
20#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
21#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
22#[repr(transparent)]
23pub struct BlockAccessIndex(pub u64);
24
25impl BlockAccessIndex {
26    /// Pre-execution slot (index `0`).
27    pub const PRE_EXECUTION: Self = Self(0);
28
29    /// Constructs a new [`BlockAccessIndex`] from a raw `u64`.
30    #[inline]
31    pub const fn new(value: u64) -> Self {
32        Self(value)
33    }
34
35    /// Returns the raw `u64` value.
36    #[inline]
37    pub const fn get(self) -> u64 {
38        self.0
39    }
40
41    /// Bumps the index by 1.
42    #[inline]
43    pub const fn increment(&mut self) {
44        self.0 += 1;
45    }
46
47    /// Classifies this index into a [`BlockAccessPhase`], given the number of transactions
48    /// in the block.
49    ///
50    /// Returns:
51    /// - `Some(BlockAccessPhase::PreExecution)` when the index is `0`.
52    /// - `Some(BlockAccessPhase::Transaction(i))` when the index is in `1..=tx_len`, with `i =
53    ///   index - 1` as a 0-based transaction index.
54    /// - `Some(BlockAccessPhase::PostExecution)` when the index is exactly `tx_len + 1`.
55    /// - `None` when the index is strictly greater than `tx_len + 1` (out of range for a block with
56    ///   `tx_len` transactions).
57    #[inline]
58    pub const fn phase(self, tx_len: usize) -> Option<BlockAccessPhase> {
59        // Widen `tx_len` to `u64` to compare against the index without risking
60        // truncation on 32-bit targets.
61        let tx_len_u64 = tx_len as u64;
62        if self.0 == 0 {
63            Some(BlockAccessPhase::PreExecution)
64        } else if self.0 <= tx_len_u64 {
65            // `self.0 >= 1` here, so the subtraction cannot underflow.
66            // Casting back to `usize` is safe because `self.0 - 1 < tx_len <= usize::MAX`.
67            Some(BlockAccessPhase::Transaction((self.0 - 1) as usize))
68        } else if self.0 == tx_len_u64 + 1 {
69            Some(BlockAccessPhase::PostExecution)
70        } else {
71            None
72        }
73    }
74}
75
76impl fmt::Display for BlockAccessIndex {
77    #[inline]
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        fmt::Display::fmt(&self.0, f)
80    }
81}
82
83impl fmt::LowerHex for BlockAccessIndex {
84    #[inline]
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        fmt::LowerHex::fmt(&self.0, f)
87    }
88}
89
90#[cfg(feature = "serde")]
91impl serde::Serialize for BlockAccessIndex {
92    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
93        alloy_primitives::U64::from(self.0).serialize(serializer)
94    }
95}
96
97#[cfg(feature = "serde")]
98impl<'de> serde::Deserialize<'de> for BlockAccessIndex {
99    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
100        alloy_primitives::U64::deserialize(deserializer).map(|value| Self(value.to()))
101    }
102}
103
104/// Classification of a [`BlockAccessIndex`] within a block.
105///
106/// See [`BlockAccessIndex::phase`] for how indices map to phases.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
109pub enum BlockAccessPhase {
110    /// Pre-execution slot (index `0`).
111    PreExecution,
112    /// Transaction execution slot. The inner value is the 0-based transaction index
113    /// within the block (i.e. `block_access_index - 1`).
114    Transaction(usize),
115    /// Post-execution slot (index `tx_len + 1`).
116    PostExecution,
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn pre_execution_is_index_zero() {
125        assert_eq!(BlockAccessIndex::new(0).phase(0), Some(BlockAccessPhase::PreExecution));
126        assert_eq!(BlockAccessIndex::new(0).phase(5), Some(BlockAccessPhase::PreExecution));
127        assert_eq!(BlockAccessIndex::PRE_EXECUTION.phase(5), Some(BlockAccessPhase::PreExecution));
128    }
129
130    #[test]
131    fn transaction_indices_are_one_based() {
132        assert_eq!(BlockAccessIndex::new(1).phase(3), Some(BlockAccessPhase::Transaction(0)));
133        assert_eq!(BlockAccessIndex::new(2).phase(3), Some(BlockAccessPhase::Transaction(1)));
134        assert_eq!(BlockAccessIndex::new(3).phase(3), Some(BlockAccessPhase::Transaction(2)));
135    }
136
137    #[test]
138    fn post_execution_is_tx_len_plus_one() {
139        assert_eq!(BlockAccessIndex::new(4).phase(3), Some(BlockAccessPhase::PostExecution));
140        assert_eq!(BlockAccessIndex::new(1).phase(0), Some(BlockAccessPhase::PostExecution));
141    }
142
143    #[test]
144    fn out_of_range_returns_none() {
145        assert_eq!(BlockAccessIndex::new(5).phase(3), None);
146        assert_eq!(BlockAccessIndex::new(u64::MAX).phase(3), None);
147    }
148
149    #[test]
150    fn empty_block_has_only_pre_and_post() {
151        assert_eq!(BlockAccessIndex::new(0).phase(0), Some(BlockAccessPhase::PreExecution));
152        assert_eq!(BlockAccessIndex::new(1).phase(0), Some(BlockAccessPhase::PostExecution));
153        assert_eq!(BlockAccessIndex::new(2).phase(0), None);
154    }
155
156    #[test]
157    fn increment_bumps_by_one() {
158        let mut idx = BlockAccessIndex::new(3);
159        idx.increment();
160        assert_eq!(idx, BlockAccessIndex::new(4));
161    }
162
163    #[test]
164    fn new_and_get_roundtrip() {
165        let idx = BlockAccessIndex::new(42);
166        assert_eq!(idx.get(), 42);
167    }
168
169    #[test]
170    fn display_matches_inner() {
171        extern crate alloc;
172        assert_eq!(alloc::format!("{}", BlockAccessIndex::new(7)), "7");
173        assert_eq!(alloc::format!("{:x}", BlockAccessIndex::new(255)), "ff");
174    }
175
176    #[cfg(feature = "serde")]
177    #[test]
178    fn serde_hex_quantity_roundtrip() {
179        let idx = BlockAccessIndex::new(26);
180        let json = serde_json::to_string(&idx).unwrap();
181        assert_eq!(json, "\"0x1a\"");
182        let back: BlockAccessIndex = serde_json::from_str(&json).unwrap();
183        assert_eq!(back, idx);
184    }
185
186    #[cfg(feature = "rlp")]
187    #[test]
188    fn rlp_matches_raw_u64() {
189        use alloy_rlp::Decodable;
190        let idx = BlockAccessIndex::new(300);
191        let encoded = alloy_rlp::encode(idx);
192        assert_eq!(encoded, alloy_rlp::encode(300u64));
193        let decoded = BlockAccessIndex::decode(&mut encoded.as_slice()).unwrap();
194        assert_eq!(decoded, idx);
195    }
196}