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