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