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    /// Constructs a new [`BlockAccessIndex`] from a 0-based transaction index.
36    ///
37    /// Transaction changes start at index `1` in a block access list, so transaction
38    /// index `0` maps to block access index `1`.
39    #[inline]
40    pub const fn from_tx_index(tx_index: u64) -> Self {
41        Self(tx_index + 1)
42    }
43
44    /// Returns the raw `u64` value.
45    #[inline]
46    pub const fn get(self) -> u64 {
47        self.0
48    }
49
50    /// Bumps the index by 1.
51    #[inline]
52    pub const fn increment(&mut self) {
53        self.0 += 1;
54    }
55
56    /// Bumps the index by 1, saturating at `u64::MAX` instead of overflowing.
57    #[inline]
58    pub const fn saturating_increment(&mut self) {
59        self.0 = self.0.saturating_add(1);
60    }
61
62    /// Classifies this index into a [`BlockAccessPhase`], given the number of transactions
63    /// in the block.
64    ///
65    /// Returns:
66    /// - `Some(BlockAccessPhase::PreExecution)` when the index is `0`.
67    /// - `Some(BlockAccessPhase::Transaction(i))` when the index is in `1..=tx_len`, with `i =
68    ///   index - 1` as a 0-based transaction index.
69    /// - `Some(BlockAccessPhase::PostExecution)` when the index is exactly `tx_len + 1`.
70    /// - `None` when the index is strictly greater than `tx_len + 1` (out of range for a block with
71    ///   `tx_len` transactions).
72    #[inline]
73    pub const fn phase(self, tx_len: usize) -> Option<BlockAccessPhase> {
74        // Widen `tx_len` to `u64` to compare against the index without risking
75        // truncation on 32-bit targets.
76        let tx_len_u64 = tx_len as u64;
77        if self.0 == 0 {
78            Some(BlockAccessPhase::PreExecution)
79        } else if self.0 <= tx_len_u64 {
80            // `self.0 >= 1` here, so the subtraction cannot underflow.
81            // Casting back to `usize` is safe because `self.0 - 1 < tx_len <= usize::MAX`.
82            Some(BlockAccessPhase::Transaction((self.0 - 1) as usize))
83        } else if self.0 == tx_len_u64 + 1 {
84            Some(BlockAccessPhase::PostExecution)
85        } else {
86            None
87        }
88    }
89}
90
91impl fmt::Display for BlockAccessIndex {
92    #[inline]
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        fmt::Display::fmt(&self.0, f)
95    }
96}
97
98impl fmt::LowerHex for BlockAccessIndex {
99    #[inline]
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        fmt::LowerHex::fmt(&self.0, f)
102    }
103}
104
105#[cfg(feature = "serde")]
106impl serde::Serialize for BlockAccessIndex {
107    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
108        alloy_primitives::U64::from(self.0).serialize(serializer)
109    }
110}
111
112#[cfg(feature = "serde")]
113impl<'de> serde::Deserialize<'de> for BlockAccessIndex {
114    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
115        alloy_primitives::U64::deserialize(deserializer).map(|value| Self(value.to()))
116    }
117}
118
119/// Classification of a [`BlockAccessIndex`] within a block.
120///
121/// See [`BlockAccessIndex::phase`] for how indices map to phases.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
124pub enum BlockAccessPhase {
125    /// Pre-execution slot (index `0`).
126    PreExecution,
127    /// Transaction execution slot. The inner value is the 0-based transaction index
128    /// within the block (i.e. `block_access_index - 1`).
129    Transaction(usize),
130    /// Post-execution slot (index `tx_len + 1`).
131    PostExecution,
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn pre_execution_is_index_zero() {
140        assert_eq!(BlockAccessIndex::new(0).phase(0), Some(BlockAccessPhase::PreExecution));
141        assert_eq!(BlockAccessIndex::new(0).phase(5), Some(BlockAccessPhase::PreExecution));
142        assert_eq!(BlockAccessIndex::PRE_EXECUTION.phase(5), Some(BlockAccessPhase::PreExecution));
143    }
144
145    #[test]
146    fn transaction_indices_are_one_based() {
147        assert_eq!(BlockAccessIndex::new(1).phase(3), Some(BlockAccessPhase::Transaction(0)));
148        assert_eq!(BlockAccessIndex::new(2).phase(3), Some(BlockAccessPhase::Transaction(1)));
149        assert_eq!(BlockAccessIndex::new(3).phase(3), Some(BlockAccessPhase::Transaction(2)));
150    }
151
152    #[test]
153    fn from_tx_index_offsets_by_one() {
154        assert_eq!(BlockAccessIndex::from_tx_index(0), BlockAccessIndex::new(1));
155        assert_eq!(BlockAccessIndex::from_tx_index(1), BlockAccessIndex::new(2));
156    }
157
158    #[test]
159    fn post_execution_is_tx_len_plus_one() {
160        assert_eq!(BlockAccessIndex::new(4).phase(3), Some(BlockAccessPhase::PostExecution));
161        assert_eq!(BlockAccessIndex::new(1).phase(0), Some(BlockAccessPhase::PostExecution));
162    }
163
164    #[test]
165    fn out_of_range_returns_none() {
166        assert_eq!(BlockAccessIndex::new(5).phase(3), None);
167        assert_eq!(BlockAccessIndex::new(u64::MAX).phase(3), None);
168    }
169
170    #[test]
171    fn empty_block_has_only_pre_and_post() {
172        assert_eq!(BlockAccessIndex::new(0).phase(0), Some(BlockAccessPhase::PreExecution));
173        assert_eq!(BlockAccessIndex::new(1).phase(0), Some(BlockAccessPhase::PostExecution));
174        assert_eq!(BlockAccessIndex::new(2).phase(0), None);
175    }
176
177    #[test]
178    fn increment_bumps_by_one() {
179        let mut idx = BlockAccessIndex::new(3);
180        idx.increment();
181        assert_eq!(idx, BlockAccessIndex::new(4));
182    }
183
184    #[test]
185    fn new_and_get_roundtrip() {
186        let idx = BlockAccessIndex::new(42);
187        assert_eq!(idx.get(), 42);
188    }
189
190    #[test]
191    fn display_matches_inner() {
192        extern crate alloc;
193        assert_eq!(alloc::format!("{}", BlockAccessIndex::new(7)), "7");
194        assert_eq!(alloc::format!("{:x}", BlockAccessIndex::new(255)), "ff");
195    }
196
197    #[cfg(feature = "serde")]
198    #[test]
199    fn serde_hex_quantity_roundtrip() {
200        let idx = BlockAccessIndex::new(26);
201        let json = serde_json::to_string(&idx).unwrap();
202        assert_eq!(json, "\"0x1a\"");
203        let back: BlockAccessIndex = serde_json::from_str(&json).unwrap();
204        assert_eq!(back, idx);
205    }
206
207    #[cfg(feature = "rlp")]
208    #[test]
209    fn rlp_matches_raw_u64() {
210        use alloy_rlp::Decodable;
211        let idx = BlockAccessIndex::new(300);
212        let encoded = alloy_rlp::encode(idx);
213        assert_eq!(encoded, alloy_rlp::encode(300u64));
214        let decoded = BlockAccessIndex::decode(&mut encoded.as_slice()).unwrap();
215        assert_eq!(decoded, idx);
216    }
217}