Skip to main content

ethrex_common/types/
requests.rs

1use crate::H256;
2use bytes::Bytes;
3use ethereum_types::Address;
4use ethrex_rlp::{decode::RLPDecode, encode::RLPEncode, error::RLPDecodeError};
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7use tracing::error;
8
9use super::{Bytes48, Receipt};
10use crate::constants::DEPOSIT_TOPIC;
11use crate::serde_utils;
12
13pub type Bytes32 = [u8; 32];
14pub type Bytes96 = [u8; 96];
15const DEPOSIT_TYPE: u8 = 0x00;
16const WITHDRAWAL_TYPE: u8 = 0x01;
17const CONSOLIDATION_TYPE: u8 = 0x02;
18// EIP-8282 builder deposit/exit request types, per EELS amsterdam `requests.py`
19// (BUILDER_DEPOSIT_REQUEST_TYPE = 0x03, BUILDER_EXIT_REQUEST_TYPE = 0x04).
20const BUILDER_DEPOSIT_TYPE: u8 = 0x03;
21const BUILDER_EXIT_TYPE: u8 = 0x04;
22
23#[derive(Clone, Debug)]
24pub struct EncodedRequests(pub Bytes);
25
26impl EncodedRequests {
27    pub fn is_empty(&self) -> bool {
28        self.0.len() <= 1
29    }
30}
31
32impl<'de> Deserialize<'de> for EncodedRequests {
33    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
34    where
35        D: serde::Deserializer<'de>,
36    {
37        Ok(EncodedRequests(serde_utils::bytes::deserialize(
38            deserializer,
39        )?))
40    }
41}
42
43impl Serialize for EncodedRequests {
44    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
45    where
46        S: serde::Serializer,
47    {
48        serde_utils::bytes::serialize(&self.0, serializer)
49    }
50}
51
52impl RLPEncode for EncodedRequests {
53    fn encode(&self, buf: &mut dyn bytes::BufMut) {
54        self.0.encode(buf)
55    }
56}
57
58impl RLPDecode for EncodedRequests {
59    fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> {
60        let (bytes, rest) = RLPDecode::decode_unfinished(rlp)?;
61        Ok((EncodedRequests(bytes), rest))
62    }
63}
64
65#[derive(Clone, Debug)]
66pub enum Requests {
67    Deposit(Vec<Deposit>),
68    Withdrawal(Vec<u8>),
69    Consolidation(Vec<u8>),
70    BuilderDeposit(Vec<u8>),
71    BuilderExit(Vec<u8>),
72}
73
74impl Requests {
75    pub fn encode(&self) -> EncodedRequests {
76        let bytes: Vec<u8> = match self {
77            Requests::Deposit(deposits) => {
78                let deposit_data = deposits.iter().flat_map(|d| d.to_summarized_byte_array());
79                std::iter::once(DEPOSIT_TYPE).chain(deposit_data).collect()
80            }
81            Requests::Withdrawal(data) => std::iter::once(WITHDRAWAL_TYPE)
82                .chain(data.iter().cloned())
83                .collect(),
84            Requests::Consolidation(data) => std::iter::once(CONSOLIDATION_TYPE)
85                .chain(data.iter().cloned())
86                .collect(),
87            Requests::BuilderDeposit(data) => std::iter::once(BUILDER_DEPOSIT_TYPE)
88                .chain(data.iter().cloned())
89                .collect(),
90            Requests::BuilderExit(data) => std::iter::once(BUILDER_EXIT_TYPE)
91                .chain(data.iter().cloned())
92                .collect(),
93        };
94
95        EncodedRequests(Bytes::from(bytes))
96    }
97
98    /// Returns None if any of the deposit requests couldn't be parsed
99    pub fn from_deposit_receipts(
100        deposit_contract_address: Address,
101        receipts: &[Receipt],
102    ) -> Option<Requests> {
103        let mut deposits = vec![];
104
105        for r in receipts {
106            for log in &r.logs {
107                if log.address == deposit_contract_address
108                    && log
109                        .topics
110                        .first()
111                        .is_some_and(|topic| topic == &*DEPOSIT_TOPIC)
112                {
113                    deposits.push(Deposit::from_abi_byte_array(&log.data)?);
114                }
115            }
116        }
117        Some(Self::Deposit(deposits))
118    }
119
120    pub fn from_withdrawals_data(data: Vec<u8>) -> Requests {
121        Requests::Withdrawal(data)
122    }
123
124    pub fn from_consolidation_data(data: Vec<u8>) -> Requests {
125        Requests::Consolidation(data)
126    }
127
128    pub fn from_builder_deposit_data(data: Vec<u8>) -> Requests {
129        Requests::BuilderDeposit(data)
130    }
131
132    pub fn from_builder_exit_data(data: Vec<u8>) -> Requests {
133        Requests::BuilderExit(data)
134    }
135}
136
137#[derive(Debug, Clone)]
138pub struct Deposit {
139    pub pub_key: Bytes48,
140    pub withdrawal_credentials: Bytes32,
141    pub amount: u64,
142    pub signature: Bytes96,
143    pub index: u64,
144}
145
146// Followed and ported implementation from:
147// https://github.com/lightclient/go-ethereum/blob/5c4d46f3614d26654241849da7dfd46b95eed1c6/core/types/deposit.go#L61
148impl Deposit {
149    pub fn from_abi_byte_array(data: &[u8]) -> Option<Deposit> {
150        if data.len() != 576 {
151            error!("Wrong data length when parsing deposit.");
152            return None;
153        }
154
155        // Encoding scheme:
156        //
157        // positional arguments -> 5 parameters with uint256 positional value for each -> 160b
158        // pub_key: 32b of len + 48b padded to 64b
159        // withdrawal_credentials: 32b of len + 32b
160        // amount: 32b of len + 8b padded to 32b
161        // signature: 32b of len + 96b
162        // index: 32b of len + 8b padded to 32b
163        //
164        // -> Total len: 576 bytes
165
166        const WORD: usize = 32;
167        const U32_TAIL: usize = WORD - 4;
168
169        const PUB_KEY_OFFSET: u32 = 160;
170        const WITHDRAWAL_CREDENTIALS_OFFSET: u32 = 256;
171        const AMOUNT_OFFSET: u32 = 320;
172        const SIGNATURE_OFFSET: u32 = 384;
173        const INDEX_OFFSET: u32 = 512;
174
175        const OFFSETS: [u32; 5] = [
176            PUB_KEY_OFFSET,
177            WITHDRAWAL_CREDENTIALS_OFFSET,
178            AMOUNT_OFFSET,
179            SIGNATURE_OFFSET,
180            INDEX_OFFSET,
181        ];
182
183        const PUB_KEY_SIZE: u32 = 48;
184        const WITHDRAWAL_CREDENTIALS_SIZE: u32 = 32;
185        const AMOUNT_SIZE: u32 = 8;
186        const SIGNATURE_SIZE: u32 = 96;
187        const INDEX_SIZE: u32 = 8;
188
189        const SIZES: [u32; 5] = [
190            PUB_KEY_SIZE,
191            WITHDRAWAL_CREDENTIALS_SIZE,
192            AMOUNT_SIZE,
193            SIGNATURE_SIZE,
194            INDEX_SIZE,
195        ];
196
197        // Validate Offsets & Sizes
198        for (i, (expected_offset, expected_size)) in
199            OFFSETS.into_iter().zip(SIZES.into_iter()).enumerate()
200        {
201            let offset = fixed_bytes::<WORD>(data, i * WORD)?;
202            let size = fixed_bytes::<WORD>(data, expected_offset as usize)?;
203            if offset[U32_TAIL..] != expected_offset.to_be_bytes()
204                || size[U32_TAIL..] != expected_size.to_be_bytes()
205            {
206                return None;
207            }
208        }
209
210        // Extract Data
211        let pub_key: Bytes48 =
212            fixed_bytes::<{ PUB_KEY_SIZE as usize }>(data, PUB_KEY_OFFSET as usize + WORD)?;
213        let withdrawal_credentials: Bytes32 =
214            fixed_bytes::<{ WITHDRAWAL_CREDENTIALS_SIZE as usize }>(
215                data,
216                WITHDRAWAL_CREDENTIALS_OFFSET as usize + WORD,
217            )?;
218        let amount: u64 = u64::from_le_bytes(fixed_bytes::<{ AMOUNT_SIZE as usize }>(
219            data,
220            AMOUNT_OFFSET as usize + WORD,
221        )?);
222        let signature: Bytes96 =
223            fixed_bytes::<{ SIGNATURE_SIZE as usize }>(data, SIGNATURE_OFFSET as usize + WORD)?;
224        let index: u64 = u64::from_le_bytes(fixed_bytes::<{ INDEX_SIZE as usize }>(
225            data,
226            INDEX_OFFSET as usize + WORD,
227        )?);
228
229        Some(Deposit {
230            pub_key,
231            withdrawal_credentials,
232            amount,
233            signature,
234            index,
235        })
236    }
237
238    pub fn to_summarized_byte_array(&self) -> [u8; 192] {
239        let mut buffer = [0u8; 192];
240        // pub_key + withdrawal_credentials + amount + signature + index
241        let mut p = 0;
242        buffer[p..48].clone_from_slice(&self.pub_key);
243        p += 48;
244        buffer[p..p + 32].clone_from_slice(&self.withdrawal_credentials);
245        p += 32;
246        buffer[p..p + 8].clone_from_slice(&self.amount.to_le_bytes());
247        p += 8;
248        buffer[p..p + 96].clone_from_slice(&self.signature);
249        p += 96;
250        buffer[p..p + 8].clone_from_slice(&self.index.to_le_bytes());
251
252        buffer
253    }
254}
255
256fn fixed_bytes<const N: usize>(data: &[u8], offset: usize) -> Option<[u8; N]> {
257    data.get(offset..offset + N)?.try_into().ok()
258}
259
260// See https://github.com/ethereum/EIPs/blob/2a6b6965e64787815f7fffb9a4c27660d9683846/EIPS/eip-7685.md?plain=1#L62.
261pub fn compute_requests_hash(requests: &[EncodedRequests]) -> H256 {
262    let mut hasher = Sha256::new();
263    for request in requests {
264        let request_bytes = request.0.as_ref();
265        if request_bytes.len() > 1 {
266            hasher.update(Sha256::digest(request_bytes));
267        }
268    }
269    H256::from_slice(&hasher.finalize())
270}