1#![no_std]
2#![forbid(unsafe_code)]
3#[cfg(feature = "std")]
6extern crate std;
7
8use core::{fmt, marker::PhantomData};
9
10use eth_valkyoth_primitives::{BlockNumber, ChainId, UnixTimestamp};
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum ProtocolError {
15 Feature(FeatureError),
17 Fork(ForkError),
19 InvalidStateTransition,
21}
22
23impl ProtocolError {
24 #[must_use]
26 pub const fn code(self) -> &'static str {
27 match self {
28 Self::Feature(error) => error.code(),
29 Self::Fork(error) => error.code(),
30 Self::InvalidStateTransition => "ETH_PROTOCOL_INVALID_STATE_TRANSITION",
31 }
32 }
33
34 #[must_use]
36 pub const fn message(self) -> &'static str {
37 match self {
38 Self::Feature(error) => error.message(),
39 Self::Fork(error) => error.message(),
40 Self::InvalidStateTransition => {
41 "validation state transition is not allowed from the current state"
42 }
43 }
44 }
45
46 #[must_use]
48 pub const fn category(self) -> ProtocolErrorCategory {
49 match self {
50 Self::Feature(_) => ProtocolErrorCategory::Feature,
51 Self::Fork(_) => ProtocolErrorCategory::Fork,
52 Self::InvalidStateTransition => ProtocolErrorCategory::State,
53 }
54 }
55}
56
57impl fmt::Display for ProtocolError {
58 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59 formatter.write_str(self.message())
60 }
61}
62
63#[cfg(feature = "std")]
64impl std::error::Error for ProtocolError {}
65
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum ProtocolErrorCategory {
69 Feature,
71 Fork,
73 State,
75}
76
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub enum FeatureError {
80 Disabled,
82 Unsupported,
84}
85
86impl FeatureError {
87 #[must_use]
89 pub const fn code(self) -> &'static str {
90 match self {
91 Self::Disabled => "ETH_FEATURE_DISABLED",
92 Self::Unsupported => "ETH_FEATURE_UNSUPPORTED",
93 }
94 }
95
96 #[must_use]
98 pub const fn message(self) -> &'static str {
99 match self {
100 Self::Disabled => "feature is disabled for this operation",
101 Self::Unsupported => "feature is not supported by this crate version",
102 }
103 }
104}
105
106impl fmt::Display for FeatureError {
107 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108 formatter.write_str(self.message())
109 }
110}
111
112#[cfg(feature = "std")]
113impl std::error::Error for FeatureError {}
114
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub enum ForkError {
118 Unsupported,
120 Inactive,
122 MissingActivation,
124}
125
126impl ForkError {
127 #[must_use]
129 pub const fn code(self) -> &'static str {
130 match self {
131 Self::Unsupported => "ETH_FORK_UNSUPPORTED",
132 Self::Inactive => "ETH_FORK_INACTIVE",
133 Self::MissingActivation => "ETH_FORK_MISSING_ACTIVATION",
134 }
135 }
136
137 #[must_use]
139 pub const fn message(self) -> &'static str {
140 match self {
141 Self::Unsupported => "fork is not supported by this crate version",
142 Self::Inactive => "fork is not active for the validation context",
143 Self::MissingActivation => "fork activation data is incomplete",
144 }
145 }
146}
147
148impl fmt::Display for ForkError {
149 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
150 formatter.write_str(self.message())
151 }
152}
153
154#[cfg(feature = "std")]
155impl std::error::Error for ForkError {}
156
157#[derive(Clone, Copy, Debug, Eq, PartialEq)]
159pub enum ForkActivation {
160 BlockOnly {
162 activation_block: BlockNumber,
164 },
165 BlockAndTimestamp {
167 activation_block: BlockNumber,
169 activation_timestamp: UnixTimestamp,
171 },
172}
173
174#[derive(Clone, Copy, Debug, Eq, PartialEq)]
176pub struct ForkSpec {
177 pub chain_id: ChainId,
179 pub activation: ForkActivation,
181}
182
183#[derive(Clone, Copy, Debug, Eq, PartialEq)]
185pub struct ValidationContext {
186 pub fork: ForkSpec,
188 pub block_number: BlockNumber,
190 pub timestamp: UnixTimestamp,
192}
193
194impl ValidationContext {
195 #[must_use]
197 pub fn fork_is_active(self) -> bool {
198 match self.fork.activation {
199 ForkActivation::BlockOnly { activation_block } => self.block_number >= activation_block,
200 ForkActivation::BlockAndTimestamp {
201 activation_block,
202 activation_timestamp,
203 } => self.block_number >= activation_block && self.timestamp >= activation_timestamp,
204 }
205 }
206}
207
208#[derive(Clone, Copy, Debug, Eq, PartialEq)]
210pub struct Decoded;
211
212#[derive(Clone, Copy, Debug, Eq, PartialEq)]
214pub struct Canonical;
215
216#[derive(Clone, Copy, Debug, Eq, PartialEq)]
218pub struct ForkValidated;
219
220#[derive(Clone, Copy, Debug, Eq, PartialEq)]
222pub struct SenderRecovered;
223
224#[derive(Clone, Copy, Debug, Eq, PartialEq)]
226pub struct Transaction<State> {
227 _state: PhantomData<State>,
228}
229
230impl Transaction<Decoded> {
231 #[must_use]
233 #[allow(dead_code)]
234 pub(crate) const fn decoded() -> Self {
235 Self {
236 _state: PhantomData,
237 }
238 }
239
240 #[must_use]
242 pub const fn into_canonical(self) -> Transaction<Canonical> {
243 Transaction {
244 _state: PhantomData,
245 }
246 }
247}
248
249impl Transaction<Canonical> {
250 #[must_use]
252 pub const fn into_fork_validated(self) -> Transaction<ForkValidated> {
253 Transaction {
254 _state: PhantomData,
255 }
256 }
257}
258
259impl Transaction<ForkValidated> {
260 #[must_use]
262 pub const fn into_sender_recovered(self) -> Transaction<SenderRecovered> {
263 Transaction {
264 _state: PhantomData,
265 }
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 extern crate std;
273 use std::string::ToString;
274
275 #[test]
276 fn activation_requires_block_and_time() {
277 let context = ValidationContext {
278 fork: ForkSpec {
279 chain_id: ChainId::new(1),
280 activation: ForkActivation::BlockAndTimestamp {
281 activation_block: BlockNumber::new(10),
282 activation_timestamp: UnixTimestamp::new(20),
283 },
284 },
285 block_number: BlockNumber::new(10),
286 timestamp: UnixTimestamp::new(19),
287 };
288 assert!(!context.fork_is_active());
289 }
290
291 #[test]
292 fn block_only_activation_ignores_timestamp() {
293 let context = ValidationContext {
294 fork: ForkSpec {
295 chain_id: ChainId::new(1),
296 activation: ForkActivation::BlockOnly {
297 activation_block: BlockNumber::new(10),
298 },
299 },
300 block_number: BlockNumber::new(10),
301 timestamp: UnixTimestamp::new(0),
302 };
303 assert!(context.fork_is_active());
304 }
305
306 #[test]
307 fn transaction_typestate_advances_in_order() {
308 let transaction = Transaction::decoded()
309 .into_canonical()
310 .into_fork_validated()
311 .into_sender_recovered();
312 assert_eq!(
313 transaction,
314 Transaction::<SenderRecovered> {
315 _state: PhantomData
316 }
317 );
318 }
319
320 #[test]
321 fn protocol_errors_have_stable_codes_and_categories() {
322 let error = ProtocolError::Fork(ForkError::Inactive);
323
324 assert_eq!(error.code(), "ETH_FORK_INACTIVE");
325 assert_eq!(
326 error.message(),
327 "fork is not active for the validation context"
328 );
329 assert_eq!(error.category(), ProtocolErrorCategory::Fork);
330 assert_eq!(
331 error.to_string(),
332 "fork is not active for the validation context"
333 );
334 }
335
336 #[test]
337 fn feature_errors_format_without_payloads() {
338 let error = ProtocolError::Feature(FeatureError::Unsupported);
339
340 assert_eq!(error.code(), "ETH_FEATURE_UNSUPPORTED");
341 assert_eq!(
342 FeatureError::Unsupported.to_string(),
343 "feature is not supported by this crate version"
344 );
345 }
346}