Skip to main content

alloy_rpc_types_beacon/
requests.rs

1#[cfg(feature = "ssz")]
2use alloy_eips::eip7685::Requests;
3use alloy_eips::{
4    eip6110::DepositRequest, eip7002::WithdrawalRequest, eip7251::ConsolidationRequest,
5};
6use serde::{Deserialize, Serialize};
7
8/// An Electra-compatible execution requests payload.
9#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
10#[serde(deny_unknown_fields)]
11#[serde(rename_all = "snake_case")]
12#[cfg_attr(feature = "ssz", derive(ssz_derive::Decode, ssz_derive::Encode))]
13pub struct ExecutionRequestsV4 {
14    /// The requested deposits.
15    pub deposits: Vec<DepositRequest>,
16    /// The requested withdrawals.
17    pub withdrawals: Vec<WithdrawalRequest>,
18    /// The requested consolidations.
19    pub consolidations: Vec<ConsolidationRequest>,
20}
21
22impl<'de> Deserialize<'de> for ExecutionRequestsV4 {
23    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
24    where
25        D: serde::Deserializer<'de>,
26    {
27        #[derive(Deserialize)]
28        struct Helper {
29            #[serde(default)]
30            deposits: Option<Vec<DepositRequest>>,
31            #[serde(default)]
32            withdrawals: Option<Vec<WithdrawalRequest>>,
33            #[serde(default)]
34            consolidations: Option<Vec<ConsolidationRequest>>,
35        }
36
37        let helper = Helper::deserialize(deserializer)?;
38
39        Ok(Self {
40            deposits: helper.deposits.unwrap_or_default(),
41            withdrawals: helper.withdrawals.unwrap_or_default(),
42            consolidations: helper.consolidations.unwrap_or_default(),
43        })
44    }
45}
46
47impl ExecutionRequestsV4 {
48    /// Convert the [ExecutionRequestsV4] into a [Requests].
49    #[cfg(feature = "ssz")]
50    pub fn to_requests(&self) -> Requests {
51        self.into()
52    }
53}
54
55#[cfg(feature = "ssz")]
56pub use ssz_requests_conversions::TryFromRequestsError;
57
58#[cfg(feature = "ssz")]
59mod ssz_requests_conversions {
60    use super::*;
61    use crate::requests::TryFromRequestsError::SszDecodeError;
62    use alloy_eips::{
63        eip6110::{DepositRequest, DEPOSIT_REQUEST_TYPE, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD},
64        eip7002::{WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_BLOCK, WITHDRAWAL_REQUEST_TYPE},
65        eip7251::{
66            ConsolidationRequest, CONSOLIDATION_REQUEST_TYPE, MAX_CONSOLIDATION_REQUESTS_PER_BLOCK,
67        },
68        eip7685::Requests,
69    };
70    use ssz::{Decode, DecodeError, Encode};
71
72    impl TryFrom<Requests> for ExecutionRequestsV4 {
73        type Error = TryFromRequestsError;
74
75        fn try_from(value: Requests) -> Result<Self, Self::Error> {
76            Self::try_from(&value)
77        }
78    }
79
80    impl TryFrom<&Requests> for ExecutionRequestsV4 {
81        type Error = TryFromRequestsError;
82
83        fn try_from(value: &Requests) -> Result<Self, Self::Error> {
84            #[derive(Default)]
85            struct RequestAccumulator {
86                deposits: Vec<DepositRequest>,
87                withdrawals: Vec<WithdrawalRequest>,
88                consolidations: Vec<ConsolidationRequest>,
89            }
90
91            impl RequestAccumulator {
92                fn parse_request_payload<T>(
93                    payload: &[u8],
94                    max_size: usize,
95                    request_type: u8,
96                ) -> Result<Vec<T>, TryFromRequestsError>
97                where
98                    T: Decode,
99                {
100                    let list: Vec<T> = Vec::from_ssz_bytes(payload)
101                        .map_err(|e| SszDecodeError(request_type, e))?;
102
103                    if list.len() > max_size {
104                        return Err(TryFromRequestsError::RequestPayloadSizeExceeded(
105                            request_type,
106                            list.len(),
107                        ));
108                    }
109
110                    Ok(list)
111                }
112
113                fn accumulate(mut self, request: &[u8]) -> Result<Self, TryFromRequestsError> {
114                    if request.is_empty() {
115                        return Err(TryFromRequestsError::EmptyRequest);
116                    }
117
118                    let (request_type, payload) =
119                        request.split_first().expect("already checked for empty");
120
121                    match *request_type {
122                        DEPOSIT_REQUEST_TYPE => {
123                            self.deposits = Self::parse_request_payload(
124                                payload,
125                                MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD,
126                                DEPOSIT_REQUEST_TYPE,
127                            )?;
128                        }
129                        WITHDRAWAL_REQUEST_TYPE => {
130                            self.withdrawals = Self::parse_request_payload(
131                                payload,
132                                MAX_WITHDRAWAL_REQUESTS_PER_BLOCK,
133                                WITHDRAWAL_REQUEST_TYPE,
134                            )?;
135                        }
136                        CONSOLIDATION_REQUEST_TYPE => {
137                            self.consolidations = Self::parse_request_payload(
138                                payload,
139                                MAX_CONSOLIDATION_REQUESTS_PER_BLOCK,
140                                CONSOLIDATION_REQUEST_TYPE,
141                            )?;
142                        }
143                        unknown => return Err(TryFromRequestsError::UnknownRequestType(unknown)),
144                    }
145
146                    Ok(self)
147                }
148            }
149
150            let accumulator = value
151                .iter()
152                .try_fold(RequestAccumulator::default(), |acc, request| acc.accumulate(request))?;
153
154            Ok(Self {
155                deposits: accumulator.deposits,
156                withdrawals: accumulator.withdrawals,
157                consolidations: accumulator.consolidations,
158            })
159        }
160    }
161
162    /// Errors possible converting a [Requests] to [ExecutionRequestsV4]
163    #[derive(Debug, thiserror::Error)]
164    pub enum TryFromRequestsError {
165        /// One of the Bytes is empty.
166        #[error("empty bytes in requests body")]
167        EmptyRequest,
168        /// Bytes prefix is not a known EIP-7685 request_type in Electra.
169        #[error("unknown request_type prefix: {0}")]
170        UnknownRequestType(u8),
171        /// Remaining bytes could not be decoded as SSZ requests_data.
172        #[error("ssz decode error for request_type {0}: {1:?}")]
173        SszDecodeError(u8, DecodeError),
174        /// Requests of request_type exceeds Electra size limits
175        #[error("requests_data payload for request_type {0} exceeds Electra size limit {1}")]
176        RequestPayloadSizeExceeded(u8, usize),
177    }
178
179    impl From<&ExecutionRequestsV4> for Requests {
180        fn from(val: &ExecutionRequestsV4) -> Self {
181            let deposit_bytes = val.deposits.as_ssz_bytes();
182            let withdrawals_bytes = val.withdrawals.as_ssz_bytes();
183            let consolidations_bytes = val.consolidations.as_ssz_bytes();
184
185            let mut requests = Self::with_capacity(3);
186            requests.push_request_with_type(DEPOSIT_REQUEST_TYPE, deposit_bytes);
187            requests.push_request_with_type(WITHDRAWAL_REQUEST_TYPE, withdrawals_bytes);
188            requests.push_request_with_type(CONSOLIDATION_REQUEST_TYPE, consolidations_bytes);
189            requests
190        }
191    }
192
193    #[cfg(test)]
194    mod tests {
195        use super::*;
196        use alloy_primitives::Bytes;
197        use std::str::FromStr;
198        #[test]
199        fn test_from_requests() -> Result<(), TryFromRequestsError> {
200            let original = Requests::new(vec![
201                // Taken from: https://github.com/ensi321/execution-apis/blob/88c08d6104e9e8ae1d369c2b26c393a0df599e9a/src/engine/openrpc/methods/payload.yaml#L554-L556
202                Bytes::from_str("0x0096a96086cff07df17668f35f7418ef8798079167e3f4f9b72ecde17b28226137cf454ab1dd20ef5d924786ab3483c2f9003f5102dabe0a27b1746098d1dc17a5d3fbd478759fea9287e4e419b3c3cef20100000000000000b1acdb2c4d3df3f1b8d3bfd33421660df358d84d78d16c4603551935f4b67643373e7eb63dcb16ec359be0ec41fee33b03a16e80745f2374ff1d3c352508ac5d857c6476d3c3bcf7e6ca37427c9209f17be3af5264c0e2132b3dd1156c28b4e9f000000000000000a5c85a60ba2905c215f6a12872e62b1ee037051364244043a5f639aa81b04a204c55e7cc851f29c7c183be253ea1510b001db70c485b6264692f26b8aeaab5b0c384180df8e2184a21a808a3ec8e86ca01000000000000009561731785b48cf1886412234531e4940064584463e96ac63a1a154320227e333fb51addc4a89b7e0d3f862d7c1fd4ea03bd8eb3d8806f1e7daf591cbbbb92b0beb74d13c01617f22c5026b4f9f9f294a8a7c32db895de3b01bee0132c9209e1f100000000000000").unwrap(),
203                Bytes::from_str("0x01a94f5374fce5edbc8e2a8697c15331677e6ebf0b85103a5617937691dfeeb89b86a80d5dc9e3c9d3a1a0e7ce311e26e0bb732eabaa47ffa288f0d54de28209a62a7d29d0000000000000000000000000000000000000000000000000000010f698daeed734da114470da559bd4b4c7259e1f7952555241dcbc90cf194a2ef676fc6005f3672fada2a3645edb297a75530100000000000000").unwrap(),
204                Bytes::from_str("0x02a94f5374fce5edbc8e2a8697c15331677e6ebf0b85103a5617937691dfeeb89b86a80d5dc9e3c9d3a1a0e7ce311e26e0bb732eabaa47ffa288f0d54de28209a62a7d29d098daeed734da114470da559bd4b4c7259e1f7952555241dcbc90cf194a2ef676fc6005f3672fada2a3645edb297a7553").unwrap(),
205            ]);
206
207            let requests = ExecutionRequestsV4::try_from(&original)?;
208            assert_eq!(requests.deposits.len(), 2);
209            assert_eq!(requests.withdrawals.len(), 2);
210            assert_eq!(requests.consolidations.len(), 1);
211
212            let round_trip: Requests = (&requests).into();
213            assert_eq!(original, round_trip);
214            Ok(())
215        }
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn deserde_requests_v4() {
225        let s = r#"{"deposits":null,"withdrawals":null,"consolidations":null}"#;
226        let requests: ExecutionRequestsV4 = serde_json::from_str(s).unwrap();
227        assert_eq!(requests, ExecutionRequestsV4::default());
228
229        let s = r#"{"deposits":null,"withdrawals":null}"#;
230        let requests: ExecutionRequestsV4 = serde_json::from_str(s).unwrap();
231        assert_eq!(requests, ExecutionRequestsV4::default());
232
233        let s = r#"{"deposits":[],"withdrawals":[],"consolidations":[]}"#;
234        let requests: ExecutionRequestsV4 = serde_json::from_str(s).unwrap();
235        assert_eq!(requests, ExecutionRequestsV4::default());
236    }
237}