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