1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
//! [EIP-7685]: General purpose execution layer requests
//!
//! [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685
use alloc::vec::Vec;
use alloy_primitives::{b256, Bytes, B256};
use derive_more::{Deref, DerefMut, From, IntoIterator};
/// The empty requests hash.
///
/// This is equivalent to `sha256("")`
pub const EMPTY_REQUESTS_HASH: B256 =
b256!("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
/// A container of EIP-7685 requests.
///
/// The container only holds the `requests` as defined by their respective EIPs. The first byte of
/// each element is the `request_type` and the remaining bytes are the `request_data`.
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash, Deref, DerefMut, From, IntoIterator)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Requests(Vec<Bytes>);
impl Requests {
/// Construct a new [`Requests`] container with the given capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self(Vec::with_capacity(capacity))
}
/// Construct a new [`Requests`] container.
///
/// This function assumes that the request type byte is already included as the
/// first byte in the provided `Bytes` blob.
pub const fn new(requests: Vec<Bytes>) -> Self {
Self(requests)
}
/// Add a new request into the container.
pub fn push_request(&mut self, request: Bytes) {
// Omit empty requests.
if request.len() == 1 {
return;
}
self.0.push(request);
}
/// Adds a new request with the given request type into the container.
pub fn push_request_with_type(
&mut self,
request_type: u8,
request: impl IntoIterator<Item = u8>,
) {
let mut request = request.into_iter().peekable();
// Omit empty requests.
if request.peek().is_none() {
return;
}
self.0.push(core::iter::once(request_type).chain(request).collect());
}
/// Consumes [`Requests`] and returns the inner raw opaque requests.
///
/// # Note
///
/// These requests include the `request_type` as the first byte in each
/// `Bytes` element, followed by the `requests_data`.
pub fn take(self) -> Vec<Bytes> {
self.0
}
/// Get an iterator over the requests.
pub fn iter(&self) -> core::slice::Iter<'_, Bytes> {
self.0.iter()
}
/// Calculate the requests hash as defined in EIP-7685 for the requests.
///
/// The requests hash is defined as
///
/// ```text
/// sha256(sha256(requests_0) ++ sha256(requests_1) ++ ...)
/// ```
///
/// Each request in the container is expected to already have the `request_type` prepended
/// to its corresponding `requests_data`. This function directly calculates the hash based
/// on the combined `request_type` and `requests_data`.
///
/// Empty requests are omitted from the hash calculation.
/// Requests are sorted by their `request_type` before hashing, see also [Ordering](https://eips.ethereum.org/EIPS/eip-7685#ordering)
#[cfg(feature = "sha2")]
pub fn requests_hash(&self) -> B256 {
use sha2::{Digest, Sha256};
let mut hash = Sha256::new();
let mut requests: Vec<_> = self.0.iter().filter(|req| !req.is_empty()).collect();
requests.sort_unstable_by_key(|req| {
// SAFETY: only includes non-empty requests
req[0]
});
for req in requests {
let mut req_hash = Sha256::new();
req_hash.update(req);
hash.update(req_hash.finalize());
}
B256::new(hash.finalize().into())
}
/// Extend this container with requests from another container.
pub fn extend(&mut self, other: Self) {
self.0.extend(other.take());
}
}
/// A list of requests or a precomputed requests hash.
///
/// For testing purposes, the `Hash` variant stores a precomputed requests hash. This can be useful
/// when the exact contents of the requests are unnecessary, and only a consistent hash value is
/// needed to simulate the presence of requests without holding actual data.
#[derive(Debug, Clone, PartialEq, Eq, Hash, derive_more::From)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RequestsOrHash {
/// Stores a list of requests, allowing for dynamic requests hash calculation.
Requests(Requests),
/// Stores a precomputed requests hash, used primarily for testing or mocking because the
/// header only contains the hash.
Hash(B256),
}
impl RequestsOrHash {
/// Returns the requests hash for the enum instance.
///
/// - If the instance contains a list of requests, this function calculates the hash using
/// `requests_hash` of the [`Requests`] struct.
/// - If it contains a precomputed hash, it returns that hash directly.
#[cfg(feature = "sha2")]
pub fn requests_hash(&self) -> B256 {
match self {
Self::Requests(requests) => requests.requests_hash(),
Self::Hash(precomputed_hash) => *precomputed_hash,
}
}
/// Returns an instance with the [`EMPTY_REQUESTS_HASH`].
pub const fn empty() -> Self {
Self::Hash(EMPTY_REQUESTS_HASH)
}
/// Returns the requests, if any.
pub const fn requests(&self) -> Option<&Requests> {
match self {
Self::Requests(requests) => Some(requests),
Self::Hash(_) => None,
}
}
}
impl Default for RequestsOrHash {
fn default() -> Self {
Self::Requests(Requests::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extend() {
// Test extending a Requests container with another Requests container
let mut reqs1 = Requests::new(vec![Bytes::from(vec![0x01, 0x02])]);
let reqs2 =
Requests::new(vec![Bytes::from(vec![0x03, 0x04]), Bytes::from(vec![0x05, 0x06])]);
// Extend reqs1 with reqs2
reqs1.extend(reqs2);
// Ensure the requests are correctly combined
assert_eq!(reqs1.0.len(), 3);
assert_eq!(
reqs1.0,
vec![
Bytes::from(vec![0x01, 0x02]),
Bytes::from(vec![0x03, 0x04]),
Bytes::from(vec![0x05, 0x06])
]
);
}
#[test]
#[cfg(feature = "sha2")]
fn test_consistent_requests_hash() {
// We test that the empty requests hash is consistent with the EIP-7685 definition.
assert_eq!(Requests::default().requests_hash(), EMPTY_REQUESTS_HASH);
// Test to hash a non-empty vector of requests.
assert_eq!(
Requests(vec![
Bytes::from(vec![0x00, 0x0a, 0x0b, 0x0c]),
Bytes::from(vec![0x01, 0x0d, 0x0e, 0x0f])
])
.requests_hash(),
b256!("be3a57667b9bb9e0275019c0faf0f415fdc8385a408fd03e13a5c50615e3530c"),
);
}
}